diff --git a/lib/ruby_lsp/listeners/definition.rb b/lib/ruby_lsp/listeners/definition.rb index 38478712cf..b52f1f6d64 100644 --- a/lib/ruby_lsp/listeners/definition.rb +++ b/lib/ruby_lsp/listeners/definition.rb @@ -22,6 +22,7 @@ def initialize(response_builder, global_state, language_id, uri, node_context, d dispatcher.register( self, :on_call_node_enter, + :on_def_node_enter, :on_block_argument_node_enter, :on_constant_read_node_enter, :on_constant_path_node_enter, @@ -69,6 +70,20 @@ def on_call_node_enter(node) handle_method_definition(message, inferrer_receiver_type) end + # On the name of a method declaration, the declaration itself is the definition. Returning it lets editors fall + # back to their alternative action, such as showing references in VS Code + #: (Prism::DefNode node) -> void + def on_def_node_enter(node) + # Sorbet can handle go to definition for methods declared on typed true or higher + return if @sorbet_level.true_or_higher? + + @response_builder << Interface::LocationLink.new( + target_uri: @uri.to_s, + target_range: range_from_location(node.location), + target_selection_range: range_from_location(node.name_loc), + ) + end + #: (Prism::StringNode node) -> void def on_string_node_enter(node) with_enclosing_call(node) do |enclosing_call, name| diff --git a/lib/ruby_lsp/requests/definition.rb b/lib/ruby_lsp/requests/definition.rb index 53ad3c29ba..2ebdab025f 100644 --- a/lib/ruby_lsp/requests/definition.rb +++ b/lib/ruby_lsp/requests/definition.rb @@ -24,6 +24,7 @@ def initialize(document, global_state, position, dispatcher, sorbet_level) char_position, node_types: [ Prism::CallNode, + Prism::DefNode, Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::BlockArgumentNode, @@ -109,7 +110,8 @@ def position_outside_target?(position, target) Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode, - Prism::InstanceVariableWriteNode + Prism::InstanceVariableWriteNode, + Prism::DefNode !covers_position?(target.name_loc, position) when Prism::CallNode diff --git a/test/requests/definition_expectations_test.rb b/test/requests/definition_expectations_test.rb index 5845300aac..8935031c26 100644 --- a/test/requests/definition_expectations_test.rb +++ b/test/requests/definition_expectations_test.rb @@ -1333,6 +1333,99 @@ def baz end end + def test_definition_on_method_name_returns_the_method_itself + source = <<~RUBY + class Foo + def bar(baz) + baz + end + end + RUBY + + with_server(source) do |server, uri| + server.process_message( + id: 1, + method: "textDocument/definition", + params: { textDocument: { uri: uri }, position: { character: 7, line: 1 } }, + ) + + response = server.pop_response.response + assert_equal(1, response.length) + + link = response.first + assert_equal(uri.to_s, link.target_uri) + + selection_range = link.target_selection_range + assert_equal([[1, 6], [1, 9]], [selection_range.start, selection_range.end].map { |p| [p.line, p.character] }) + assert_equal([1, 3], [link.target_range.start.line, link.target_range.end.line]) + end + end + + def test_definition_on_singleton_method_name_returns_the_method_itself + source = <<~RUBY + class Foo + def self.bar; end + end + RUBY + + with_server(source) do |server, uri| + server.process_message( + id: 1, + method: "textDocument/definition", + params: { textDocument: { uri: uri }, position: { character: 12, line: 1 } }, + ) + + response = server.pop_response.response + assert_equal(1, response.length) + + selection_range = response.first.target_selection_range + assert_equal([[1, 11], [1, 14]], [selection_range.start, selection_range.end].map { |p| [p.line, p.character] }) + end + end + + def test_definition_inside_method_declaration_outside_its_name_returns_nothing + source = <<~RUBY + class Foo + def bar(baz) + baz + end + end + RUBY + + with_server(source) do |server, uri| + # The `def` keyword, the parameter and the local variable in the body + [{ character: 3, line: 1 }, { character: 11, line: 1 }, { character: 5, line: 2 }].each do |position| + server.process_message( + id: 1, + method: "textDocument/definition", + params: { textDocument: { uri: uri }, position: position }, + ) + + assert_empty(server.pop_response.response) + end + end + end + + def test_definition_on_method_name_is_disabled_for_typed_true + source = <<~RUBY + # typed: true + class Foo + def bar + end + end + RUBY + + with_server(source) do |server, uri| + server.process_message( + id: 1, + method: "textDocument/definition", + params: { textDocument: { uri: uri }, position: { character: 7, line: 2 } }, + ) + + assert_empty(server.pop_response.response) + end + end + def test_definition_for_instance_variables_is_disabled_on_typed_strict source = <<~RUBY # typed: strict