ruby-lsp 0.16.5 → 0.16.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,6 +16,17 @@ module RubyIndexer
16
16
  assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
17
17
  end
18
18
 
19
+ def test_conditional_method
20
+ index(<<~RUBY)
21
+ class Foo
22
+ def bar
23
+ end if condition
24
+ end
25
+ RUBY
26
+
27
+ assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
28
+ end
29
+
19
30
  def test_singleton_method_using_self_receiver
20
31
  index(<<~RUBY)
21
32
  class Foo
@@ -38,6 +49,28 @@ module RubyIndexer
38
49
  assert_no_entry("bar")
39
50
  end
40
51
 
52
+ def test_method_under_dynamic_class_or_module
53
+ index(<<~RUBY)
54
+ module Foo
55
+ class self::Bar
56
+ def bar
57
+ end
58
+ end
59
+ end
60
+
61
+ module Bar
62
+ def bar
63
+ end
64
+ end
65
+ RUBY
66
+
67
+ assert_equal(2, @index["bar"].length)
68
+ first_entry = T.must(@index["bar"].first)
69
+ assert_equal("Foo::self::Bar", first_entry.owner.name)
70
+ second_entry = T.must(@index["bar"].last)
71
+ assert_equal("Bar", second_entry.owner.name)
72
+ end
73
+
41
74
  def test_method_with_parameters
42
75
  index(<<~RUBY)
43
76
  class Foo
@@ -285,5 +318,28 @@ module RubyIndexer
285
318
 
286
319
  assert_no_entry("bar")
287
320
  end
321
+
322
+ def test_properly_tracks_multiple_levels_of_nesting
323
+ index(<<~RUBY)
324
+ module Foo
325
+ def first; end
326
+
327
+ module Bar
328
+ def second; end
329
+ end
330
+
331
+ def third; end
332
+ end
333
+ RUBY
334
+
335
+ entry = T.must(@index["first"]&.first)
336
+ assert_equal("Foo", T.must(entry.owner).name)
337
+
338
+ entry = T.must(@index["second"]&.first)
339
+ assert_equal("Foo::Bar", T.must(entry.owner).name)
340
+
341
+ entry = T.must(@index["third"]&.first)
342
+ assert_equal("Foo", T.must(entry.owner).name)
343
+ end
288
344
  end
289
345
  end
@@ -42,6 +42,8 @@ module RubyLsp
42
42
  @group_stack = T.let([], T::Array[String])
43
43
  @group_id = T.let(1, Integer)
44
44
  @group_id_stack = T.let([], T::Array[Integer])
45
+ # We want to avoid adding code lenses for nested definitions
46
+ @def_depth = T.let(0, Integer)
45
47
 
46
48
  dispatcher.register(
47
49
  self,
@@ -50,6 +52,7 @@ module RubyLsp
50
52
  :on_module_node_enter,
51
53
  :on_module_node_leave,
52
54
  :on_def_node_enter,
55
+ :on_def_node_leave,
53
56
  :on_call_node_enter,
54
57
  :on_call_node_leave,
55
58
  )
@@ -88,6 +91,9 @@ module RubyLsp
88
91
 
89
92
  sig { params(node: Prism::DefNode).void }
90
93
  def on_def_node_enter(node)
94
+ @def_depth += 1
95
+ return if @def_depth > 1
96
+
91
97
  class_name = @group_stack.last
92
98
  return unless class_name&.end_with?("Test")
93
99
 
@@ -105,6 +111,11 @@ module RubyLsp
105
111
  end
106
112
  end
107
113
 
114
+ sig { params(node: Prism::DefNode).void }
115
+ def on_def_node_leave(node)
116
+ @def_depth -= 1
117
+ end
118
+
108
119
  sig { params(node: Prism::ModuleNode).void }
109
120
  def on_module_node_enter(node)
110
121
  if (path = namespace_constant_name(node))
@@ -47,7 +47,7 @@ module RubyLsp
47
47
  @response_builder << build_entry_completion(
48
48
  complete_name,
49
49
  name,
50
- node,
50
+ range_from_location(node.location),
51
51
  entries,
52
52
  top_level?(complete_name),
53
53
  )
@@ -62,6 +62,53 @@ module RubyLsp
62
62
  name = constant_name(node)
63
63
  return if name.nil?
64
64
 
65
+ constant_path_completion(name, range_from_location(node.location))
66
+ end
67
+
68
+ sig { params(node: Prism::CallNode).void }
69
+ def on_call_node_enter(node)
70
+ receiver = node.receiver
71
+
72
+ # When writing `Foo::`, the AST assigns a method call node (because you can use that syntax to invoke singleton
73
+ # methods). However, in addition to providing method completion, we also need to show possible constant
74
+ # completions
75
+ if (receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)) &&
76
+ node.call_operator == "::"
77
+
78
+ name = constant_name(receiver)
79
+
80
+ if name
81
+ start_loc = node.location
82
+ end_loc = T.must(node.call_operator_loc)
83
+
84
+ constant_path_completion(
85
+ "#{name}::",
86
+ Interface::Range.new(
87
+ start: Interface::Position.new(line: start_loc.start_line - 1, character: start_loc.start_column),
88
+ end: Interface::Position.new(line: end_loc.end_line - 1, character: end_loc.end_column),
89
+ ),
90
+ )
91
+ return
92
+ end
93
+ end
94
+
95
+ name = node.message
96
+ return unless name
97
+
98
+ case name
99
+ when "require"
100
+ complete_require(node)
101
+ when "require_relative"
102
+ complete_require_relative(node)
103
+ else
104
+ complete_self_receiver_method(node, name) if !@typechecker_enabled && self_receiver?(node)
105
+ end
106
+ end
107
+
108
+ private
109
+
110
+ sig { params(name: String, range: Interface::Range).void }
111
+ def constant_path_completion(name, range)
65
112
  top_level_reference = if name.start_with?("::")
66
113
  name = name.delete_prefix("::")
67
114
  true
@@ -71,8 +118,13 @@ module RubyLsp
71
118
 
72
119
  # If we're trying to provide completion for an aliased namespace, we need to first discover it's real name in
73
120
  # order to find which possible constants match the desired search
74
- *namespace, incomplete_name = name.split("::")
75
- aliased_namespace = T.must(namespace).join("::")
121
+ aliased_namespace = if name.end_with?("::")
122
+ name.delete_suffix("::")
123
+ else
124
+ *namespace, incomplete_name = name.split("::")
125
+ T.must(namespace).join("::")
126
+ end
127
+
76
128
  namespace_entries = @index.resolve(aliased_namespace, @nesting)
77
129
  return unless namespace_entries
78
130
 
@@ -88,37 +140,19 @@ module RubyLsp
88
140
  first_entry = T.must(entries.first)
89
141
  next if first_entry.visibility == :private && !first_entry.name.start_with?("#{@nesting}::")
90
142
 
91
- constant_name = T.must(first_entry.name.split("::").last)
92
-
143
+ constant_name = first_entry.name.delete_prefix("#{real_namespace}::")
93
144
  full_name = aliased_namespace.empty? ? constant_name : "#{aliased_namespace}::#{constant_name}"
94
145
 
95
146
  @response_builder << build_entry_completion(
96
147
  full_name,
97
148
  name,
98
- node,
149
+ range,
99
150
  entries,
100
151
  top_level_reference || top_level?(T.must(entries.first).name),
101
152
  )
102
153
  end
103
154
  end
104
155
 
105
- sig { params(node: Prism::CallNode).void }
106
- def on_call_node_enter(node)
107
- name = node.message
108
- return unless name
109
-
110
- case name
111
- when "require"
112
- complete_require(node)
113
- when "require_relative"
114
- complete_require_relative(node)
115
- else
116
- complete_self_receiver_method(node, name) if !@typechecker_enabled && self_receiver?(node)
117
- end
118
- end
119
-
120
- private
121
-
122
156
  sig { params(node: Prism::CallNode).void }
123
157
  def complete_require(node)
124
158
  arguments_node = node.arguments
@@ -223,12 +257,12 @@ module RubyLsp
223
257
  params(
224
258
  real_name: String,
225
259
  incomplete_name: String,
226
- node: Prism::Node,
260
+ range: Interface::Range,
227
261
  entries: T::Array[RubyIndexer::Entry],
228
262
  top_level: T::Boolean,
229
263
  ).returns(Interface::CompletionItem)
230
264
  end
231
- def build_entry_completion(real_name, incomplete_name, node, entries, top_level)
265
+ def build_entry_completion(real_name, incomplete_name, range, entries, top_level)
232
266
  first_entry = T.must(entries.first)
233
267
  kind = case first_entry
234
268
  when RubyIndexer::Entry::Class
@@ -263,20 +297,22 @@ module RubyLsp
263
297
  #
264
298
  # Foo::B # --> completion inserts `Bar` instead of `Foo::Bar`
265
299
  # end
266
- @nesting.each do |namespace|
267
- prefix = "#{namespace}::"
268
- shortened_name = insertion_text.delete_prefix(prefix)
269
-
270
- # If a different entry exists for the shortened name, then there's a conflict and we should not shorten it
271
- conflict_name = "#{@nesting.join("::")}::#{shortened_name}"
272
- break if real_name != conflict_name && @index[conflict_name]
273
-
274
- insertion_text = shortened_name
275
-
276
- # If the user is typing a fully qualified name `Foo::Bar::Baz`, then we should not use the short name (e.g.:
277
- # `Baz`) as filtering. So we only shorten the filter text if the user is not including the namespaces in their
278
- # typing
279
- filter_text.delete_prefix!(prefix) unless incomplete_name.start_with?(prefix)
300
+ unless @nesting.join("::").start_with?(incomplete_name)
301
+ @nesting.each do |namespace|
302
+ prefix = "#{namespace}::"
303
+ shortened_name = insertion_text.delete_prefix(prefix)
304
+
305
+ # If a different entry exists for the shortened name, then there's a conflict and we should not shorten it
306
+ conflict_name = "#{@nesting.join("::")}::#{shortened_name}"
307
+ break if real_name != conflict_name && @index[conflict_name]
308
+
309
+ insertion_text = shortened_name
310
+
311
+ # If the user is typing a fully qualified name `Foo::Bar::Baz`, then we should not use the short name (e.g.:
312
+ # `Baz`) as filtering. So we only shorten the filter text if the user is not including the namespaces in
313
+ # their typing
314
+ filter_text.delete_prefix!(prefix) unless incomplete_name.start_with?(prefix)
315
+ end
280
316
  end
281
317
 
282
318
  # When using a top level constant reference (e.g.: `::Bar`), the editor includes the `::` as part of the filter.
@@ -286,7 +322,7 @@ module RubyLsp
286
322
  label: real_name,
287
323
  filter_text: filter_text,
288
324
  text_edit: Interface::TextEdit.new(
289
- range: range_from_node(node),
325
+ range: range,
290
326
  new_text: insertion_text,
291
327
  ),
292
328
  kind: kind,
@@ -30,6 +30,7 @@ module RubyLsp
30
30
  dispatcher.register(
31
31
  self,
32
32
  :on_call_node_enter,
33
+ :on_block_argument_node_enter,
33
34
  :on_constant_read_node_enter,
34
35
  :on_constant_path_node_enter,
35
36
  )
@@ -42,10 +43,21 @@ module RubyLsp
42
43
  if message == :require || message == :require_relative
43
44
  handle_require_definition(node)
44
45
  else
45
- handle_method_definition(node)
46
+ handle_method_definition(message.to_s, self_receiver?(node))
46
47
  end
47
48
  end
48
49
 
50
+ sig { params(node: Prism::BlockArgumentNode).void }
51
+ def on_block_argument_node_enter(node)
52
+ expression = node.expression
53
+ return unless expression.is_a?(Prism::SymbolNode)
54
+
55
+ value = expression.value
56
+ return unless value
57
+
58
+ handle_method_definition(value, false)
59
+ end
60
+
49
61
  sig { params(node: Prism::ConstantPathNode).void }
50
62
  def on_constant_path_node_enter(node)
51
63
  name = constant_name(node)
@@ -64,12 +76,9 @@ module RubyLsp
64
76
 
65
77
  private
66
78
 
67
- sig { params(node: Prism::CallNode).void }
68
- def handle_method_definition(node)
69
- message = node.message
70
- return unless message
71
-
72
- methods = if self_receiver?(node)
79
+ sig { params(message: String, self_receiver: T::Boolean).void }
80
+ def handle_method_definition(message, self_receiver)
81
+ methods = if self_receiver
73
82
  @index.resolve_method(message, @nesting.join("::"))
74
83
  else
75
84
  # If the method doesn't have a receiver, then we provide a few candidates to jump to
@@ -271,7 +271,7 @@ module RubyLsp
271
271
  def on_constant_path_node_enter(node)
272
272
  return unless matches?(node, CONSTANT_PATH_NODES)
273
273
 
274
- add_highlight(Constant::DocumentHighlightKind::READ, node.location)
274
+ add_highlight(Constant::DocumentHighlightKind::READ, node.name_loc)
275
275
  end
276
276
 
277
277
  sig { params(node: Prism::ConstantReadNode).void }
@@ -30,7 +30,7 @@ module RubyLsp
30
30
  lookup[spec.name] = {}
31
31
  lookup[spec.name][spec.version.to_s] = {}
32
32
 
33
- Dir.glob("**/*.rb", base: "#{spec.full_gem_path}/").each do |path|
33
+ Dir.glob("**/*.rb", base: "#{spec.full_gem_path.delete_prefix("//?/")}/").each do |path|
34
34
  lookup[spec.name][spec.version.to_s][path] = "#{spec.full_gem_path}/#{path}"
35
35
  end
36
36
  end
@@ -58,6 +58,7 @@ module RubyLsp
58
58
  :on_constant_operator_write_node_enter,
59
59
  :on_constant_or_write_node_enter,
60
60
  :on_constant_target_node_enter,
61
+ :on_constant_path_node_enter,
61
62
  :on_local_variable_and_write_node_enter,
62
63
  :on_local_variable_operator_write_node_enter,
63
64
  :on_local_variable_or_write_node_enter,
@@ -302,17 +303,64 @@ module RubyLsp
302
303
  def on_class_node_enter(node)
303
304
  return unless visible?(node, @range)
304
305
 
305
- @response_builder.add_token(node.constant_path.location, :class, [:declaration])
306
+ constant_path = node.constant_path
307
+
308
+ if constant_path.is_a?(Prism::ConstantReadNode)
309
+ @response_builder.add_token(constant_path.location, :class, [:declaration])
310
+ else
311
+ each_constant_path_part(constant_path) do |part|
312
+ loc = case part
313
+ when Prism::ConstantPathNode
314
+ part.name_loc
315
+ when Prism::ConstantReadNode
316
+ part.location
317
+ end
318
+ next unless loc
319
+
320
+ @response_builder.add_token(loc, :class, [:declaration])
321
+ end
322
+ end
306
323
 
307
324
  superclass = node.superclass
308
- @response_builder.add_token(superclass.location, :class) if superclass
325
+
326
+ if superclass.is_a?(Prism::ConstantReadNode)
327
+ @response_builder.add_token(superclass.location, :class)
328
+ elsif superclass
329
+ each_constant_path_part(superclass) do |part|
330
+ loc = case part
331
+ when Prism::ConstantPathNode
332
+ part.name_loc
333
+ when Prism::ConstantReadNode
334
+ part.location
335
+ end
336
+ next unless loc
337
+
338
+ @response_builder.add_token(loc, :class)
339
+ end
340
+ end
309
341
  end
310
342
 
311
343
  sig { params(node: Prism::ModuleNode).void }
312
344
  def on_module_node_enter(node)
313
345
  return unless visible?(node, @range)
314
346
 
315
- @response_builder.add_token(node.constant_path.location, :namespace, [:declaration])
347
+ constant_path = node.constant_path
348
+
349
+ if constant_path.is_a?(Prism::ConstantReadNode)
350
+ @response_builder.add_token(constant_path.location, :namespace, [:declaration])
351
+ else
352
+ each_constant_path_part(constant_path) do |part|
353
+ loc = case part
354
+ when Prism::ConstantPathNode
355
+ part.name_loc
356
+ when Prism::ConstantReadNode
357
+ part.location
358
+ end
359
+ next unless loc
360
+
361
+ @response_builder.add_token(loc, :namespace, [:declaration])
362
+ end
363
+ end
316
364
  end
317
365
 
318
366
  sig { params(node: Prism::ImplicitNode).void }
@@ -327,6 +375,14 @@ module RubyLsp
327
375
  @inside_implicit_node = false
328
376
  end
329
377
 
378
+ sig { params(node: Prism::ConstantPathNode).void }
379
+ def on_constant_path_node_enter(node)
380
+ return if @inside_implicit_node
381
+ return unless visible?(node, @range)
382
+
383
+ @response_builder.add_token(node.name_loc, :namespace)
384
+ end
385
+
330
386
  private
331
387
 
332
388
  # Textmate provides highlighting for a subset of these special Ruby-specific methods. We want to utilize that
@@ -11,6 +11,7 @@ module RubyLsp
11
11
  # suggests possible completions according to what the developer is typing.
12
12
  #
13
13
  # Currently supported targets:
14
+ #
14
15
  # - Classes
15
16
  # - Modules
16
17
  # - Constants
@@ -34,7 +35,7 @@ module RubyLsp
34
35
  def provider
35
36
  Interface::CompletionOptions.new(
36
37
  resolve_provider: true,
37
- trigger_characters: ["/", "\"", "'"],
38
+ trigger_characters: ["/", "\"", "'", ":"],
38
39
  completion_item: {
39
40
  labelDetailsSupport: true,
40
41
  },
@@ -36,20 +36,27 @@ module RubyLsp
36
36
  @item = item
37
37
  end
38
38
 
39
- sig { override.returns(Interface::CompletionItem) }
39
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
40
40
  def perform
41
+ # Based on the spec https://microsoft.github.io/language-server-protocol/specification#textDocument_completion,
42
+ # a completion resolve request must always return the original completion item without modifying ANY fields
43
+ # other than label details and documentation. If we modify anything, the completion behaviour might be broken.
44
+ #
45
+ # For example, forgetting to return the `insertText` included in the original item will make the editor use the
46
+ # `label` for the text edit instead
41
47
  label = @item[:label]
42
48
  entries = @index[label] || []
43
- Interface::CompletionItem.new(
44
- label: label,
45
- label_details: Interface::CompletionItemLabelDetails.new(
46
- description: entries.take(MAX_DOCUMENTATION_ENTRIES).map(&:file_name).join(","),
47
- ),
48
- documentation: Interface::MarkupContent.new(
49
- kind: "markdown",
50
- value: markdown_from_index_entries(label, entries, MAX_DOCUMENTATION_ENTRIES),
51
- ),
49
+
50
+ @item[:labelDetails] = Interface::CompletionItemLabelDetails.new(
51
+ description: entries.take(MAX_DOCUMENTATION_ENTRIES).map(&:file_name).join(","),
52
+ )
53
+
54
+ @item[:documentation] = Interface::MarkupContent.new(
55
+ kind: "markdown",
56
+ value: markdown_from_index_entries(label, entries, MAX_DOCUMENTATION_ENTRIES),
52
57
  )
58
+
59
+ @item
53
60
  end
54
61
  end
55
62
  end
@@ -12,6 +12,7 @@ module RubyLsp
12
12
  # definition of the symbol under the cursor.
13
13
  #
14
14
  # Currently supported targets:
15
+ #
15
16
  # - Classes
16
17
  # - Modules
17
18
  # - Constants
@@ -43,40 +44,49 @@ module RubyLsp
43
44
  ResponseBuilders::CollectionResponseBuilder[Interface::Location].new,
44
45
  ResponseBuilders::CollectionResponseBuilder[Interface::Location],
45
46
  )
47
+ @dispatcher = dispatcher
46
48
 
47
49
  target, parent, nesting = document.locate_node(
48
50
  position,
49
- node_types: [Prism::CallNode, Prism::ConstantReadNode, Prism::ConstantPathNode],
51
+ node_types: [Prism::CallNode, Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::BlockArgumentNode],
50
52
  )
51
53
 
52
54
  if target.is_a?(Prism::ConstantReadNode) && parent.is_a?(Prism::ConstantPathNode)
55
+ # If the target is part of a constant path node, we need to find the exact portion of the constant that the
56
+ # user is requesting to go to definition for
53
57
  target = determine_target(
54
58
  target,
55
59
  parent,
56
60
  position,
57
61
  )
62
+ elsif target.is_a?(Prism::CallNode) && target.name != :require && target.name != :require_relative &&
63
+ !covers_position?(target.message_loc, position)
64
+ # If the target is a method call, we need to ensure that the requested position is exactly on top of the
65
+ # method identifier. Otherwise, we risk showing definitions for unrelated things
66
+ target = nil
58
67
  end
59
68
 
60
- Listeners::Definition.new(
61
- @response_builder,
62
- global_state,
63
- document.uri,
64
- nesting,
65
- dispatcher,
66
- typechecker_enabled,
67
- )
69
+ if target
70
+ Listeners::Definition.new(
71
+ @response_builder,
72
+ global_state,
73
+ document.uri,
74
+ nesting,
75
+ dispatcher,
76
+ typechecker_enabled,
77
+ )
68
78
 
69
- Addon.addons.each do |addon|
70
- addon.create_definition_listener(@response_builder, document.uri, nesting, dispatcher)
79
+ Addon.addons.each do |addon|
80
+ addon.create_definition_listener(@response_builder, document.uri, nesting, dispatcher)
81
+ end
71
82
  end
72
83
 
73
84
  @target = T.let(target, T.nilable(Prism::Node))
74
- @dispatcher = dispatcher
75
85
  end
76
86
 
77
87
  sig { override.returns(T::Array[Interface::Location]) }
78
88
  def perform
79
- @dispatcher.dispatch_once(@target)
89
+ @dispatcher.dispatch_once(@target) if @target
80
90
  @response_builder.response
81
91
  end
82
92
  end
@@ -41,25 +41,29 @@ module RubyLsp
41
41
  end
42
42
  def initialize(document, global_state, position, dispatcher, typechecker_enabled)
43
43
  super()
44
- @target = T.let(nil, T.nilable(Prism::Node))
45
- @target, parent, nesting = document.locate_node(
44
+ target, parent, nesting = document.locate_node(
46
45
  position,
47
46
  node_types: Listeners::Hover::ALLOWED_TARGETS,
48
47
  )
49
48
 
50
49
  if (Listeners::Hover::ALLOWED_TARGETS.include?(parent.class) &&
51
- !Listeners::Hover::ALLOWED_TARGETS.include?(@target.class)) ||
52
- (parent.is_a?(Prism::ConstantPathNode) && @target.is_a?(Prism::ConstantReadNode))
53
- @target = determine_target(
54
- T.must(@target),
50
+ !Listeners::Hover::ALLOWED_TARGETS.include?(target.class)) ||
51
+ (parent.is_a?(Prism::ConstantPathNode) && target.is_a?(Prism::ConstantReadNode))
52
+ target = determine_target(
53
+ T.must(target),
55
54
  T.must(parent),
56
55
  position,
57
56
  )
57
+ elsif target.is_a?(Prism::CallNode) && target.name != :require && target.name != :require_relative &&
58
+ !covers_position?(target.message_loc, position)
59
+
60
+ target = nil
58
61
  end
59
62
 
60
63
  # Don't need to instantiate any listeners if there's no target
61
- return unless @target
64
+ return unless target
62
65
 
66
+ @target = T.let(target, T.nilable(Prism::Node))
63
67
  uri = document.uri
64
68
  @response_builder = T.let(ResponseBuilders::Hover.new, ResponseBuilders::Hover)
65
69
  Listeners::Hover.new(@response_builder, global_state, uri, nesting, dispatcher, typechecker_enabled)
@@ -65,6 +65,20 @@ module RubyLsp
65
65
 
66
66
  target
67
67
  end
68
+
69
+ # Checks if a given location covers the position requested
70
+ sig { params(location: T.nilable(Prism::Location), position: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
71
+ def covers_position?(location, position)
72
+ return false unless location
73
+
74
+ start_line = location.start_line - 1
75
+ end_line = location.end_line - 1
76
+ line = position[:line]
77
+ character = position[:character]
78
+
79
+ (start_line < line || (start_line == line && location.start_column <= character)) &&
80
+ (end_line > line || (end_line == line && location.end_column >= character))
81
+ end
68
82
  end
69
83
  end
70
84
  end
@@ -50,12 +50,12 @@ module RubyLsp
50
50
  params(
51
51
  node: Prism::Node,
52
52
  title: String,
53
- command_name: String,
53
+ command_name: T.nilable(String),
54
54
  arguments: T.nilable(T::Array[T.untyped]),
55
55
  data: T.nilable(T::Hash[T.untyped, T.untyped]),
56
56
  ).returns(Interface::CodeLens)
57
57
  end
58
- def create_code_lens(node, title:, command_name:, arguments:, data:)
58
+ def create_code_lens(node, title:, command_name: nil, arguments: nil, data: nil)
59
59
  range = range_from_node(node)
60
60
 
61
61
  Interface::CodeLens.new(
@@ -167,6 +167,24 @@ module RubyLsp
167
167
  constant_name(path)
168
168
  end
169
169
  end
170
+
171
+ # Iterates over each part of a constant path, so that we can easily push response items for each section of the
172
+ # name. For example, for `Foo::Bar::Baz`, this method will invoke the block with `Foo`, then `Bar` and finally
173
+ # `Baz`.
174
+ sig do
175
+ params(
176
+ node: Prism::Node,
177
+ block: T.proc.params(part: Prism::Node).void,
178
+ ).void
179
+ end
180
+ def each_constant_path_part(node, &block)
181
+ current = T.let(node, T.nilable(Prism::Node))
182
+
183
+ while current.is_a?(Prism::ConstantPathNode)
184
+ block.call(current)
185
+ current = current.parent
186
+ end
187
+ end
170
188
  end
171
189
  end
172
190
  end