ruby-lsp 0.16.5 → 0.16.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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