From ee0d25b8c8b0c3f7b805e0e790cd07265a772210 Mon Sep 17 00:00:00 2001 From: Vinicius Stock Date: Fri, 8 May 2026 10:14:35 -0400 Subject: [PATCH] Use Rubydex for method hover --- lib/ruby_lsp/listeners/hover.rb | 16 +++++----- lib/ruby_lsp/listeners/signature_help.rb | 9 +----- lib/ruby_lsp/rubydex/declaration.rb | 38 ++++++++++++++++++++++++ test/requests/hover_expectations_test.rb | 21 +++++++++---- 4 files changed, 63 insertions(+), 21 deletions(-) diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index c65bb673b..a757de5d2 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -15,7 +15,6 @@ class Hover def initialize(response_builder, global_state, uri, node_context, dispatcher, sorbet_level, position) # rubocop:disable Metrics/ParameterLists @response_builder = response_builder @global_state = global_state - @index = global_state.index #: RubyIndexer::Index @graph = global_state.graph #: Rubydex::Graph @type_inferrer = global_state.type_inferrer #: TypeInferrer @path = uri.to_standardized_path #: String? @@ -457,21 +456,22 @@ def handle_method_hover(message, inherited_only: false) type = @type_inferrer.infer_receiver_type(@node_context) return unless type - methods = @index.resolve_method(message, type.name, inherited_only: inherited_only) - return unless methods + owner = @graph[type.name] + return unless owner.is_a?(Rubydex::Namespace) - first_method = methods.first #: as !nil - return unless method_reachable_from_call_site?(first_method, type, @graph, @node_context) + method = owner.find_member("#{message}()", only_inherited: inherited_only) + return unless method.is_a?(Rubydex::Method) + return unless method_reachable_from_call_site?(method, type, @graph, @node_context) - title = "#{message}#{first_method.decorated_parameters}" - title << first_method.formatted_signatures + title = +"#{message}#{method.decorated_parameters}" + title << method.formatted_signatures if type.is_a?(TypeInferrer::GuessedType) title << "\n\nGuessed receiver: #{type.name}" @response_builder.push("[Learn more about guessed types](#{GUESSED_TYPES_URL})\n", category: :links) end - categorized_markdown_from_index_entries(title, methods).each do |category, content| + categorized_markdown_from_definitions(title, method.definitions).each do |category, content| @response_builder.push(content, category: category) end end diff --git a/lib/ruby_lsp/listeners/signature_help.rb b/lib/ruby_lsp/listeners/signature_help.rb index f2a83876e..186bfd0cb 100644 --- a/lib/ruby_lsp/listeners/signature_help.rb +++ b/lib/ruby_lsp/listeners/signature_help.rb @@ -33,14 +33,7 @@ def on_call_node_enter(node) target_method = owner.find_member("#{message}()") return unless target_method.is_a?(Rubydex::Method) - signatures = target_method.definitions.flat_map do |defn| - case defn - when Rubydex::MethodDefinition, Rubydex::MethodAliasDefinition - defn.signatures - else - [] - end - end + signatures = target_method.signatures # If the method doesn't have any signatures, there's nothing to show return if signatures.empty? diff --git a/lib/ruby_lsp/rubydex/declaration.rb b/lib/ruby_lsp/rubydex/declaration.rb index a8aad7d82..da89f1a1f 100644 --- a/lib/ruby_lsp/rubydex/declaration.rb +++ b/lib/ruby_lsp/rubydex/declaration.rb @@ -108,6 +108,44 @@ class Method def to_lsp_completion_kind RubyLsp::Constant::CompletionItemKind::METHOD end + + # All signatures collected across every definition (re-opens, RBS overloads, alias targets) of this method. + #: () -> Array[Rubydex::Signature] + def signatures + definitions.flat_map do |defn| + case defn + when Rubydex::MethodDefinition, Rubydex::MethodAliasDefinition + defn.signatures + else + [] + end + end + end + + # Decorated parameter list of the first signature, e.g. `(a, b = , &block)`. Returns `()` when there are + # no signatures (e.g. an unresolved alias). + #: () -> String + def decorated_parameters + first = signatures.first + return "()" unless first + + "(#{first.format})" + end + + # Suffix line that hints at additional overloads beyond the first signature, matching the legacy index entry + # rendering used in hover. + #: () -> String + def formatted_signatures + count = signatures.size + case count + when 0, 1 + "" + when 2 + "\n(+1 overload)" + else + "\n(+#{count - 1} overloads)" + end + end end class InstanceVariable diff --git a/test/requests/hover_expectations_test.rb b/test/requests/hover_expectations_test.rb index ab1b773d7..e92f3c8f9 100644 --- a/test/requests/hover_expectations_test.rb +++ b/test/requests/hover_expectations_test.rb @@ -806,19 +806,28 @@ def baz end def test_hover_for_methods_shows_overload_count - skip("[RUBYDEX] Temporarily skipped because we don't yet index RBS methods") + rbs = <<~RBS + class Foo + def try_convert: (Object object) -> String? + | (String s) -> String + | (Symbol s) -> String + end + RBS + rbs_uri = URI::Generic.from_path(path: "/fake/path/foo.rbs").to_s source = <<~RUBY - String.try_convert + Foo.new.try_convert RUBY with_server(source) do |server, uri| - index = server.instance_variable_get(:@global_state).index - RubyIndexer::RBSIndexer.new(index).index_ruby_core + graph = server.global_state.graph + graph.index_source(rbs_uri, rbs, "rbs") + graph.resolve + server.process_message( id: 1, method: "textDocument/hover", - params: { textDocument: { uri: uri }, position: { character: 8, line: 0 } }, + params: { textDocument: { uri: uri }, position: { character: 12, line: 0 } }, ) contents = server.pop_response.response.contents.value @@ -905,6 +914,8 @@ def baz end def test_hover_for_aliased_methods + skip("[RUBYDEX] need to expose method alias targets in the Ruby API") + source = <<~RUBY class Parent # Original