Skip to content

Commit 9af3aa6

Browse files
committed
Complete keyword hover with Rubydex
1 parent d1435ac commit 9af3aa6

12 files changed

Lines changed: 745 additions & 364 deletions

File tree

lib/ruby_lsp/internal.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
require "ruby_lsp/base_server"
4040
require "ruby_indexer/ruby_indexer"
4141
require "ruby_lsp/utils"
42-
require "ruby_lsp/static_docs"
4342
require "ruby_lsp/scope"
4443
require "ruby_lsp/client_capabilities"
4544
require "ruby_lsp/global_state"

lib/ruby_lsp/listeners/hover.rb

Lines changed: 223 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,13 @@ module Listeners
66
class Hover
77
include Requests::Support::Common
88

9-
ALLOWED_TARGETS = [
10-
Prism::BreakNode,
11-
Prism::CallNode,
12-
Prism::ConstantReadNode,
13-
Prism::ConstantWriteNode,
14-
Prism::ConstantPathNode,
15-
Prism::GlobalVariableAndWriteNode,
16-
Prism::GlobalVariableOperatorWriteNode,
17-
Prism::GlobalVariableOrWriteNode,
18-
Prism::GlobalVariableReadNode,
19-
Prism::GlobalVariableTargetNode,
20-
Prism::GlobalVariableWriteNode,
21-
Prism::InstanceVariableReadNode,
22-
Prism::InstanceVariableAndWriteNode,
23-
Prism::InstanceVariableOperatorWriteNode,
24-
Prism::InstanceVariableOrWriteNode,
25-
Prism::InstanceVariableTargetNode,
26-
Prism::InstanceVariableWriteNode,
27-
Prism::SymbolNode,
28-
Prism::StringNode,
29-
Prism::InterpolatedStringNode,
30-
Prism::SuperNode,
31-
Prism::ForwardingSuperNode,
32-
Prism::YieldNode,
33-
Prism::ClassVariableAndWriteNode,
34-
Prism::ClassVariableOperatorWriteNode,
35-
Prism::ClassVariableOrWriteNode,
36-
Prism::ClassVariableReadNode,
37-
Prism::ClassVariableTargetNode,
38-
Prism::ClassVariableWriteNode,
39-
] #: Array[singleton(Prism::Node)]
40-
419
ALLOWED_REMOTE_PROVIDERS = [
4210
"https://github.com",
4311
"https://gitlab.com",
4412
].freeze #: Array[String]
4513

46-
#: (ResponseBuilders::Hover response_builder, GlobalState global_state, URI::Generic uri, NodeContext node_context, Prism::Dispatcher dispatcher, SorbetLevel sorbet_level) -> void
47-
def initialize(response_builder, global_state, uri, node_context, dispatcher, sorbet_level) # rubocop:disable Metrics/ParameterLists
14+
#: (ResponseBuilders::Hover response_builder, GlobalState global_state, URI::Generic uri, NodeContext node_context, Prism::Dispatcher dispatcher, SorbetLevel sorbet_level, Hash[Symbol, untyped] position) -> void
15+
def initialize(response_builder, global_state, uri, node_context, dispatcher, sorbet_level, position) # rubocop:disable Metrics/ParameterLists
4816
@response_builder = response_builder
4917
@global_state = global_state
5018
@index = global_state.index #: RubyIndexer::Index
@@ -53,45 +21,80 @@ def initialize(response_builder, global_state, uri, node_context, dispatcher, so
5321
@path = uri.to_standardized_path #: String?
5422
@node_context = node_context
5523
@sorbet_level = sorbet_level
24+
@position = position
5625

5726
dispatcher.register(
5827
self,
28+
:on_alias_global_variable_node_enter,
29+
:on_alias_method_node_enter,
30+
:on_and_node_enter,
31+
:on_begin_node_enter,
32+
:on_block_node_enter,
5933
:on_break_node_enter,
34+
:on_call_node_enter,
35+
:on_case_match_node_enter,
36+
:on_case_node_enter,
37+
:on_class_node_enter,
38+
:on_singleton_class_node_enter,
39+
:on_lambda_node_enter,
40+
:on_class_variable_and_write_node_enter,
41+
:on_class_variable_operator_write_node_enter,
42+
:on_class_variable_or_write_node_enter,
43+
:on_class_variable_read_node_enter,
44+
:on_class_variable_target_node_enter,
45+
:on_class_variable_write_node_enter,
46+
:on_constant_path_node_enter,
6047
:on_constant_read_node_enter,
6148
:on_constant_write_node_enter,
62-
:on_constant_path_node_enter,
63-
:on_call_node_enter,
49+
:on_def_node_enter,
50+
:on_defined_node_enter,
51+
:on_else_node_enter,
52+
:on_ensure_node_enter,
53+
:on_false_node_enter,
54+
:on_for_node_enter,
55+
:on_forwarding_super_node_enter,
6456
:on_global_variable_and_write_node_enter,
6557
:on_global_variable_operator_write_node_enter,
6658
:on_global_variable_or_write_node_enter,
6759
:on_global_variable_read_node_enter,
6860
:on_global_variable_target_node_enter,
6961
:on_global_variable_write_node_enter,
70-
:on_instance_variable_read_node_enter,
71-
:on_instance_variable_write_node_enter,
62+
:on_if_node_enter,
63+
:on_in_node_enter,
7264
:on_instance_variable_and_write_node_enter,
7365
:on_instance_variable_operator_write_node_enter,
7466
:on_instance_variable_or_write_node_enter,
67+
:on_instance_variable_read_node_enter,
7568
:on_instance_variable_target_node_enter,
76-
:on_super_node_enter,
77-
:on_forwarding_super_node_enter,
78-
:on_string_node_enter,
69+
:on_instance_variable_write_node_enter,
7970
:on_interpolated_string_node_enter,
71+
:on_module_node_enter,
72+
:on_next_node_enter,
73+
:on_nil_node_enter,
74+
:on_or_node_enter,
75+
:on_post_execution_node_enter,
76+
:on_pre_execution_node_enter,
77+
:on_redo_node_enter,
78+
:on_rescue_modifier_node_enter,
79+
:on_rescue_node_enter,
80+
:on_retry_node_enter,
81+
:on_return_node_enter,
82+
:on_self_node_enter,
83+
:on_source_encoding_node_enter,
84+
:on_source_file_node_enter,
85+
:on_source_line_node_enter,
86+
:on_string_node_enter,
87+
:on_super_node_enter,
88+
:on_true_node_enter,
89+
:on_undef_node_enter,
90+
:on_unless_node_enter,
91+
:on_until_node_enter,
92+
:on_when_node_enter,
93+
:on_while_node_enter,
8094
:on_yield_node_enter,
81-
:on_class_variable_and_write_node_enter,
82-
:on_class_variable_operator_write_node_enter,
83-
:on_class_variable_or_write_node_enter,
84-
:on_class_variable_read_node_enter,
85-
:on_class_variable_target_node_enter,
86-
:on_class_variable_write_node_enter,
8795
)
8896
end
8997

90-
#: (Prism::BreakNode node) -> void
91-
def on_break_node_enter(node)
92-
handle_keyword_documentation(node.keyword)
93-
end
94-
9598
#: (Prism::StringNode node) -> void
9699
def on_string_node_enter(node)
97100
if @path && File.basename(@path) == GEMFILE_NAME
@@ -144,6 +147,12 @@ def on_call_node_enter(node)
144147
message = node.message
145148
return unless message
146149

150+
# `not x` is parsed as a call to `!` whose message_loc slices to "not"
151+
if node.name == :! && message == "not"
152+
handle_keyword_documentation("not")
153+
return
154+
end
155+
147156
handle_method_hover(message)
148157
end
149158

@@ -209,19 +218,150 @@ def on_instance_variable_target_node_enter(node)
209218

210219
#: (Prism::SuperNode node) -> void
211220
def on_super_node_enter(node)
212-
handle_super_node_hover
221+
handle_super_node_hover(node.keyword_loc)
213222
end
214223

215224
#: (Prism::ForwardingSuperNode node) -> void
216225
def on_forwarding_super_node_enter(node)
217-
handle_super_node_hover
226+
handle_super_node_hover(node.location)
227+
end
228+
229+
#: (Prism::AliasGlobalVariableNode) -> void
230+
def on_alias_global_variable_node_enter(node) = handle_keyword_at_location(node.keyword_loc)
231+
232+
#: (Prism::AliasMethodNode) -> void
233+
def on_alias_method_node_enter(node) = handle_keyword_at_location(node.keyword_loc)
234+
235+
#: (Prism::AndNode) -> void
236+
def on_and_node_enter(node) = handle_keyword_at_location(node.operator_loc)
237+
238+
#: (Prism::BeginNode) -> void
239+
def on_begin_node_enter(node) = handle_keyword_at_location(node.begin_keyword_loc, node.end_keyword_loc)
240+
241+
#: (Prism::BlockNode) -> void
242+
def on_block_node_enter(node) = handle_keyword_at_location(node.opening_loc, node.closing_loc)
243+
244+
#: (Prism::BreakNode) -> void
245+
def on_break_node_enter(node) = handle_keyword_at_location(node.keyword_loc)
246+
247+
#: (Prism::CaseMatchNode) -> void
248+
def on_case_match_node_enter(node) = handle_keyword_at_location(node.case_keyword_loc, node.end_keyword_loc)
249+
250+
#: (Prism::CaseNode) -> void
251+
def on_case_node_enter(node) = handle_keyword_at_location(node.case_keyword_loc, node.end_keyword_loc)
252+
253+
#: (Prism::ClassNode) -> void
254+
def on_class_node_enter(node) = handle_keyword_at_location(node.class_keyword_loc, node.end_keyword_loc)
255+
256+
#: (Prism::SingletonClassNode) -> void
257+
def on_singleton_class_node_enter(node)
258+
handle_keyword_at_location(node.class_keyword_loc, node.end_keyword_loc)
259+
end
260+
261+
#: (Prism::LambdaNode) -> void
262+
def on_lambda_node_enter(node) = handle_keyword_at_location(node.opening_loc, node.closing_loc)
263+
264+
#: (Prism::DefNode) -> void
265+
def on_def_node_enter(node) = handle_keyword_at_location(node.def_keyword_loc, node.end_keyword_loc)
266+
267+
#: (Prism::DefinedNode) -> void
268+
def on_defined_node_enter(node) = handle_keyword_at_location(node.keyword_loc)
269+
270+
#: (Prism::ElseNode) -> void
271+
def on_else_node_enter(node) = handle_keyword_at_location(node.else_keyword_loc, node.end_keyword_loc)
272+
273+
#: (Prism::EnsureNode) -> void
274+
def on_ensure_node_enter(node) = handle_keyword_at_location(node.ensure_keyword_loc, node.end_keyword_loc)
275+
276+
#: (Prism::FalseNode) -> void
277+
def on_false_node_enter(node) = handle_keyword_at_location(node.location)
278+
279+
#: (Prism::ForNode) -> void
280+
def on_for_node_enter(node)
281+
handle_keyword_at_location(
282+
node.for_keyword_loc,
283+
node.in_keyword_loc,
284+
node.do_keyword_loc,
285+
node.end_keyword_loc,
286+
)
287+
end
288+
289+
#: (Prism::IfNode) -> void
290+
def on_if_node_enter(node)
291+
handle_keyword_at_location(node.if_keyword_loc, node.then_keyword_loc, node.end_keyword_loc)
218292
end
219293

220-
#: (Prism::YieldNode node) -> void
221-
def on_yield_node_enter(node)
222-
handle_keyword_documentation(node.keyword)
294+
#: (Prism::InNode) -> void
295+
def on_in_node_enter(node) = handle_keyword_at_location(node.in_loc, node.then_loc)
296+
297+
#: (Prism::ModuleNode) -> void
298+
def on_module_node_enter(node) = handle_keyword_at_location(node.module_keyword_loc, node.end_keyword_loc)
299+
300+
#: (Prism::NextNode) -> void
301+
def on_next_node_enter(node) = handle_keyword_at_location(node.keyword_loc)
302+
303+
#: (Prism::NilNode) -> void
304+
def on_nil_node_enter(node) = handle_keyword_at_location(node.location)
305+
306+
#: (Prism::OrNode) -> void
307+
def on_or_node_enter(node) = handle_keyword_at_location(node.operator_loc)
308+
309+
#: (Prism::PostExecutionNode) -> void
310+
def on_post_execution_node_enter(node) = handle_keyword_at_location(node.keyword_loc)
311+
312+
#: (Prism::PreExecutionNode) -> void
313+
def on_pre_execution_node_enter(node) = handle_keyword_at_location(node.keyword_loc)
314+
315+
#: (Prism::RedoNode) -> void
316+
def on_redo_node_enter(node) = handle_keyword_at_location(node.location)
317+
318+
#: (Prism::RescueModifierNode) -> void
319+
def on_rescue_modifier_node_enter(node) = handle_keyword_at_location(node.keyword_loc)
320+
321+
#: (Prism::RescueNode) -> void
322+
def on_rescue_node_enter(node) = handle_keyword_at_location(node.keyword_loc, node.then_keyword_loc)
323+
324+
#: (Prism::RetryNode) -> void
325+
def on_retry_node_enter(node) = handle_keyword_at_location(node.location)
326+
327+
#: (Prism::ReturnNode) -> void
328+
def on_return_node_enter(node) = handle_keyword_at_location(node.keyword_loc)
329+
330+
#: (Prism::SelfNode) -> void
331+
def on_self_node_enter(node) = handle_keyword_at_location(node.location)
332+
333+
#: (Prism::SourceEncodingNode) -> void
334+
def on_source_encoding_node_enter(node) = handle_keyword_at_location(node.location)
335+
336+
#: (Prism::SourceFileNode) -> void
337+
def on_source_file_node_enter(node) = handle_keyword_at_location(node.location)
338+
339+
#: (Prism::SourceLineNode) -> void
340+
def on_source_line_node_enter(node) = handle_keyword_at_location(node.location)
341+
342+
#: (Prism::TrueNode) -> void
343+
def on_true_node_enter(node) = handle_keyword_at_location(node.location)
344+
345+
#: (Prism::UndefNode) -> void
346+
def on_undef_node_enter(node) = handle_keyword_at_location(node.keyword_loc)
347+
348+
#: (Prism::UnlessNode) -> void
349+
def on_unless_node_enter(node)
350+
handle_keyword_at_location(node.keyword_loc, node.then_keyword_loc, node.end_keyword_loc)
223351
end
224352

353+
#: (Prism::UntilNode) -> void
354+
def on_until_node_enter(node) = handle_keyword_at_location(node.keyword_loc, node.do_keyword_loc, node.closing_loc)
355+
356+
#: (Prism::WhenNode) -> void
357+
def on_when_node_enter(node) = handle_keyword_at_location(node.keyword_loc, node.then_keyword_loc)
358+
359+
#: (Prism::WhileNode) -> void
360+
def on_while_node_enter(node) = handle_keyword_at_location(node.keyword_loc, node.do_keyword_loc, node.closing_loc)
361+
362+
#: (Prism::YieldNode) -> void
363+
def on_yield_node_enter(node) = handle_keyword_at_location(node.keyword_loc)
364+
225365
#: (Prism::ClassVariableAndWriteNode node) -> void
226366
def on_class_variable_and_write_node_enter(node)
227367
handle_variable_hover(node.name.to_s)
@@ -279,27 +419,37 @@ def generate_heredoc_hover(node)
279419
end
280420
end
281421

282-
#: (String keyword) -> void
283-
def handle_keyword_documentation(keyword)
284-
content = KEYWORD_DOCS[keyword]
285-
return unless content
422+
#: (String) -> void
423+
def handle_keyword_documentation(name)
424+
keyword = @graph.keyword(name)
425+
return unless keyword
286426

287-
doc_uri = URI::Generic.from_path(path: File.join(STATIC_DOCS_PATH, "#{keyword}.md"))
288-
289-
@response_builder.push("```ruby\n#{keyword}\n```", category: :title)
290-
@response_builder.push("[Read more](#{doc_uri})", category: :links)
291-
@response_builder.push(content, category: :documentation)
427+
@response_builder.push("```ruby\n#{keyword.name}\n```", category: :title)
428+
@response_builder.push(keyword.documentation, category: :documentation)
292429
end
293430

294-
#: -> void
295-
def handle_super_node_hover
296-
# Sorbet can handle super hover on typed true or higher
297-
return if @sorbet_level.true_or_higher?
431+
# Push keyword documentation when the cursor is on one of the provided locations. The keyword name is taken from
432+
# the covering location's slice so that operator forms (`&&`, `||`, `{`, `}`, ternary `? :`) yield no hover —
433+
# their slice is not a keyword in the Rubydex graph.
434+
#
435+
#: (*Prism::Location?) -> void
436+
def handle_keyword_at_location(*locations)
437+
loc = locations.find { |l| l && covers_position?(l, @position) }
438+
return unless loc
298439

299-
surrounding_method = @node_context.surrounding_method
300-
return unless surrounding_method
440+
handle_keyword_documentation(loc.slice)
441+
end
442+
443+
#: (Prism::Location keyword_location) -> void
444+
def handle_super_node_hover(keyword_location)
445+
# Sorbet can handle the inherited-method hover on typed true or higher, but it does not surface keyword docs, so
446+
# we still push those
447+
unless @sorbet_level.true_or_higher?
448+
surrounding_method = @node_context.surrounding_method
449+
handle_method_hover(surrounding_method, inherited_only: true) if surrounding_method
450+
end
301451

302-
handle_method_hover(surrounding_method, inherited_only: true)
452+
handle_keyword_at_location(keyword_location)
303453
end
304454

305455
#: (String message, ?inherited_only: bool) -> void

0 commit comments

Comments
 (0)