ruby-lsp 0.17.3 → 0.17.4

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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +241 -91
  4. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +74 -102
  5. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +81 -19
  6. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +50 -2
  7. data/lib/ruby_indexer/test/classes_and_modules_test.rb +46 -0
  8. data/lib/ruby_indexer/test/index_test.rb +326 -27
  9. data/lib/ruby_indexer/test/instance_variables_test.rb +84 -7
  10. data/lib/ruby_indexer/test/method_test.rb +54 -24
  11. data/lib/ruby_indexer/test/rbs_indexer_test.rb +27 -2
  12. data/lib/ruby_lsp/document.rb +37 -8
  13. data/lib/ruby_lsp/global_state.rb +7 -3
  14. data/lib/ruby_lsp/internal.rb +1 -0
  15. data/lib/ruby_lsp/listeners/completion.rb +53 -14
  16. data/lib/ruby_lsp/listeners/definition.rb +11 -7
  17. data/lib/ruby_lsp/listeners/hover.rb +14 -7
  18. data/lib/ruby_lsp/listeners/signature_help.rb +5 -2
  19. data/lib/ruby_lsp/node_context.rb +6 -1
  20. data/lib/ruby_lsp/requests/completion.rb +5 -4
  21. data/lib/ruby_lsp/requests/completion_resolve.rb +8 -0
  22. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +88 -0
  23. data/lib/ruby_lsp/requests/support/common.rb +19 -1
  24. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +12 -4
  25. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +91 -0
  26. data/lib/ruby_lsp/requests/workspace_symbol.rb +1 -21
  27. data/lib/ruby_lsp/requests.rb +2 -0
  28. data/lib/ruby_lsp/server.rb +54 -15
  29. data/lib/ruby_lsp/test_helper.rb +1 -1
  30. data/lib/ruby_lsp/type_inferrer.rb +86 -0
  31. metadata +5 -2
@@ -13,7 +13,7 @@ module RubyIndexer
13
13
  end
14
14
  RUBY
15
15
 
16
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
16
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
17
17
  end
18
18
 
19
19
  def test_conditional_method
@@ -24,7 +24,7 @@ module RubyIndexer
24
24
  end
25
25
  RUBY
26
26
 
27
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
27
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
28
28
  end
29
29
 
30
30
  def test_singleton_method_using_self_receiver
@@ -35,7 +35,12 @@ module RubyIndexer
35
35
  end
36
36
  RUBY
37
37
 
38
- assert_entry("bar", Entry::SingletonMethod, "/fake/path/foo.rb:1-2:2-5")
38
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
39
+
40
+ entry = T.must(@index["bar"].first)
41
+ owner = T.must(entry.owner)
42
+ assert_equal("Foo::<Class:Foo>", owner.name)
43
+ assert_instance_of(Entry::SingletonClass, owner)
39
44
  end
40
45
 
41
46
  def test_singleton_method_using_other_receiver_is_not_indexed
@@ -83,9 +88,9 @@ module RubyIndexer
83
88
  def baz; end
84
89
  RUBY
85
90
 
86
- assert_entry("foo", Entry::InstanceMethod, "/fake/path/foo.rb:0-8:1-3", visibility: Entry::Visibility::PRIVATE)
87
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:3-0:3-12", visibility: Entry::Visibility::PUBLIC)
88
- assert_entry("baz", Entry::InstanceMethod, "/fake/path/foo.rb:7-0:7-12", visibility: Entry::Visibility::PROTECTED)
91
+ assert_entry("foo", Entry::Method, "/fake/path/foo.rb:0-8:1-3", visibility: Entry::Visibility::PRIVATE)
92
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:3-0:3-12", visibility: Entry::Visibility::PUBLIC)
93
+ assert_entry("baz", Entry::Method, "/fake/path/foo.rb:7-0:7-12", visibility: Entry::Visibility::PROTECTED)
89
94
  end
90
95
 
91
96
  def test_visibility_tracking_with_nested_class_or_modules
@@ -103,9 +108,9 @@ module RubyIndexer
103
108
  end
104
109
  RUBY
105
110
 
106
- assert_entry("foo", Entry::InstanceMethod, "/fake/path/foo.rb:3-2:3-14", visibility: Entry::Visibility::PRIVATE)
107
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:6-4:6-16", visibility: Entry::Visibility::PUBLIC)
108
- assert_entry("baz", Entry::InstanceMethod, "/fake/path/foo.rb:9-2:9-14", visibility: Entry::Visibility::PRIVATE)
111
+ assert_entry("foo", Entry::Method, "/fake/path/foo.rb:3-2:3-14", visibility: Entry::Visibility::PRIVATE)
112
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:6-4:6-16", visibility: Entry::Visibility::PUBLIC)
113
+ assert_entry("baz", Entry::Method, "/fake/path/foo.rb:9-2:9-14", visibility: Entry::Visibility::PRIVATE)
109
114
  end
110
115
 
111
116
  def test_method_with_parameters
@@ -116,7 +121,7 @@ module RubyIndexer
116
121
  end
117
122
  RUBY
118
123
 
119
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
124
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
120
125
  entry = T.must(@index["bar"].first)
121
126
  assert_equal(1, entry.parameters.length)
122
127
  parameter = entry.parameters.first
@@ -132,7 +137,7 @@ module RubyIndexer
132
137
  end
133
138
  RUBY
134
139
 
135
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
140
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
136
141
  entry = T.must(@index["bar"].first)
137
142
  assert_equal(1, entry.parameters.length)
138
143
  parameter = entry.parameters.first
@@ -148,7 +153,7 @@ module RubyIndexer
148
153
  end
149
154
  RUBY
150
155
 
151
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
156
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
152
157
  entry = T.must(@index["bar"].first)
153
158
  assert_equal(1, entry.parameters.length)
154
159
  parameter = entry.parameters.first
@@ -164,7 +169,7 @@ module RubyIndexer
164
169
  end
165
170
  RUBY
166
171
 
167
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
172
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
168
173
  entry = T.must(@index["bar"].first)
169
174
  assert_equal(2, entry.parameters.length)
170
175
  a, b = entry.parameters
@@ -184,7 +189,7 @@ module RubyIndexer
184
189
  end
185
190
  RUBY
186
191
 
187
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
192
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
188
193
  entry = T.must(@index["bar"].first)
189
194
  assert_equal(2, entry.parameters.length)
190
195
  a, b = entry.parameters
@@ -209,7 +214,7 @@ module RubyIndexer
209
214
  end
210
215
  RUBY
211
216
 
212
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
217
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
213
218
  entry = T.must(@index["bar"].first)
214
219
  assert_equal(2, entry.parameters.length)
215
220
  a, b = entry.parameters
@@ -246,7 +251,7 @@ module RubyIndexer
246
251
  end
247
252
  RUBY
248
253
 
249
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
254
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
250
255
  entry = T.must(@index["bar"].first)
251
256
  assert_equal(1, entry.parameters.length)
252
257
  param = entry.parameters.first
@@ -287,7 +292,7 @@ module RubyIndexer
287
292
  end
288
293
  RUBY
289
294
 
290
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
295
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
291
296
  entry = T.must(@index["bar"].first)
292
297
  assert_equal(2, entry.parameters.length)
293
298
  first, second = entry.parameters
@@ -307,7 +312,7 @@ module RubyIndexer
307
312
  end
308
313
  RUBY
309
314
 
310
- assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
315
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:2-5")
311
316
  entry = T.must(@index["bar"].first)
312
317
  assert_empty(entry.parameters)
313
318
  end
@@ -359,23 +364,23 @@ module RubyIndexer
359
364
  def test_properly_tracks_multiple_levels_of_nesting
360
365
  index(<<~RUBY)
361
366
  module Foo
362
- def first; end
367
+ def first_method; end
363
368
 
364
369
  module Bar
365
- def second; end
370
+ def second_method; end
366
371
  end
367
372
 
368
- def third; end
373
+ def third_method; end
369
374
  end
370
375
  RUBY
371
376
 
372
- entry = T.must(@index["first"]&.first)
377
+ entry = T.cast(@index["first_method"]&.first, Entry::Method)
373
378
  assert_equal("Foo", T.must(entry.owner).name)
374
379
 
375
- entry = T.must(@index["second"]&.first)
380
+ entry = T.cast(@index["second_method"]&.first, Entry::Method)
376
381
  assert_equal("Foo::Bar", T.must(entry.owner).name)
377
382
 
378
- entry = T.must(@index["third"]&.first)
383
+ entry = T.cast(@index["third_method"]&.first, Entry::Method)
379
384
  assert_equal("Foo", T.must(entry.owner).name)
380
385
  end
381
386
 
@@ -398,5 +403,30 @@ module RubyIndexer
398
403
  # Foo plus 3 valid aliases
399
404
  assert_equal(4, @index.instance_variable_get(:@entries).length - @default_indexed_entries.length)
400
405
  end
406
+
407
+ def test_singleton_methods
408
+ index(<<~RUBY)
409
+ class Foo
410
+ def self.bar; end
411
+
412
+ class << self
413
+ def baz; end
414
+ end
415
+ end
416
+ RUBY
417
+
418
+ assert_entry("bar", Entry::Method, "/fake/path/foo.rb:1-2:1-19")
419
+ assert_entry("baz", Entry::Method, "/fake/path/foo.rb:4-4:4-16")
420
+
421
+ bar_owner = T.must(T.must(@index["bar"].first).owner)
422
+ baz_owner = T.must(T.must(@index["baz"].first).owner)
423
+
424
+ assert_instance_of(Entry::SingletonClass, bar_owner)
425
+ assert_instance_of(Entry::SingletonClass, baz_owner)
426
+
427
+ # Regardless of whether the method was added through `self.something` or `class << self`, the owner object must be
428
+ # the exact same
429
+ assert_same(bar_owner, baz_owner)
430
+ end
401
431
  end
402
432
  end
@@ -8,8 +8,9 @@ module RubyIndexer
8
8
  def test_index_core_classes
9
9
  entries = @index["Array"]
10
10
  refute_nil(entries)
11
- assert_equal(1, entries.length)
12
- entry = entries.first
11
+ # Array is a class but also an instance method on Kernel
12
+ assert_equal(2, entries.length)
13
+ entry = entries.find { |entry| entry.is_a?(RubyIndexer::Entry::Class) }
13
14
  assert_match(%r{/gems/rbs-.*/core/array.rbs}, entry.file_path)
14
15
  assert_equal("array.rbs", entry.file_name)
15
16
  assert_equal("Object", entry.parent_class)
@@ -38,5 +39,29 @@ module RubyIndexer
38
39
  assert_equal(0, entry.location.start_column)
39
40
  assert_operator(entry.location.end_column, :>, 0)
40
41
  end
42
+
43
+ def test_index_methods
44
+ entries = @index["initialize"]
45
+ refute_nil(entries)
46
+ entry = entries.find { |entry| entry.owner.name == "Array" }
47
+ assert_match(%r{/gems/rbs-.*/core/array.rbs}, entry.file_path)
48
+ assert_equal("array.rbs", entry.file_name)
49
+ assert_equal(Entry::Visibility::PUBLIC, entry.visibility)
50
+
51
+ # Using fixed positions would be fragile, so let's just check some basics.
52
+ assert_operator(entry.location.start_line, :>, 0)
53
+ assert_operator(entry.location.end_line, :>, entry.location.start_line)
54
+ assert_equal(2, entry.location.start_column)
55
+ assert_operator(entry.location.end_column, :>, 0)
56
+ end
57
+
58
+ def test_attaches_correct_owner_to_singleton_methods
59
+ entries = @index["basename"]
60
+ refute_nil(entries)
61
+
62
+ owner = entries.first.owner
63
+ assert_instance_of(Entry::SingletonClass, owner)
64
+ assert_equal("File::<Class:File>", owner.name)
65
+ end
41
66
  end
42
67
  end
@@ -127,7 +127,10 @@ module RubyLsp
127
127
  queue = T.let(node.child_nodes.compact, T::Array[T.nilable(Prism::Node)])
128
128
  closest = node
129
129
  parent = T.let(nil, T.nilable(Prism::Node))
130
- nesting = T.let([], T::Array[T.any(Prism::ClassNode, Prism::ModuleNode)])
130
+ nesting_nodes = T.let(
131
+ [],
132
+ T::Array[T.any(Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefNode)],
133
+ )
131
134
  call_node = T.let(nil, T.nilable(Prism::CallNode))
132
135
 
133
136
  until queue.empty?
@@ -151,13 +154,18 @@ module RubyLsp
151
154
 
152
155
  # If the candidate starts after the end of the previous nesting level, then we've exited that nesting level and
153
156
  # need to pop the stack
154
- previous_level = nesting.last
155
- nesting.pop if previous_level && loc.start_offset > previous_level.location.end_offset
157
+ previous_level = nesting_nodes.last
158
+ nesting_nodes.pop if previous_level && loc.start_offset > previous_level.location.end_offset
156
159
 
157
160
  # Keep track of the nesting where we found the target. This is used to determine the fully qualified name of the
158
161
  # target when it is a constant
159
- if candidate.is_a?(Prism::ClassNode) || candidate.is_a?(Prism::ModuleNode)
160
- nesting << candidate
162
+ case candidate
163
+ when Prism::ClassNode, Prism::ModuleNode
164
+ nesting_nodes << candidate
165
+ when Prism::SingletonClassNode
166
+ nesting_nodes << candidate
167
+ when Prism::DefNode
168
+ nesting_nodes << candidate
161
169
  end
162
170
 
163
171
  if candidate.is_a?(Prism::CallNode)
@@ -189,11 +197,32 @@ module RubyLsp
189
197
  # The correct target is `Foo::Bar` with an empty nesting. `Foo::Bar` should not appear in the nesting stack, even
190
198
  # though the class/module node does indeed enclose the target, because it would lead to incorrect behavior
191
199
  if closest.is_a?(Prism::ConstantReadNode) || closest.is_a?(Prism::ConstantPathNode)
192
- last_level = nesting.last
193
- nesting.pop if last_level && last_level.constant_path == closest
200
+ last_level = nesting_nodes.last
201
+
202
+ if (last_level.is_a?(Prism::ModuleNode) || last_level.is_a?(Prism::ClassNode)) &&
203
+ last_level.constant_path == closest
204
+ nesting_nodes.pop
205
+ end
206
+ end
207
+
208
+ nesting = []
209
+ surrounding_method = T.let(nil, T.nilable(String))
210
+
211
+ nesting_nodes.each do |node|
212
+ case node
213
+ when Prism::ClassNode, Prism::ModuleNode
214
+ nesting << node.constant_path.slice
215
+ when Prism::SingletonClassNode
216
+ nesting << "<Class:#{nesting.last}>"
217
+ when Prism::DefNode
218
+ surrounding_method = node.name.to_s
219
+ next unless node.receiver.is_a?(Prism::SelfNode)
220
+
221
+ nesting << "<Class:#{nesting.last}>"
222
+ end
194
223
  end
195
224
 
196
- NodeContext.new(closest, parent, nesting.map { |n| n.constant_path.location.slice }, call_node)
225
+ NodeContext.new(closest, parent, nesting, call_node, surrounding_method)
197
226
  end
198
227
 
199
228
  sig { returns(T::Boolean) }
@@ -12,7 +12,7 @@ module RubyLsp
12
12
  attr_accessor :formatter
13
13
 
14
14
  sig { returns(T::Boolean) }
15
- attr_reader :typechecker
15
+ attr_reader :has_type_checker
16
16
 
17
17
  sig { returns(RubyIndexer::Index) }
18
18
  attr_reader :index
@@ -23,6 +23,9 @@ module RubyLsp
23
23
  sig { returns(T::Boolean) }
24
24
  attr_reader :supports_watching_files
25
25
 
26
+ sig { returns(TypeInferrer) }
27
+ attr_reader :type_inferrer
28
+
26
29
  sig { void }
27
30
  def initialize
28
31
  @workspace_uri = T.let(URI::Generic.from_path(path: Dir.pwd), URI::Generic)
@@ -31,8 +34,9 @@ module RubyLsp
31
34
  @formatter = T.let("auto", String)
32
35
  @linters = T.let([], T::Array[String])
33
36
  @test_library = T.let("minitest", String)
34
- @typechecker = T.let(true, T::Boolean)
37
+ @has_type_checker = T.let(true, T::Boolean)
35
38
  @index = T.let(RubyIndexer::Index.new, RubyIndexer::Index)
39
+ @type_inferrer = T.let(TypeInferrer.new(@index), TypeInferrer)
36
40
  @supported_formatters = T.let({}, T::Hash[String, Requests::Support::Formatter])
37
41
  @supports_watching_files = T.let(false, T::Boolean)
38
42
  end
@@ -66,7 +70,7 @@ module RubyLsp
66
70
  specified_linters = options.dig(:initializationOptions, :linters)
67
71
  @linters = specified_linters || detect_linters(direct_dependencies)
68
72
  @test_library = detect_test_library(direct_dependencies)
69
- @typechecker = detect_typechecker(direct_dependencies)
73
+ @has_type_checker = detect_typechecker(direct_dependencies)
70
74
 
71
75
  encodings = options.dig(:capabilities, :general, :positionEncodings)
72
76
  @encoding = if !encodings || encodings.empty?
@@ -28,6 +28,7 @@ require "ruby_lsp/utils"
28
28
  require "ruby_lsp/parameter_scope"
29
29
  require "ruby_lsp/global_state"
30
30
  require "ruby_lsp/server"
31
+ require "ruby_lsp/type_inferrer"
31
32
  require "ruby_lsp/requests"
32
33
  require "ruby_lsp/response_builders"
33
34
  require "ruby_lsp/node_context"
@@ -15,15 +15,26 @@ module RubyLsp
15
15
  typechecker_enabled: T::Boolean,
16
16
  dispatcher: Prism::Dispatcher,
17
17
  uri: URI::Generic,
18
+ trigger_character: T.nilable(String),
18
19
  ).void
19
20
  end
20
- def initialize(response_builder, global_state, node_context, typechecker_enabled, dispatcher, uri) # rubocop:disable Metrics/ParameterLists
21
+ def initialize( # rubocop:disable Metrics/ParameterLists
22
+ response_builder,
23
+ global_state,
24
+ node_context,
25
+ typechecker_enabled,
26
+ dispatcher,
27
+ uri,
28
+ trigger_character
29
+ )
21
30
  @response_builder = response_builder
22
31
  @global_state = global_state
23
32
  @index = T.let(global_state.index, RubyIndexer::Index)
33
+ @type_inferrer = T.let(global_state.type_inferrer, TypeInferrer)
24
34
  @node_context = node_context
25
35
  @typechecker_enabled = typechecker_enabled
26
36
  @uri = uri
37
+ @trigger_character = trigger_character
27
38
 
28
39
  dispatcher.register(
29
40
  self,
@@ -42,7 +53,7 @@ module RubyLsp
42
53
  # Handle completion on regular constant references (e.g. `Bar`)
43
54
  sig { params(node: Prism::ConstantReadNode).void }
44
55
  def on_constant_read_node_enter(node)
45
- return if @global_state.typechecker
56
+ return if @global_state.has_type_checker
46
57
 
47
58
  name = constant_name(node)
48
59
  return if name.nil?
@@ -63,7 +74,7 @@ module RubyLsp
63
74
  # Handle completion on namespaced constant references (e.g. `Foo::Bar`)
64
75
  sig { params(node: Prism::ConstantPathNode).void }
65
76
  def on_constant_path_node_enter(node)
66
- return if @global_state.typechecker
77
+ return if @global_state.has_type_checker
67
78
 
68
79
  name = constant_name(node)
69
80
  return if name.nil?
@@ -107,7 +118,7 @@ module RubyLsp
107
118
  when "require_relative"
108
119
  complete_require_relative(node)
109
120
  else
110
- complete_self_receiver_method(node, name) if !@typechecker_enabled && self_receiver?(node)
121
+ complete_methods(node, name) unless @typechecker_enabled
111
122
  end
112
123
  end
113
124
 
@@ -158,7 +169,7 @@ module RubyLsp
158
169
  name.delete_suffix("::")
159
170
  else
160
171
  *namespace, incomplete_name = name.split("::")
161
- T.must(namespace).join("::")
172
+ namespace.join("::")
162
173
  end
163
174
 
164
175
  nesting = @node_context.nesting
@@ -192,7 +203,10 @@ module RubyLsp
192
203
 
193
204
  sig { params(name: String, location: Prism::Location).void }
194
205
  def handle_instance_variable_completion(name, location)
195
- @index.instance_variable_completion_candidates(name, @node_context.fully_qualified_name).each do |entry|
206
+ type = @type_inferrer.infer_receiver_type(@node_context)
207
+ return unless type
208
+
209
+ @index.instance_variable_completion_candidates(name, type).each do |entry|
196
210
  variable_name = entry.name
197
211
 
198
212
  @response_builder << Interface::CompletionItem.new(
@@ -257,20 +271,45 @@ module RubyLsp
257
271
  end
258
272
 
259
273
  sig { params(node: Prism::CallNode, name: String).void }
260
- def complete_self_receiver_method(node, name)
261
- receiver_entries = @index[@node_context.fully_qualified_name]
262
- return unless receiver_entries
274
+ def complete_methods(node, name)
275
+ type = @type_inferrer.infer_receiver_type(@node_context)
276
+ return unless type
277
+
278
+ # When the trigger character is a dot, Prism matches the name of the call node to whatever is next in the source
279
+ # code, leading to us searching for the wrong name. What we want to do instead is show every available method
280
+ # when dot is pressed
281
+ method_name = @trigger_character == "." ? nil : name
282
+
283
+ range = if method_name
284
+ range_from_location(T.must(node.message_loc))
285
+ else
286
+ loc = T.must(node.call_operator_loc)
287
+ Interface::Range.new(
288
+ start: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column + 1),
289
+ end: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column + 1),
290
+ )
291
+ end
263
292
 
264
- receiver = T.must(receiver_entries.first)
293
+ @index.method_completion_candidates(method_name, type).each do |entry|
294
+ entry_name = entry.name
265
295
 
266
- @index.method_completion_candidates(name, receiver.name).each do |entry|
267
- @response_builder << build_method_completion(T.cast(entry, RubyIndexer::Entry::Member), node)
296
+ @response_builder << Interface::CompletionItem.new(
297
+ label: entry_name,
298
+ filter_text: entry_name,
299
+ text_edit: Interface::TextEdit.new(range: range, new_text: entry_name),
300
+ kind: Constant::CompletionItemKind::METHOD,
301
+ data: {
302
+ owner_name: entry.owner&.name,
303
+ },
304
+ )
268
305
  end
306
+ rescue RubyIndexer::Index::NonExistingNamespaceError
307
+ # We have not indexed this namespace, so we can't provide any completions
269
308
  end
270
309
 
271
310
  sig do
272
311
  params(
273
- entry: RubyIndexer::Entry::Member,
312
+ entry: T.any(RubyIndexer::Entry::Member, RubyIndexer::Entry::MethodAlias),
274
313
  node: Prism::CallNode,
275
314
  ).returns(Interface::CompletionItem)
276
315
  end
@@ -283,7 +322,7 @@ module RubyLsp
283
322
  text_edit: Interface::TextEdit.new(range: range_from_location(T.must(node.message_loc)), new_text: name),
284
323
  kind: Constant::CompletionItemKind::METHOD,
285
324
  label_details: Interface::CompletionItemLabelDetails.new(
286
- detail: "(#{entry.parameters.map(&:decorated_name).join(", ")})",
325
+ detail: entry.decorated_parameters,
287
326
  description: entry.file_name,
288
327
  ),
289
328
  documentation: Interface::MarkupContent.new(
@@ -23,6 +23,7 @@ module RubyLsp
23
23
  @response_builder = response_builder
24
24
  @global_state = global_state
25
25
  @index = T.let(global_state.index, RubyIndexer::Index)
26
+ @type_inferrer = T.let(global_state.type_inferrer, TypeInferrer)
26
27
  @uri = uri
27
28
  @node_context = node_context
28
29
  @typechecker_enabled = typechecker_enabled
@@ -48,7 +49,7 @@ module RubyLsp
48
49
  message = node.message
49
50
  return unless message
50
51
 
51
- handle_method_definition(message, self_receiver?(node))
52
+ handle_method_definition(message, @type_inferrer.infer_receiver_type(@node_context))
52
53
  end
53
54
 
54
55
  sig { params(node: Prism::StringNode).void }
@@ -70,7 +71,7 @@ module RubyLsp
70
71
  value = expression.value
71
72
  return unless value
72
73
 
73
- handle_method_definition(value, false)
74
+ handle_method_definition(value, nil)
74
75
  end
75
76
 
76
77
  sig { params(node: Prism::ConstantPathNode).void }
@@ -123,7 +124,10 @@ module RubyLsp
123
124
 
124
125
  sig { params(name: String).void }
125
126
  def handle_instance_variable_definition(name)
126
- entries = @index.resolve_instance_variable(name, @node_context.fully_qualified_name)
127
+ type = @type_inferrer.infer_receiver_type(@node_context)
128
+ return unless type
129
+
130
+ entries = @index.resolve_instance_variable(name, type)
127
131
  return unless entries
128
132
 
129
133
  entries.each do |entry|
@@ -141,10 +145,10 @@ module RubyLsp
141
145
  # If by any chance we haven't indexed the owner, then there's no way to find the right declaration
142
146
  end
143
147
 
144
- sig { params(message: String, self_receiver: T::Boolean).void }
145
- def handle_method_definition(message, self_receiver)
146
- methods = if self_receiver
147
- @index.resolve_method(message, @node_context.fully_qualified_name)
148
+ sig { params(message: String, receiver_type: T.nilable(String)).void }
149
+ def handle_method_definition(message, receiver_type)
150
+ methods = if receiver_type
151
+ @index.resolve_method(message, receiver_type)
148
152
  else
149
153
  # If the method doesn't have a receiver, then we provide a few candidates to jump to
150
154
  # But we don't want to provide too many candidates, as it can be overwhelming
@@ -47,6 +47,7 @@ module RubyLsp
47
47
  @response_builder = response_builder
48
48
  @global_state = global_state
49
49
  @index = T.let(global_state.index, RubyIndexer::Index)
50
+ @type_inferrer = T.let(global_state.type_inferrer, TypeInferrer)
50
51
  @path = T.let(uri.to_standardized_path, T.nilable(String))
51
52
  @node_context = node_context
52
53
  @typechecker_enabled = typechecker_enabled
@@ -78,14 +79,14 @@ module RubyLsp
78
79
 
79
80
  sig { params(node: Prism::ConstantWriteNode).void }
80
81
  def on_constant_write_node_enter(node)
81
- return if @global_state.typechecker
82
+ return if @global_state.has_type_checker
82
83
 
83
84
  generate_hover(node.name.to_s, node.name_loc)
84
85
  end
85
86
 
86
87
  sig { params(node: Prism::ConstantPathNode).void }
87
88
  def on_constant_path_node_enter(node)
88
- return if @global_state.typechecker
89
+ return if @global_state.has_type_checker
89
90
 
90
91
  name = constant_name(node)
91
92
  return if name.nil?
@@ -95,8 +96,6 @@ module RubyLsp
95
96
 
96
97
  sig { params(node: Prism::CallNode).void }
97
98
  def on_call_node_enter(node)
98
- return unless self_receiver?(node)
99
-
100
99
  if @path && File.basename(@path) == GEMFILE_NAME && node.name == :gem
101
100
  generate_gem_hover(node)
102
101
  return
@@ -107,10 +106,15 @@ module RubyLsp
107
106
  message = node.message
108
107
  return unless message
109
108
 
110
- methods = @index.resolve_method(message, @node_context.fully_qualified_name)
109
+ type = @type_inferrer.infer_receiver_type(@node_context)
110
+ return unless type
111
+
112
+ methods = @index.resolve_method(message, type)
111
113
  return unless methods
112
114
 
113
- categorized_markdown_from_index_entries(message, methods).each do |category, content|
115
+ title = "#{message}#{T.must(methods.first).decorated_parameters}"
116
+
117
+ categorized_markdown_from_index_entries(title, methods).each do |category, content|
114
118
  @response_builder.push(content, category: category)
115
119
  end
116
120
  end
@@ -149,7 +153,10 @@ module RubyLsp
149
153
 
150
154
  sig { params(name: String).void }
151
155
  def handle_instance_variable_hover(name)
152
- entries = @index.resolve_instance_variable(name, @node_context.fully_qualified_name)
156
+ type = @type_inferrer.infer_receiver_type(@node_context)
157
+ return unless type
158
+
159
+ entries = @index.resolve_instance_variable(name, type)
153
160
  return unless entries
154
161
 
155
162
  categorized_markdown_from_index_entries(name, entries).each do |category, content|
@@ -21,6 +21,7 @@ module RubyLsp
21
21
  @response_builder = response_builder
22
22
  @global_state = global_state
23
23
  @index = T.let(global_state.index, RubyIndexer::Index)
24
+ @type_inferrer = T.let(global_state.type_inferrer, TypeInferrer)
24
25
  @node_context = node_context
25
26
  dispatcher.register(self, :on_call_node_enter)
26
27
  end
@@ -28,12 +29,14 @@ module RubyLsp
28
29
  sig { params(node: Prism::CallNode).void }
29
30
  def on_call_node_enter(node)
30
31
  return if @typechecker_enabled
31
- return unless self_receiver?(node)
32
32
 
33
33
  message = node.message
34
34
  return unless message
35
35
 
36
- methods = @index.resolve_method(message, @node_context.fully_qualified_name)
36
+ type = @type_inferrer.infer_receiver_type(@node_context)
37
+ return unless type
38
+
39
+ methods = @index.resolve_method(message, type)
37
40
  return unless methods
38
41
 
39
42
  target_method = methods.first
@@ -16,19 +16,24 @@ module RubyLsp
16
16
  sig { returns(T.nilable(Prism::CallNode)) }
17
17
  attr_reader :call_node
18
18
 
19
+ sig { returns(T.nilable(String)) }
20
+ attr_reader :surrounding_method
21
+
19
22
  sig do
20
23
  params(
21
24
  node: T.nilable(Prism::Node),
22
25
  parent: T.nilable(Prism::Node),
23
26
  nesting: T::Array[String],
24
27
  call_node: T.nilable(Prism::CallNode),
28
+ surrounding_method: T.nilable(String),
25
29
  ).void
26
30
  end
27
- def initialize(node, parent, nesting, call_node)
31
+ def initialize(node, parent, nesting, call_node, surrounding_method)
28
32
  @node = node
29
33
  @parent = parent
30
34
  @nesting = nesting
31
35
  @call_node = call_node
36
+ @surrounding_method = surrounding_method
32
37
  end
33
38
 
34
39
  sig { returns(String) }