Skip to content

Commit f35ec9a

Browse files
committed
Partially migrate hover to use Rubydex
1 parent 0333da1 commit f35ec9a

File tree

4 files changed

+91
-65
lines changed

4 files changed

+91
-65
lines changed

lib/ruby_lsp/listeners/hover.rb

Lines changed: 35 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so
4848
@response_builder = response_builder
4949
@global_state = global_state
5050
@index = global_state.index #: RubyIndexer::Index
51+
@graph = global_state.graph #: Rubydex::Graph
5152
@type_inferrer = global_state.type_inferrer #: TypeInferrer
5253
@path = uri.to_standardized_path #: String?
5354
@node_context = node_context
@@ -178,32 +179,32 @@ def on_global_variable_write_node_enter(node)
178179

179180
#: (Prism::InstanceVariableReadNode node) -> void
180181
def on_instance_variable_read_node_enter(node)
181-
handle_instance_variable_hover(node.name.to_s)
182+
handle_variable_hover(node.name.to_s)
182183
end
183184

184185
#: (Prism::InstanceVariableWriteNode node) -> void
185186
def on_instance_variable_write_node_enter(node)
186-
handle_instance_variable_hover(node.name.to_s)
187+
handle_variable_hover(node.name.to_s)
187188
end
188189

189190
#: (Prism::InstanceVariableAndWriteNode node) -> void
190191
def on_instance_variable_and_write_node_enter(node)
191-
handle_instance_variable_hover(node.name.to_s)
192+
handle_variable_hover(node.name.to_s)
192193
end
193194

194195
#: (Prism::InstanceVariableOperatorWriteNode node) -> void
195196
def on_instance_variable_operator_write_node_enter(node)
196-
handle_instance_variable_hover(node.name.to_s)
197+
handle_variable_hover(node.name.to_s)
197198
end
198199

199200
#: (Prism::InstanceVariableOrWriteNode node) -> void
200201
def on_instance_variable_or_write_node_enter(node)
201-
handle_instance_variable_hover(node.name.to_s)
202+
handle_variable_hover(node.name.to_s)
202203
end
203204

204205
#: (Prism::InstanceVariableTargetNode node) -> void
205206
def on_instance_variable_target_node_enter(node)
206-
handle_instance_variable_hover(node.name.to_s)
207+
handle_variable_hover(node.name.to_s)
207208
end
208209

209210
#: (Prism::SuperNode node) -> void
@@ -223,32 +224,32 @@ def on_yield_node_enter(node)
223224

224225
#: (Prism::ClassVariableAndWriteNode node) -> void
225226
def on_class_variable_and_write_node_enter(node)
226-
handle_class_variable_hover(node.name.to_s)
227+
handle_variable_hover(node.name.to_s)
227228
end
228229

229230
#: (Prism::ClassVariableOperatorWriteNode node) -> void
230231
def on_class_variable_operator_write_node_enter(node)
231-
handle_class_variable_hover(node.name.to_s)
232+
handle_variable_hover(node.name.to_s)
232233
end
233234

234235
#: (Prism::ClassVariableOrWriteNode node) -> void
235236
def on_class_variable_or_write_node_enter(node)
236-
handle_class_variable_hover(node.name.to_s)
237+
handle_variable_hover(node.name.to_s)
237238
end
238239

239240
#: (Prism::ClassVariableTargetNode node) -> void
240241
def on_class_variable_target_node_enter(node)
241-
handle_class_variable_hover(node.name.to_s)
242+
handle_variable_hover(node.name.to_s)
242243
end
243244

244245
#: (Prism::ClassVariableReadNode node) -> void
245246
def on_class_variable_read_node_enter(node)
246-
handle_class_variable_hover(node.name.to_s)
247+
handle_variable_hover(node.name.to_s)
247248
end
248249

249250
#: (Prism::ClassVariableWriteNode node) -> void
250251
def on_class_variable_write_node_enter(node)
251-
handle_class_variable_hover(node.name.to_s)
252+
handle_variable_hover(node.name.to_s)
252253
end
253254

254255
private
@@ -324,62 +325,46 @@ def handle_method_hover(message, inherited_only: false)
324325
end
325326
end
326327

327-
#: (String name) -> void
328-
def handle_instance_variable_hover(name)
329-
# Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able
330-
# to provide all features for them
331-
return if @sorbet_level.strict?
332-
333-
type = @type_inferrer.infer_receiver_type(@node_context)
334-
return unless type
335-
336-
entries = @index.resolve_instance_variable(name, type.name)
337-
return unless entries
338-
339-
categorized_markdown_from_index_entries(name, entries).each do |category, content|
340-
@response_builder.push(content, category: category)
341-
end
342-
rescue RubyIndexer::Index::NonExistingNamespaceError
343-
# If by any chance we haven't indexed the owner, then there's no way to find the right declaration
344-
end
345-
346328
#: (String name) -> void
347329
def handle_global_variable_hover(name)
348-
entries = @index[name]
349-
return unless entries
330+
declaration = @graph[name]
331+
return unless declaration
350332

351-
categorized_markdown_from_index_entries(name, entries).each do |category, content|
333+
categorized_markdown_from_definitions(name, declaration.definitions).each do |category, content|
352334
@response_builder.push(content, category: category)
353335
end
354336
end
355337

338+
# Handle class or instance variables. We collect all definitions across the ancestors of the type
339+
#
356340
#: (String name) -> void
357-
def handle_class_variable_hover(name)
341+
def handle_variable_hover(name)
342+
# Sorbet enforces that all variables be declared on typed strict or higher, which means it will be able to
343+
# provide all features for them
344+
return if @sorbet_level.strict?
345+
358346
type = @type_inferrer.infer_receiver_type(@node_context)
359347
return unless type
360348

361-
entries = @index.resolve_class_variable(name, type.name)
362-
return unless entries
349+
owner = @graph[type.name]
350+
return unless owner.is_a?(Rubydex::Namespace)
363351

364-
categorized_markdown_from_index_entries(name, entries).each do |category, content|
365-
@response_builder.push(content, category: category)
352+
owner.ancestors.each do |ancestor|
353+
member = ancestor.member(name)
354+
next unless member
355+
356+
categorized_markdown_from_definitions(member.name, member.definitions).each do |category, content|
357+
@response_builder.push(content, category: category)
358+
end
366359
end
367-
rescue RubyIndexer::Index::NonExistingNamespaceError
368-
# If by any chance we haven't indexed the owner, then there's no way to find the right declaration
369360
end
370361

371362
#: (String name, Prism::Location location) -> void
372363
def generate_hover(name, location)
373-
entries = @index.resolve(name, @node_context.nesting)
374-
return unless entries
375-
376-
# We should only show hover for private constants if the constant is defined in the same namespace as the
377-
# reference
378-
first_entry = entries.first #: as !nil
379-
full_name = first_entry.name
380-
return if first_entry.private? && full_name != "#{@node_context.fully_qualified_name}::#{name}"
364+
declaration = @graph.resolve_constant(name, @node_context.nesting)
365+
return unless declaration
381366

382-
categorized_markdown_from_index_entries(full_name, entries).each do |category, content|
367+
categorized_markdown_from_definitions(declaration.name, declaration.definitions).each do |category, content|
383368
@response_builder.push(content, category: category)
384369
end
385370
end

lib/ruby_lsp/requests/support/common.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,44 @@ def self_receiver?(node)
6464
receiver.nil? || receiver.is_a?(Prism::SelfNode)
6565
end
6666

67+
#: (String, Enumerable[Rubydex::Definition], ?Integer?) -> Hash[Symbol, String]
68+
def categorized_markdown_from_definitions(title, definitions, max_entries = nil)
69+
markdown_title = "```ruby\n#{title}\n```"
70+
file_links = []
71+
content = +""
72+
defs = max_entries ? definitions.take(max_entries) : definitions
73+
defs.each do |definition|
74+
# For Markdown links, we need 1 based display locations
75+
loc = definition.location.to_display
76+
uri = URI(loc.uri)
77+
file_name = if uri.scheme == "untitled"
78+
uri.opaque #: as !nil
79+
else
80+
File.basename(
81+
uri.full_path, #: as !nil
82+
)
83+
end
84+
85+
# The format for VS Code file URIs is `file:///path/to/file.rb#Lstart_line,start_column-end_line,end_column`
86+
string_uri = "#{loc.uri}#L#{loc.start_line},#{loc.start_column}-#{loc.end_line},#{loc.end_column}"
87+
file_links << "[#{file_name}](#{string_uri})"
88+
content << "\n\n#{definition.comments.map { |comment| comment.string.delete_prefix("# ") }.join("\n")}" unless definition.comments.empty?
89+
end
90+
91+
additional_entries_text = if max_entries && defs.length > max_entries
92+
additional = defs.length - max_entries
93+
" | #{additional} other#{additional > 1 ? "s" : ""}"
94+
else
95+
""
96+
end
97+
98+
{
99+
title: markdown_title,
100+
links: "**Definitions**: #{file_links.join(" | ")}#{additional_entries_text}",
101+
documentation: content,
102+
}
103+
end
104+
67105
#: (String title, (Array[RubyIndexer::Entry] | RubyIndexer::Entry) entries, ?Integer? max_entries) -> Hash[Symbol, String]
68106
def categorized_markdown_from_index_entries(title, entries, max_entries = nil)
69107
markdown_title = "```ruby\n#{title}\n```"

test/expectations/hover/documented_constant.exp.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"result": {
99
"contents": {
1010
"kind": "markdown",
11-
"value": "```ruby\nBAZ\n```\n\n**Definitions**: [fake.rb](file:///fake.rb#L2,1-2,10)\n\n\n\nThis is the documentation for Baz"
11+
"value": "```ruby\nBAZ\n```\n\n**Definitions**: [fake.rb](file:///fake.rb#L2,1-2,4)\n\n\n\nThis is the documentation for Baz"
1212
}
1313
}
1414
}

test/requests/hover_expectations_test.rb

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,25 @@ class A
5151

5252
def test_hovering_on_erb
5353
source = <<~ERB
54-
<% String %>
54+
<% Person %>
5555
ERB
5656

5757
with_server(source, Kernel.URI("file:///fake.erb"), stub_no_typechecker: true) do |server, uri|
58-
RubyIndexer::RBSIndexer.new(server.global_state.index).index_ruby_core
58+
graph = server.global_state.graph
59+
graph.index_source(URI::Generic.from_path(path: "/person.rb").to_s, <<~RUBY, "ruby")
60+
# Hello from person.rb
61+
class Person
62+
end
63+
RUBY
64+
graph.resolve
65+
5966
server.process_message(
6067
id: 1,
6168
method: "textDocument/hover",
6269
params: { textDocument: { uri: uri }, position: { line: 0, character: 4 } },
6370
)
6471
response = server.pop_response
65-
assert_match(/String\b/, response.response.contents.value)
72+
assert_match(/Hello from person\.rb/, response.response.contents.value)
6673
end
6774
end
6875

@@ -76,25 +83,21 @@ def test_hovering_for_global_variables
7683
$qux ||= 1
7784
# target write node
7885
$quux, $corge = 1
79-
# write node
86+
# foo docs
8087
$foo = 1
81-
# read node
82-
$DEBUG
88+
$foo
8389
RUBY
8490

8591
expectations = [
8692
{ line: 1, documentation: "and write node" },
8793
{ line: 3, documentation: "operator write node" },
8894
{ line: 5, documentation: "or write node" },
8995
{ line: 7, documentation: "target write node" },
90-
{ line: 9, documentation: "write node" },
91-
{ line: 11, documentation: "The debug flag" },
96+
{ line: 9, documentation: "foo docs" },
97+
{ line: 10, documentation: "foo docs" },
9298
]
9399

94100
with_server(source) do |server, uri|
95-
index = server.instance_variable_get(:@global_state).index
96-
RubyIndexer::RBSIndexer.new(index).index_ruby_core
97-
98101
expectations.each do |expectation|
99102
server.process_message(
100103
id: 1,
@@ -264,18 +267,18 @@ class A
264267
private_constant(:CONST)
265268
end
266269
267-
A::CONST # invalid private reference
270+
A::CONST
268271
RUBY
269272

270-
# We need to pretend that Sorbet is not a dependency or else we can't properly test
271273
with_server(source, stub_no_typechecker: true) do |server, uri|
272274
server.process_message(
273275
id: 1,
274276
method: "textDocument/hover",
275277
params: { textDocument: { uri: uri }, position: { character: 3, line: 5 } },
276278
)
277279

278-
assert_nil(server.pop_response.response)
280+
# TODO: once we have visibility exposed from Rubydex, let's show that the constant is private
281+
assert_match("A::CONST", server.pop_response.response.contents.value)
279282
end
280283
end
281284

0 commit comments

Comments
 (0)