ruby-lsp 0.16.7 → 0.17.1
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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/VERSION +1 -1
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +139 -18
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +101 -12
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +183 -10
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +55 -9
- data/lib/ruby_indexer/test/configuration_test.rb +4 -5
- data/lib/ruby_indexer/test/constant_test.rb +8 -8
- data/lib/ruby_indexer/test/index_test.rb +528 -0
- data/lib/ruby_indexer/test/instance_variables_test.rb +131 -0
- data/lib/ruby_indexer/test/method_test.rb +37 -0
- data/lib/ruby_indexer/test/test_case.rb +3 -1
- data/lib/ruby_lsp/addon.rb +8 -8
- data/lib/ruby_lsp/document.rb +3 -3
- data/lib/ruby_lsp/internal.rb +1 -0
- data/lib/ruby_lsp/listeners/completion.rb +74 -17
- data/lib/ruby_lsp/listeners/definition.rb +62 -6
- data/lib/ruby_lsp/listeners/hover.rb +60 -6
- data/lib/ruby_lsp/listeners/signature_help.rb +4 -4
- data/lib/ruby_lsp/node_context.rb +28 -0
- data/lib/ruby_lsp/requests/code_action_resolve.rb +73 -2
- data/lib/ruby_lsp/requests/code_actions.rb +16 -15
- data/lib/ruby_lsp/requests/completion.rb +21 -13
- data/lib/ruby_lsp/requests/completion_resolve.rb +9 -0
- data/lib/ruby_lsp/requests/definition.rb +20 -5
- data/lib/ruby_lsp/requests/document_highlight.rb +2 -2
- data/lib/ruby_lsp/requests/hover.rb +5 -6
- data/lib/ruby_lsp/requests/on_type_formatting.rb +8 -4
- data/lib/ruby_lsp/requests/signature_help.rb +3 -3
- data/lib/ruby_lsp/requests/support/common.rb +4 -3
- data/lib/ruby_lsp/requests/workspace_symbol.rb +3 -1
- data/lib/ruby_lsp/server.rb +10 -4
- data/lib/ruby_lsp/test_helper.rb +1 -0
- metadata +4 -2
@@ -6,6 +6,7 @@ module RubyIndexer
|
|
6
6
|
extend T::Sig
|
7
7
|
|
8
8
|
class UnresolvableAliasError < StandardError; end
|
9
|
+
class NonExistingNamespaceError < StandardError; end
|
9
10
|
|
10
11
|
# The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query
|
11
12
|
ENTRY_SIMILARITY_THRESHOLD = 0.7
|
@@ -31,6 +32,9 @@ module RubyIndexer
|
|
31
32
|
|
32
33
|
# Holds all require paths for every indexed item so that we can provide autocomplete for requires
|
33
34
|
@require_paths_tree = T.let(PrefixTree[IndexablePath].new, PrefixTree[IndexablePath])
|
35
|
+
|
36
|
+
# Holds the linearized ancestors list for every namespace
|
37
|
+
@ancestors = T.let({}, T::Hash[String, T::Array[String]])
|
34
38
|
end
|
35
39
|
|
36
40
|
sig { params(indexable: IndexablePath).void }
|
@@ -126,6 +130,16 @@ module RubyIndexer
|
|
126
130
|
results.flat_map(&:first)
|
127
131
|
end
|
128
132
|
|
133
|
+
sig { params(name: String, receiver_name: String).returns(T::Array[Entry]) }
|
134
|
+
def method_completion_candidates(name, receiver_name)
|
135
|
+
ancestors = linearized_ancestors_of(receiver_name)
|
136
|
+
candidates = prefix_search(name).flatten
|
137
|
+
candidates.select! do |entry|
|
138
|
+
entry.is_a?(RubyIndexer::Entry::Member) && ancestors.any?(entry.owner&.name)
|
139
|
+
end
|
140
|
+
candidates
|
141
|
+
end
|
142
|
+
|
129
143
|
# Try to find the entry based on the nesting from the most specific to the least specific. For example, if we have
|
130
144
|
# the nesting as ["Foo", "Bar"] and the name as "Baz", we will try to find it in this order:
|
131
145
|
# 1. Foo::Bar::Baz
|
@@ -245,16 +259,175 @@ module RubyIndexer
|
|
245
259
|
sig { params(method_name: String, receiver_name: String).returns(T.nilable(T::Array[Entry::Member])) }
|
246
260
|
def resolve_method(method_name, receiver_name)
|
247
261
|
method_entries = self[method_name]
|
248
|
-
|
249
|
-
return unless
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
262
|
+
ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
|
263
|
+
return unless method_entries
|
264
|
+
|
265
|
+
ancestors.each do |ancestor|
|
266
|
+
found = method_entries.select do |entry|
|
267
|
+
next unless entry.is_a?(Entry::Member)
|
268
|
+
|
269
|
+
entry.owner&.name == ancestor
|
270
|
+
end
|
271
|
+
|
272
|
+
return T.cast(found, T::Array[Entry::Member]) if found.any?
|
273
|
+
end
|
274
|
+
|
275
|
+
nil
|
276
|
+
rescue NonExistingNamespaceError
|
277
|
+
nil
|
278
|
+
end
|
279
|
+
|
280
|
+
# Linearizes the ancestors for a given name, returning the order of namespaces in which Ruby will search for method
|
281
|
+
# or constant declarations.
|
282
|
+
#
|
283
|
+
# When we add an ancestor in Ruby, that namespace might have ancestors of its own. Therefore, we need to linearize
|
284
|
+
# everything recursively to ensure that we are placing ancestors in the right order. For example, if you include a
|
285
|
+
# module that prepends another module, then the prepend module appears before the included module.
|
286
|
+
#
|
287
|
+
# The order of ancestors is [linearized_prepends, self, linearized_includes, linearized_superclass]
|
288
|
+
sig { params(fully_qualified_name: String).returns(T::Array[String]) }
|
289
|
+
def linearized_ancestors_of(fully_qualified_name)
|
290
|
+
# If we already computed the ancestors for this namespace, return it straight away
|
291
|
+
cached_ancestors = @ancestors[fully_qualified_name]
|
292
|
+
return cached_ancestors if cached_ancestors
|
293
|
+
|
294
|
+
ancestors = [fully_qualified_name]
|
295
|
+
|
296
|
+
# Cache the linearized ancestors array eagerly. This is important because we might have circular dependencies and
|
297
|
+
# this will prevent us from falling into an infinite recursion loop. Because we mutate the ancestors array later,
|
298
|
+
# the cache will reflect the final result
|
299
|
+
@ancestors[fully_qualified_name] = ancestors
|
300
|
+
|
301
|
+
# If we don't have an entry for `name`, raise
|
302
|
+
entries = resolve(fully_qualified_name, [])
|
303
|
+
raise NonExistingNamespaceError, "No entry found for #{fully_qualified_name}" unless entries
|
304
|
+
|
305
|
+
# If none of the entries for `name` are namespaces, raise
|
306
|
+
namespaces = entries.filter_map do |entry|
|
307
|
+
case entry
|
308
|
+
when Entry::Namespace
|
309
|
+
entry
|
310
|
+
when Entry::Alias
|
311
|
+
self[entry.target]&.grep(Entry::Namespace)
|
312
|
+
end
|
313
|
+
end.flatten
|
314
|
+
|
315
|
+
raise NonExistingNamespaceError,
|
316
|
+
"None of the entries for #{fully_qualified_name} are modules or classes" if namespaces.empty?
|
317
|
+
|
318
|
+
mixin_operations = namespaces.flat_map(&:mixin_operations)
|
319
|
+
main_namespace_index = 0
|
320
|
+
|
321
|
+
# The original nesting where we discovered this namespace, so that we resolve the correct names of the
|
322
|
+
# included/prepended/extended modules and parent classes
|
323
|
+
nesting = T.must(namespaces.first).nesting
|
324
|
+
|
325
|
+
mixin_operations.each do |operation|
|
326
|
+
resolved_module = resolve(operation.module_name, nesting)
|
327
|
+
next unless resolved_module
|
328
|
+
|
329
|
+
module_fully_qualified_name = T.must(resolved_module.first).name
|
330
|
+
|
331
|
+
case operation
|
332
|
+
when Entry::Prepend
|
333
|
+
# When a module is prepended, Ruby checks if it hasn't been prepended already to prevent adding it in front of
|
334
|
+
# the actual namespace twice. However, it does not check if it has been included because you are allowed to
|
335
|
+
# prepend the same module after it has already been included
|
336
|
+
linearized_prepends = linearized_ancestors_of(module_fully_qualified_name)
|
337
|
+
|
338
|
+
# When there are duplicate prepended modules, we have to insert the new prepends after the existing ones. For
|
339
|
+
# example, if the current ancestors are `["A", "Foo"]` and we try to prepend `["A", "B"]`, then `"B"` has to
|
340
|
+
# be inserted after `"A`
|
341
|
+
uniq_prepends = linearized_prepends - T.must(ancestors[0...main_namespace_index])
|
342
|
+
insert_position = linearized_prepends.length - uniq_prepends.length
|
343
|
+
|
344
|
+
T.unsafe(ancestors).insert(
|
345
|
+
insert_position,
|
346
|
+
*(linearized_prepends - T.must(ancestors[0...main_namespace_index])),
|
347
|
+
)
|
348
|
+
|
349
|
+
main_namespace_index += linearized_prepends.length
|
350
|
+
when Entry::Include
|
351
|
+
# When including a module, Ruby will always prevent duplicate entries in case the module has already been
|
352
|
+
# prepended or included
|
353
|
+
linearized_includes = linearized_ancestors_of(module_fully_qualified_name)
|
354
|
+
T.unsafe(ancestors).insert(main_namespace_index + 1, *(linearized_includes - ancestors))
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
# Find the first class entry that has a parent class. Notice that if the developer makes a mistake and inherits
|
359
|
+
# from two diffent classes in different files, we simply ignore it
|
360
|
+
superclass = T.cast(namespaces.find { |n| n.is_a?(Entry::Class) && n.parent_class }, T.nilable(Entry::Class))
|
361
|
+
|
362
|
+
if superclass
|
363
|
+
# If the user makes a mistake and creates a class that inherits from itself, this method would throw a stack
|
364
|
+
# error. We need to ensure that this isn't the case
|
365
|
+
parent_class = T.must(superclass.parent_class)
|
366
|
+
|
367
|
+
resolved_parent_class = resolve(parent_class, nesting)
|
368
|
+
parent_class_name = resolved_parent_class&.first&.name
|
369
|
+
|
370
|
+
if parent_class_name && fully_qualified_name != parent_class_name
|
371
|
+
ancestors.concat(linearized_ancestors_of(parent_class_name))
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
ancestors
|
376
|
+
end
|
377
|
+
|
378
|
+
# Resolves an instance variable name for a given owner name. This method will linearize the ancestors of the owner
|
379
|
+
# and find inherited instance variables as well
|
380
|
+
sig { params(variable_name: String, owner_name: String).returns(T.nilable(T::Array[Entry::InstanceVariable])) }
|
381
|
+
def resolve_instance_variable(variable_name, owner_name)
|
382
|
+
entries = T.cast(self[variable_name], T.nilable(T::Array[Entry::InstanceVariable]))
|
383
|
+
return unless entries
|
384
|
+
|
385
|
+
ancestors = linearized_ancestors_of(owner_name)
|
386
|
+
return if ancestors.empty?
|
387
|
+
|
388
|
+
entries.select { |e| ancestors.include?(e.owner&.name) }
|
389
|
+
end
|
390
|
+
|
391
|
+
# Returns a list of possible candidates for completion of instance variables for a given owner name. The name must
|
392
|
+
# include the `@` prefix
|
393
|
+
sig { params(name: String, owner_name: String).returns(T::Array[Entry::InstanceVariable]) }
|
394
|
+
def instance_variable_completion_candidates(name, owner_name)
|
395
|
+
entries = T.cast(prefix_search(name).flatten, T::Array[Entry::InstanceVariable])
|
396
|
+
ancestors = linearized_ancestors_of(owner_name)
|
397
|
+
|
398
|
+
variables = entries.uniq(&:name)
|
399
|
+
variables.select! { |e| ancestors.any?(e.owner&.name) }
|
400
|
+
variables
|
401
|
+
end
|
402
|
+
|
403
|
+
# Synchronizes a change made to the given indexable path. This method will ensure that new declarations are indexed,
|
404
|
+
# removed declarations removed and that the ancestor linearization cache is cleared if necessary
|
405
|
+
sig { params(indexable: IndexablePath).void }
|
406
|
+
def handle_change(indexable)
|
407
|
+
original_entries = @files_to_entries[indexable.full_path]
|
408
|
+
|
409
|
+
delete(indexable)
|
410
|
+
index_single(indexable)
|
411
|
+
|
412
|
+
updated_entries = @files_to_entries[indexable.full_path]
|
413
|
+
|
414
|
+
return unless original_entries && updated_entries
|
415
|
+
|
416
|
+
# A change in one ancestor may impact several different others, which could be including that ancestor through
|
417
|
+
# indirect means like including a module that than includes the ancestor. Trying to figure out exactly which
|
418
|
+
# ancestors need to be deleted is too expensive. Therefore, if any of the namespace entries has a change to their
|
419
|
+
# ancestor hash, we clear all ancestors and start linearizing lazily again from scratch
|
420
|
+
original_map = T.cast(
|
421
|
+
original_entries.select { |e| e.is_a?(Entry::Namespace) },
|
422
|
+
T::Array[Entry::Namespace],
|
423
|
+
).to_h { |e| [e.name, e.ancestor_hash] }
|
424
|
+
|
425
|
+
updated_map = T.cast(
|
426
|
+
updated_entries.select { |e| e.is_a?(Entry::Namespace) },
|
427
|
+
T::Array[Entry::Namespace],
|
428
|
+
).to_h { |e| [e.name, e.ancestor_hash] }
|
429
|
+
|
430
|
+
@ancestors.clear if original_map.any? { |name, hash| updated_map[name] != hash }
|
258
431
|
end
|
259
432
|
|
260
433
|
private
|
@@ -290,13 +290,13 @@ module RubyIndexer
|
|
290
290
|
RUBY
|
291
291
|
|
292
292
|
b_const = @index["A::B"].first
|
293
|
-
assert_equal(
|
293
|
+
assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
|
294
294
|
|
295
295
|
c_const = @index["A::C"].first
|
296
|
-
assert_equal(
|
296
|
+
assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
|
297
297
|
|
298
298
|
d_const = @index["A::D"].first
|
299
|
-
assert_equal(
|
299
|
+
assert_equal(Entry::Visibility::PUBLIC, d_const.visibility)
|
300
300
|
end
|
301
301
|
|
302
302
|
def test_keeping_track_of_super_classes
|
@@ -369,13 +369,13 @@ module RubyIndexer
|
|
369
369
|
RUBY
|
370
370
|
|
371
371
|
foo = T.must(@index["Foo"][0])
|
372
|
-
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.
|
372
|
+
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
|
373
373
|
|
374
374
|
qux = T.must(@index["Foo::Qux"][0])
|
375
|
-
assert_equal(["Corge", "Corge", "Baz"], qux.
|
375
|
+
assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
|
376
376
|
|
377
377
|
constant_path_references = T.must(@index["ConstantPathReferences"][0])
|
378
|
-
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.
|
378
|
+
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
|
379
379
|
end
|
380
380
|
|
381
381
|
def test_keeping_track_of_prepended_modules
|
@@ -415,13 +415,59 @@ module RubyIndexer
|
|
415
415
|
RUBY
|
416
416
|
|
417
417
|
foo = T.must(@index["Foo"][0])
|
418
|
-
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.
|
418
|
+
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
|
419
419
|
|
420
420
|
qux = T.must(@index["Foo::Qux"][0])
|
421
|
-
assert_equal(["Corge", "Corge", "Baz"], qux.
|
421
|
+
assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
|
422
422
|
|
423
423
|
constant_path_references = T.must(@index["ConstantPathReferences"][0])
|
424
|
-
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.
|
424
|
+
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
|
425
|
+
end
|
426
|
+
|
427
|
+
def test_keeping_track_of_extended_modules
|
428
|
+
index(<<~RUBY)
|
429
|
+
class Foo
|
430
|
+
# valid syntaxes that we can index
|
431
|
+
extend A1
|
432
|
+
self.extend A2
|
433
|
+
extend A3, A4
|
434
|
+
self.extend A5, A6
|
435
|
+
|
436
|
+
# valid syntaxes that we cannot index because of their dynamic nature
|
437
|
+
extend some_variable_or_method_call
|
438
|
+
self.extend some_variable_or_method_call
|
439
|
+
|
440
|
+
def something
|
441
|
+
extend A7 # We should not index this because of this dynamic nature
|
442
|
+
end
|
443
|
+
|
444
|
+
# Valid inner class syntax definition with its own modules prepended
|
445
|
+
class Qux
|
446
|
+
extend Corge
|
447
|
+
self.extend Corge
|
448
|
+
extend Baz
|
449
|
+
|
450
|
+
extend some_variable_or_method_call
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
class ConstantPathReferences
|
455
|
+
extend Foo::Bar
|
456
|
+
self.extend Foo::Bar2
|
457
|
+
|
458
|
+
extend dynamic::Bar
|
459
|
+
extend Foo::
|
460
|
+
end
|
461
|
+
RUBY
|
462
|
+
|
463
|
+
foo = T.must(@index["Foo"][0])
|
464
|
+
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.mixin_operation_module_names)
|
465
|
+
|
466
|
+
qux = T.must(@index["Foo::Qux"][0])
|
467
|
+
assert_equal(["Corge", "Corge", "Baz"], qux.mixin_operation_module_names)
|
468
|
+
|
469
|
+
constant_path_references = T.must(@index["ConstantPathReferences"][0])
|
470
|
+
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.mixin_operation_module_names)
|
425
471
|
end
|
426
472
|
end
|
427
473
|
end
|
@@ -68,12 +68,11 @@ module RubyIndexer
|
|
68
68
|
end
|
69
69
|
|
70
70
|
def test_indexables_avoids_duplicates_if_bundle_path_is_inside_project
|
71
|
-
Bundler.settings.
|
72
|
-
|
71
|
+
Bundler.settings.temporary(path: "vendor/bundle") do
|
72
|
+
config = Configuration.new
|
73
73
|
|
74
|
-
|
75
|
-
|
76
|
-
Bundler.settings.set_global("path", nil)
|
74
|
+
assert_includes(config.instance_variable_get(:@excluded_patterns), "#{Dir.pwd}/vendor/bundle/**/*.rb")
|
75
|
+
end
|
77
76
|
end
|
78
77
|
|
79
78
|
def test_indexables_does_not_include_gems_own_installed_files
|
@@ -122,13 +122,13 @@ module RubyIndexer
|
|
122
122
|
RUBY
|
123
123
|
|
124
124
|
b_const = @index["A::B"].first
|
125
|
-
assert_equal(
|
125
|
+
assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
|
126
126
|
|
127
127
|
c_const = @index["A::C"].first
|
128
|
-
assert_equal(
|
128
|
+
assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
|
129
129
|
|
130
130
|
d_const = @index["A::D"].first
|
131
|
-
assert_equal(
|
131
|
+
assert_equal(Entry::Visibility::PUBLIC, d_const.visibility)
|
132
132
|
end
|
133
133
|
|
134
134
|
def test_marking_constants_as_private_reopening_namespaces
|
@@ -155,13 +155,13 @@ module RubyIndexer
|
|
155
155
|
RUBY
|
156
156
|
|
157
157
|
a_const = @index["A::B::CONST_A"].first
|
158
|
-
assert_equal(
|
158
|
+
assert_equal(Entry::Visibility::PRIVATE, a_const.visibility)
|
159
159
|
|
160
160
|
b_const = @index["A::B::CONST_B"].first
|
161
|
-
assert_equal(
|
161
|
+
assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
|
162
162
|
|
163
163
|
c_const = @index["A::B::CONST_C"].first
|
164
|
-
assert_equal(
|
164
|
+
assert_equal(Entry::Visibility::PRIVATE, c_const.visibility)
|
165
165
|
end
|
166
166
|
|
167
167
|
def test_marking_constants_as_private_with_receiver
|
@@ -179,10 +179,10 @@ module RubyIndexer
|
|
179
179
|
RUBY
|
180
180
|
|
181
181
|
a_const = @index["A::B::CONST_A"].first
|
182
|
-
assert_equal(
|
182
|
+
assert_equal(Entry::Visibility::PRIVATE, a_const.visibility)
|
183
183
|
|
184
184
|
b_const = @index["A::B::CONST_B"].first
|
185
|
-
assert_equal(
|
185
|
+
assert_equal(Entry::Visibility::PRIVATE, b_const.visibility)
|
186
186
|
end
|
187
187
|
|
188
188
|
def test_indexing_constant_aliases
|