From c8e2f4bc9c282161ef4b8f84e4f7b91f7a14a4c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matic=20Perov=C5=A1ek?= Date: Thu, 22 Oct 2026 17:27:56 +0200 Subject: [PATCH] Support go to definition on method declaration names Go to definition on the name of a method declaration returned nothing. Class and module names already resolve to their own declaration, which lets editors switch to their alternative action, such as VS Code showing references. Methods now behave the same way, except on typed true or higher, where Sorbet handles method definitions. --- lib/ruby_lsp/listeners/definition.rb | 15 +++ lib/ruby_lsp/requests/definition.rb | 4 +- test/requests/definition_expectations_test.rb | 93 +++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) 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