Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 59 additions & 28 deletions lib/ruby_lsp/requests/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,40 +19,55 @@ def initialize(document, global_state, position, dispatcher, sorbet_level)
char_position, _ = document.find_index_by_position(position)
delegate_request_if_needed!(global_state, document, char_position)

node_types = [
Prism::CallNode,
Prism::ConstantReadNode,
Prism::ConstantPathNode,
Prism::BlockArgumentNode,
Prism::GlobalVariableAndWriteNode,
Prism::GlobalVariableOperatorWriteNode,
Prism::GlobalVariableOrWriteNode,
Prism::GlobalVariableReadNode,
Prism::GlobalVariableTargetNode,
Prism::GlobalVariableWriteNode,
Prism::InstanceVariableReadNode,
Prism::InstanceVariableAndWriteNode,
Prism::InstanceVariableOperatorWriteNode,
Prism::InstanceVariableOrWriteNode,
Prism::InstanceVariableTargetNode,
Prism::InstanceVariableWriteNode,
Prism::SymbolNode,
Prism::StringNode,
Prism::SuperNode,
Prism::ForwardingSuperNode,
Prism::ClassVariableAndWriteNode,
Prism::ClassVariableOperatorWriteNode,
Prism::ClassVariableOrWriteNode,
Prism::ClassVariableReadNode,
Prism::ClassVariableTargetNode,
Prism::ClassVariableWriteNode,
]

node_context = RubyDocument.locate(
document.ast,
char_position,
node_types: [
Prism::CallNode,
Prism::ConstantReadNode,
Prism::ConstantPathNode,
Prism::BlockArgumentNode,
Prism::GlobalVariableAndWriteNode,
Prism::GlobalVariableOperatorWriteNode,
Prism::GlobalVariableOrWriteNode,
Prism::GlobalVariableReadNode,
Prism::GlobalVariableTargetNode,
Prism::GlobalVariableWriteNode,
Prism::InstanceVariableReadNode,
Prism::InstanceVariableAndWriteNode,
Prism::InstanceVariableOperatorWriteNode,
Prism::InstanceVariableOrWriteNode,
Prism::InstanceVariableTargetNode,
Prism::InstanceVariableWriteNode,
Prism::SymbolNode,
Prism::StringNode,
Prism::SuperNode,
Prism::ForwardingSuperNode,
Prism::ClassVariableAndWriteNode,
Prism::ClassVariableOperatorWriteNode,
Prism::ClassVariableOrWriteNode,
Prism::ClassVariableReadNode,
Prism::ClassVariableTargetNode,
Prism::ClassVariableWriteNode,
],
node_types: node_types,
code_units_cache: document.code_units_cache,
)

if char_position.positive? &&
(node_context.node.is_a?(Prism::ProgramNode) || position_outside_target?(position, node_context.node))
previous_node_context = RubyDocument.locate(
document.ast,
char_position - 1,
node_types: node_types,
code_units_cache: document.code_units_cache,
)
if reference_ends_at_position?(previous_node_context.node, char_position, document)
node_context = previous_node_context
end
end

target = node_context.node
parent = node_context.parent

Expand Down Expand Up @@ -99,6 +114,22 @@ def perform

private

#: (Prism::Node?, Integer, RubyDocument | ERBDocument) -> bool
def reference_ends_at_position?(node, char_position, document)
return false unless node && !node.is_a?(Prism::ProgramNode)

location = case node
when Prism::CallNode
node.message_loc
when Prism::ConstantPathNode
node.name_loc
else
node.location
end

location&.cached_end_code_units_offset(document.code_units_cache) == char_position
end

#: (Hash[Symbol, untyped] position, Prism::Node? target) -> bool
def position_outside_target?(position, target)
case target
Expand Down
118 changes: 118 additions & 0 deletions test/requests/definition_expectations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,124 @@ class Baz
end
end

def test_go_to_definition_when_cursor_is_after_reference
source = <<~RUBY
class Foo; end

Foo
RUBY

with_server(source, stub_no_typechecker: true) do |server, uri|
server.process_message(
id: 1,
method: "textDocument/definition",
params: { textDocument: { uri: uri }, position: { line: 2, character: 3 } },
)

response = server.pop_response.response

refute_empty(response)
assert_equal(uri.to_s, response.first.attributes[:targetUri])
target_range = response.first.attributes[:targetRange].attributes
assert_equal(0, target_range[:start].to_hash[:line])
end
end

def test_go_to_definition_when_cursor_is_after_method_reference
source = <<~RUBY
class Bar
def self.something; end
end

Bar.something
RUBY

with_server(source, stub_no_typechecker: true) do |server, uri|
server.process_message(
id: 1,
method: "textDocument/definition",
params: { textDocument: { uri: uri }, position: { line: 4, character: 13 } },
)

response = server.pop_response.response

refute_empty(response)
assert_equal(uri.to_s, response.first.attributes[:targetUri])
target_range = response.first.attributes[:targetRange].attributes
assert_equal(1, target_range[:start].to_hash[:line])
end
end

def test_go_to_definition_at_reference_boundaries
constants = "module Foo\n class Bar; end\nend\n"
method = "def consume(value); end\n"
cases = {
"bare constant" => ["class Foo; end\nFoo|\n", 0],
"method reference" => ["class Bar\n def self.something; end\nend\nBar.something|\n", 1],
"qualified constant" => [constants + "Foo::Bar|\n", 1],
"absolute constant" => ["class Foo; end\n::Foo|\n", 0],
"absolute qualified constant" => [constants + "::Foo::Bar|\n", 1],
"deep qualified constant" => [
"module Outer\n module Inner\n class Leaf; end\n end\nend\nOuter::Inner::Leaf|\n", 2,
],
"lexically nested constant" => ["module Foo\n class Bar; end\n Bar|\nend\n", 1],
"qualified argument" => [method + constants + "consume(Foo::Bar|)\n", 2],
"bare argument" => ["class Foo; end\n" + method + "consume(Foo|)\n", 0],
"array element" => [constants + "[Foo::Bar|]\n", 1],
"before whitespace" => [constants + "Foo::Bar| \n", 1],
"after whitespace" => [constants + "Foo::Bar |\n", nil],
"unknown constant" => [constants + "Foo::Missing|\n", nil],
"partial constant" => [constants + "Foo::Ba|\n", nil],
"inside a constant" => [constants + "Foo::Ba|r\n", 1],
"start of terminal constant" => [constants + "Foo::|Bar\n", 1],
"preserves the current namespace separator target" => [constants + "Foo|::Bar\n", 1],
"incomplete constant path" => [constants + "Foo::|\n", nil],
"line start" => [constants + "|Foo::Bar\n", 0],
"multibyte constant" => ["module Fほげ\n class Bar; end\nend\nFほげ::Bar|\n", 1],
"surrogate pair before reference" => [constants + '"😀"; Foo::Bar|' + "\n", 1],
"argument after whitespace" => ["class Foo; end\n" + method + "consume(Foo |)\n", nil],
"unknown argument" => ["class Foo; end\n" + method + "consume(Missing|)\n", nil],
"call name" => ["class Host\n def self.consume(value); end\nend\nHost.consume|(1)\n", 1],
"after call parentheses" => [method + "consume(1)|\n", nil],
"after a constant argument call" => [method + constants + "consume(Foo::Bar)|\n", nil],
"numeric argument" => [method + "consume(1|)\n", nil],
"string argument" => [method + 'consume("Foo"|)' + "\n", nil],
"symbol argument" => [method + "consume(:Foo|)\n", nil],
"inside a comment" => [constants + "Foo::Bar # comment|\n", nil],
"after array delimiter" => [constants + "[Foo::Bar]|\n", nil],
"write target" => [constants + "Foo::Bar| = nil\n", 1],
"assignment value" => [constants + "value = Foo::Bar|\n", 1],
"next argument" => [method + constants + "consume(Foo::Bar, |1)\n", nil],
}

cases.each do |name, (marked_source, expected_line)|
before_cursor, after_cursor = marked_source.split("|", 2)
source = before_cursor + after_cursor
position = {
line: before_cursor.count("\n"),
character: before_cursor.split("\n", -1).last.encode(Encoding::UTF_16LE).bytesize / 2,
}

with_server(source, stub_no_typechecker: true) do |server, uri|
server.process_message(
id: 1,
method: "textDocument/definition",
params: { textDocument: { uri: uri }, position: position },
)
response = server.pop_response.response

if expected_line
refute_empty(response, name)
assert_equal(uri.to_s, response.first.attributes[:targetUri], name)
target_range = response.first.attributes[:targetSelectionRange].attributes
assert_equal(expected_line, target_range[:start].to_hash[:line], name)
else
assert_empty(response, name)
end
end
end
end

def test_multibyte_character_precision
source = <<~RUBY
module Fほげ
Expand Down
Loading