ruby-lsp 0.22.1 → 0.23.10

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +12 -11
  5. data/exe/ruby-lsp-check +5 -5
  6. data/exe/ruby-lsp-launcher +41 -15
  7. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +26 -20
  8. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +191 -100
  9. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +60 -30
  10. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +174 -61
  11. data/lib/ruby_indexer/lib/ruby_indexer/location.rb +12 -0
  12. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +16 -14
  13. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +82 -61
  14. data/lib/{core_ext → ruby_indexer/lib/ruby_indexer}/uri.rb +29 -3
  15. data/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb +36 -0
  16. data/lib/ruby_indexer/ruby_indexer.rb +2 -1
  17. data/lib/ruby_indexer/test/class_variables_test.rb +140 -0
  18. data/lib/ruby_indexer/test/classes_and_modules_test.rb +30 -6
  19. data/lib/ruby_indexer/test/configuration_test.rb +116 -51
  20. data/lib/ruby_indexer/test/enhancements_test.rb +2 -2
  21. data/lib/ruby_indexer/test/index_test.rb +143 -44
  22. data/lib/ruby_indexer/test/instance_variables_test.rb +20 -0
  23. data/lib/ruby_indexer/test/method_test.rb +86 -8
  24. data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
  25. data/lib/ruby_indexer/test/reference_finder_test.rb +90 -2
  26. data/lib/ruby_indexer/test/test_case.rb +2 -2
  27. data/lib/ruby_indexer/test/uri_test.rb +72 -0
  28. data/lib/ruby_lsp/addon.rb +9 -0
  29. data/lib/ruby_lsp/base_server.rb +17 -18
  30. data/lib/ruby_lsp/client_capabilities.rb +7 -1
  31. data/lib/ruby_lsp/document.rb +72 -10
  32. data/lib/ruby_lsp/erb_document.rb +5 -3
  33. data/lib/ruby_lsp/global_state.rb +42 -3
  34. data/lib/ruby_lsp/internal.rb +3 -1
  35. data/lib/ruby_lsp/listeners/code_lens.rb +9 -5
  36. data/lib/ruby_lsp/listeners/completion.rb +78 -6
  37. data/lib/ruby_lsp/listeners/definition.rb +80 -19
  38. data/lib/ruby_lsp/listeners/document_highlight.rb +3 -2
  39. data/lib/ruby_lsp/listeners/document_link.rb +21 -3
  40. data/lib/ruby_lsp/listeners/document_symbol.rb +12 -1
  41. data/lib/ruby_lsp/listeners/folding_ranges.rb +1 -1
  42. data/lib/ruby_lsp/listeners/hover.rb +59 -2
  43. data/lib/ruby_lsp/load_sorbet.rb +3 -3
  44. data/lib/ruby_lsp/rbs_document.rb +2 -2
  45. data/lib/ruby_lsp/requests/code_action_resolve.rb +90 -6
  46. data/lib/ruby_lsp/requests/code_actions.rb +57 -1
  47. data/lib/ruby_lsp/requests/completion.rb +8 -1
  48. data/lib/ruby_lsp/requests/completion_resolve.rb +2 -1
  49. data/lib/ruby_lsp/requests/definition.rb +7 -1
  50. data/lib/ruby_lsp/requests/diagnostics.rb +1 -1
  51. data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
  52. data/lib/ruby_lsp/requests/folding_ranges.rb +2 -6
  53. data/lib/ruby_lsp/requests/formatting.rb +2 -6
  54. data/lib/ruby_lsp/requests/hover.rb +1 -1
  55. data/lib/ruby_lsp/requests/on_type_formatting.rb +2 -2
  56. data/lib/ruby_lsp/requests/prepare_rename.rb +51 -0
  57. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +1 -1
  58. data/lib/ruby_lsp/requests/references.rb +29 -2
  59. data/lib/ruby_lsp/requests/rename.rb +17 -7
  60. data/lib/ruby_lsp/requests/semantic_highlighting.rb +1 -1
  61. data/lib/ruby_lsp/requests/show_syntax_tree.rb +1 -4
  62. data/lib/ruby_lsp/requests/signature_help.rb +1 -1
  63. data/lib/ruby_lsp/requests/support/common.rb +2 -9
  64. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +3 -3
  65. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +13 -13
  66. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +1 -1
  67. data/lib/ruby_lsp/requests/workspace_symbol.rb +4 -3
  68. data/lib/ruby_lsp/ruby_document.rb +80 -6
  69. data/lib/ruby_lsp/scripts/compose_bundle.rb +1 -1
  70. data/lib/ruby_lsp/server.rb +205 -61
  71. data/lib/ruby_lsp/setup_bundler.rb +50 -43
  72. data/lib/ruby_lsp/store.rb +7 -7
  73. data/lib/ruby_lsp/test_helper.rb +45 -11
  74. data/lib/ruby_lsp/type_inferrer.rb +60 -31
  75. data/lib/ruby_lsp/utils.rb +63 -3
  76. metadata +8 -8
  77. data/lib/ruby_indexer/lib/ruby_indexer/indexable_path.rb +0 -29
@@ -16,8 +16,8 @@ module RubyIndexer
16
16
  sig { returns(String) }
17
17
  attr_reader :name
18
18
 
19
- sig { returns(String) }
20
- attr_reader :file_path
19
+ sig { returns(URI::Generic) }
20
+ attr_reader :uri
21
21
 
22
22
  sig { returns(RubyIndexer::Location) }
23
23
  attr_reader :location
@@ -30,14 +30,14 @@ module RubyIndexer
30
30
  sig do
31
31
  params(
32
32
  name: String,
33
- file_path: String,
33
+ uri: URI::Generic,
34
34
  location: Location,
35
35
  comments: T.nilable(String),
36
36
  ).void
37
37
  end
38
- def initialize(name, file_path, location, comments)
38
+ def initialize(name, uri, location, comments)
39
39
  @name = name
40
- @file_path = file_path
40
+ @uri = uri
41
41
  @comments = comments
42
42
  @visibility = T.let(Visibility::PUBLIC, Visibility)
43
43
  @location = location
@@ -60,14 +60,24 @@ module RubyIndexer
60
60
 
61
61
  sig { returns(String) }
62
62
  def file_name
63
- File.basename(@file_path)
63
+ if @uri.scheme == "untitled"
64
+ T.must(@uri.opaque)
65
+ else
66
+ File.basename(T.must(file_path))
67
+ end
68
+ end
69
+
70
+ sig { returns(T.nilable(String)) }
71
+ def file_path
72
+ @uri.full_path
64
73
  end
65
74
 
66
75
  sig { returns(String) }
67
76
  def comments
68
77
  @comments ||= begin
69
78
  # Parse only the comments based on the file path, which is much faster than parsing the entire file
70
- parsed_comments = Prism.parse_file_comments(@file_path)
79
+ path = file_path
80
+ parsed_comments = path ? Prism.parse_file_comments(path) : []
71
81
 
72
82
  # Group comments based on whether they belong to a single block of comments
73
83
  grouped = parsed_comments.slice_when do |left, right|
@@ -137,18 +147,18 @@ module RubyIndexer
137
147
  sig do
138
148
  params(
139
149
  nesting: T::Array[String],
140
- file_path: String,
150
+ uri: URI::Generic,
141
151
  location: Location,
142
152
  name_location: Location,
143
153
  comments: T.nilable(String),
144
154
  ).void
145
155
  end
146
- def initialize(nesting, file_path, location, name_location, comments)
156
+ def initialize(nesting, uri, location, name_location, comments)
147
157
  @name = T.let(nesting.join("::"), String)
148
158
  # The original nesting where this namespace was discovered
149
159
  @nesting = nesting
150
160
 
151
- super(@name, file_path, location, comments)
161
+ super(@name, uri, location, comments)
152
162
 
153
163
  @name_location = name_location
154
164
  end
@@ -186,15 +196,15 @@ module RubyIndexer
186
196
  sig do
187
197
  params(
188
198
  nesting: T::Array[String],
189
- file_path: String,
199
+ uri: URI::Generic,
190
200
  location: Location,
191
201
  name_location: Location,
192
202
  comments: T.nilable(String),
193
203
  parent_class: T.nilable(String),
194
204
  ).void
195
205
  end
196
- def initialize(nesting, file_path, location, name_location, comments, parent_class) # rubocop:disable Metrics/ParameterLists
197
- super(nesting, file_path, location, name_location, comments)
206
+ def initialize(nesting, uri, location, name_location, comments, parent_class) # rubocop:disable Metrics/ParameterLists
207
+ super(nesting, uri, location, name_location, comments)
198
208
  @parent_class = parent_class
199
209
  end
200
210
 
@@ -332,15 +342,15 @@ module RubyIndexer
332
342
  sig do
333
343
  params(
334
344
  name: String,
335
- file_path: String,
345
+ uri: URI::Generic,
336
346
  location: Location,
337
347
  comments: T.nilable(String),
338
348
  visibility: Visibility,
339
349
  owner: T.nilable(Entry::Namespace),
340
350
  ).void
341
351
  end
342
- def initialize(name, file_path, location, comments, visibility, owner) # rubocop:disable Metrics/ParameterLists
343
- super(name, file_path, location, comments)
352
+ def initialize(name, uri, location, comments, visibility, owner) # rubocop:disable Metrics/ParameterLists
353
+ super(name, uri, location, comments)
344
354
  @visibility = visibility
345
355
  @owner = owner
346
356
  end
@@ -399,7 +409,7 @@ module RubyIndexer
399
409
  sig do
400
410
  params(
401
411
  name: String,
402
- file_path: String,
412
+ uri: URI::Generic,
403
413
  location: Location,
404
414
  name_location: Location,
405
415
  comments: T.nilable(String),
@@ -408,8 +418,8 @@ module RubyIndexer
408
418
  owner: T.nilable(Entry::Namespace),
409
419
  ).void
410
420
  end
411
- def initialize(name, file_path, location, name_location, comments, signatures, visibility, owner) # rubocop:disable Metrics/ParameterLists
412
- super(name, file_path, location, comments, visibility, owner)
421
+ def initialize(name, uri, location, name_location, comments, signatures, visibility, owner) # rubocop:disable Metrics/ParameterLists
422
+ super(name, uri, location, comments, visibility, owner)
413
423
  @signatures = signatures
414
424
  @name_location = name_location
415
425
  end
@@ -439,13 +449,13 @@ module RubyIndexer
439
449
  target: String,
440
450
  nesting: T::Array[String],
441
451
  name: String,
442
- file_path: String,
452
+ uri: URI::Generic,
443
453
  location: Location,
444
454
  comments: T.nilable(String),
445
455
  ).void
446
456
  end
447
- def initialize(target, nesting, name, file_path, location, comments) # rubocop:disable Metrics/ParameterLists
448
- super(name, file_path, location, comments)
457
+ def initialize(target, nesting, name, uri, location, comments) # rubocop:disable Metrics/ParameterLists
458
+ super(name, uri, location, comments)
449
459
 
450
460
  @target = target
451
461
  @nesting = nesting
@@ -463,7 +473,7 @@ module RubyIndexer
463
473
  def initialize(target, unresolved_alias)
464
474
  super(
465
475
  unresolved_alias.name,
466
- unresolved_alias.file_path,
476
+ unresolved_alias.uri,
467
477
  unresolved_alias.location,
468
478
  unresolved_alias.comments,
469
479
  )
@@ -476,6 +486,26 @@ module RubyIndexer
476
486
  # Represents a global variable e.g.: $DEBUG
477
487
  class GlobalVariable < Entry; end
478
488
 
489
+ # Represents a class variable e.g.: @@a = 1
490
+ class ClassVariable < Entry
491
+ sig { returns(T.nilable(Entry::Namespace)) }
492
+ attr_reader :owner
493
+
494
+ sig do
495
+ params(
496
+ name: String,
497
+ uri: URI::Generic,
498
+ location: Location,
499
+ comments: T.nilable(String),
500
+ owner: T.nilable(Entry::Namespace),
501
+ ).void
502
+ end
503
+ def initialize(name, uri, location, comments, owner)
504
+ super(name, uri, location, comments)
505
+ @owner = owner
506
+ end
507
+ end
508
+
479
509
  # Represents an instance variable e.g.: @a = 1
480
510
  class InstanceVariable < Entry
481
511
  sig { returns(T.nilable(Entry::Namespace)) }
@@ -484,14 +514,14 @@ module RubyIndexer
484
514
  sig do
485
515
  params(
486
516
  name: String,
487
- file_path: String,
517
+ uri: URI::Generic,
488
518
  location: Location,
489
519
  comments: T.nilable(String),
490
520
  owner: T.nilable(Entry::Namespace),
491
521
  ).void
492
522
  end
493
- def initialize(name, file_path, location, comments, owner)
494
- super(name, file_path, location, comments)
523
+ def initialize(name, uri, location, comments, owner)
524
+ super(name, uri, location, comments)
495
525
  @owner = owner
496
526
  end
497
527
  end
@@ -513,13 +543,13 @@ module RubyIndexer
513
543
  new_name: String,
514
544
  old_name: String,
515
545
  owner: T.nilable(Entry::Namespace),
516
- file_path: String,
546
+ uri: URI::Generic,
517
547
  location: Location,
518
548
  comments: T.nilable(String),
519
549
  ).void
520
550
  end
521
- def initialize(new_name, old_name, owner, file_path, location, comments) # rubocop:disable Metrics/ParameterLists
522
- super(new_name, file_path, location, comments)
551
+ def initialize(new_name, old_name, owner, uri, location, comments) # rubocop:disable Metrics/ParameterLists
552
+ super(new_name, uri, location, comments)
523
553
 
524
554
  @new_name = new_name
525
555
  @old_name = old_name
@@ -547,7 +577,7 @@ module RubyIndexer
547
577
 
548
578
  super(
549
579
  unresolved_alias.new_name,
550
- unresolved_alias.file_path,
580
+ unresolved_alias.uri,
551
581
  unresolved_alias.location,
552
582
  full_comments,
553
583
  )
@@ -15,6 +15,45 @@ module RubyIndexer
15
15
  sig { returns(Configuration) }
16
16
  attr_reader :configuration
17
17
 
18
+ class << self
19
+ extend T::Sig
20
+
21
+ # Returns the real nesting of a constant name taking into account top level
22
+ # references that may be included anywhere in the name or nesting where that
23
+ # constant was found
24
+ sig { params(stack: T::Array[String], name: String).returns(T::Array[String]) }
25
+ def actual_nesting(stack, name)
26
+ nesting = stack + [name]
27
+ corrected_nesting = []
28
+
29
+ nesting.reverse_each do |name|
30
+ corrected_nesting.prepend(name.delete_prefix("::"))
31
+
32
+ break if name.start_with?("::")
33
+ end
34
+
35
+ corrected_nesting
36
+ end
37
+
38
+ # Returns the unresolved name for a constant reference including all parts of a constant path, or `nil` if the
39
+ # constant contains dynamic or incomplete parts
40
+ sig do
41
+ params(
42
+ node: T.any(
43
+ Prism::ConstantPathNode,
44
+ Prism::ConstantReadNode,
45
+ Prism::ConstantPathTargetNode,
46
+ ),
47
+ ).returns(T.nilable(String))
48
+ end
49
+ def constant_name(node)
50
+ node.full_name
51
+ rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
52
+ Prism::ConstantPathNode::MissingNodesInConstantPathError
53
+ nil
54
+ end
55
+ end
56
+
18
57
  sig { void }
19
58
  def initialize
20
59
  # Holds all entries in the index using the following format:
@@ -29,13 +68,14 @@ module RubyIndexer
29
68
 
30
69
  # Holds references to where entries where discovered so that we can easily delete them
31
70
  # {
32
- # "/my/project/foo.rb" => [#<Entry::Class>, #<Entry::Class>],
33
- # "/my/project/bar.rb" => [#<Entry::Class>],
71
+ # "file:///my/project/foo.rb" => [#<Entry::Class>, #<Entry::Class>],
72
+ # "file:///my/project/bar.rb" => [#<Entry::Class>],
73
+ # "untitled:Untitled-1" => [#<Entry::Class>],
34
74
  # }
35
- @files_to_entries = T.let({}, T::Hash[String, T::Array[Entry]])
75
+ @uris_to_entries = T.let({}, T::Hash[String, T::Array[Entry]])
36
76
 
37
77
  # Holds all require paths for every indexed item so that we can provide autocomplete for requires
38
- @require_paths_tree = T.let(PrefixTree[IndexablePath].new, PrefixTree[IndexablePath])
78
+ @require_paths_tree = T.let(PrefixTree[URI::Generic].new, PrefixTree[URI::Generic])
39
79
 
40
80
  # Holds the linearized ancestors list for every namespace
41
81
  @ancestors = T.let({}, T::Hash[String, T::Array[String]])
@@ -47,6 +87,8 @@ module RubyIndexer
47
87
  )
48
88
 
49
89
  @configuration = T.let(RubyIndexer::Configuration.new, Configuration)
90
+
91
+ @initial_indexing_completed = T.let(false, T::Boolean)
50
92
  end
51
93
 
52
94
  # Register an included `hook` that will be executed when `module_name` is included into any namespace
@@ -55,11 +97,12 @@ module RubyIndexer
55
97
  (@included_hooks[module_name] ||= []) << hook
56
98
  end
57
99
 
58
- sig { params(indexable: IndexablePath).void }
59
- def delete(indexable)
100
+ sig { params(uri: URI::Generic, skip_require_paths_tree: T::Boolean).void }
101
+ def delete(uri, skip_require_paths_tree: false)
102
+ key = uri.to_s
60
103
  # For each constant discovered in `path`, delete the associated entry from the index. If there are no entries
61
104
  # left, delete the constant from the index.
62
- @files_to_entries[indexable.full_path]&.each do |entry|
105
+ @uris_to_entries[key]&.each do |entry|
63
106
  name = entry.name
64
107
  entries = @entries[name]
65
108
  next unless entries
@@ -77,9 +120,10 @@ module RubyIndexer
77
120
  end
78
121
  end
79
122
 
80
- @files_to_entries.delete(indexable.full_path)
123
+ @uris_to_entries.delete(key)
124
+ return if skip_require_paths_tree
81
125
 
82
- require_path = indexable.require_path
126
+ require_path = uri.require_path
83
127
  @require_paths_tree.delete(require_path) if require_path
84
128
  end
85
129
 
@@ -88,7 +132,7 @@ module RubyIndexer
88
132
  name = entry.name
89
133
 
90
134
  (@entries[name] ||= []) << entry
91
- (@files_to_entries[entry.file_path] ||= []) << entry
135
+ (@uris_to_entries[entry.uri.to_s] ||= []) << entry
92
136
  @entries_tree.insert(name, T.must(@entries[name])) unless skip_prefix_tree
93
137
  end
94
138
 
@@ -97,7 +141,7 @@ module RubyIndexer
97
141
  @entries[fully_qualified_name.delete_prefix("::")]
98
142
  end
99
143
 
100
- sig { params(query: String).returns(T::Array[IndexablePath]) }
144
+ sig { params(query: String).returns(T::Array[URI::Generic]) }
101
145
  def search_require_paths(query)
102
146
  @require_paths_tree.search(query)
103
147
  end
@@ -115,8 +159,16 @@ module RubyIndexer
115
159
  )]))
116
160
  end
117
161
  def first_unqualified_const(name)
162
+ # Look for an exact match first
118
163
  _name, entries = @entries.find do |const_name, _entries|
119
- const_name.end_with?(name)
164
+ const_name == name || const_name.end_with?("::#{name}")
165
+ end
166
+
167
+ # If an exact match is not found, then try to find a constant that ends with the name
168
+ unless entries
169
+ _name, entries = @entries.find do |const_name, _entries|
170
+ const_name.end_with?(name)
171
+ end
120
172
  end
121
173
 
122
174
  T.cast(
@@ -342,75 +394,70 @@ module RubyIndexer
342
394
  nil
343
395
  end
344
396
 
345
- # Index all files for the given indexable paths, which defaults to what is configured. A block can be used to track
346
- # and control indexing progress. That block is invoked with the current progress percentage and should return `true`
347
- # to continue indexing or `false` to stop indexing.
397
+ # Index all files for the given URIs, which defaults to what is configured. A block can be used to track and control
398
+ # indexing progress. That block is invoked with the current progress percentage and should return `true` to continue
399
+ # indexing or `false` to stop indexing.
348
400
  sig do
349
401
  params(
350
- indexable_paths: T::Array[IndexablePath],
402
+ uris: T::Array[URI::Generic],
351
403
  block: T.nilable(T.proc.params(progress: Integer).returns(T::Boolean)),
352
404
  ).void
353
405
  end
354
- def index_all(indexable_paths: @configuration.indexables, &block)
406
+ def index_all(uris: @configuration.indexable_uris, &block)
355
407
  # When troubleshooting an indexing issue, e.g. through irb, it's not obvious that `index_all` will augment the
356
408
  # existing index values, meaning it may contain 'stale' entries. This check ensures that the user is aware of this
357
409
  # behavior and can take appropriate action.
358
- # binding.break
359
- if @entries.any?
410
+ if @initial_indexing_completed
360
411
  raise IndexNotEmptyError,
361
412
  "The index is not empty. To prevent invalid entries, `index_all` can only be called once."
362
413
  end
363
414
 
415
+ @initial_indexing_completed = true
416
+
364
417
  RBSIndexer.new(self).index_ruby_core
365
418
  # Calculate how many paths are worth 1% of progress
366
- progress_step = (indexable_paths.length / 100.0).ceil
419
+ progress_step = (uris.length / 100.0).ceil
367
420
 
368
- indexable_paths.each_with_index do |path, index|
421
+ uris.each_with_index do |uri, index|
369
422
  if block && index % progress_step == 0
370
423
  progress = (index / progress_step) + 1
371
424
  break unless block.call(progress)
372
425
  end
373
426
 
374
- index_single(path, collect_comments: false)
427
+ index_file(uri, collect_comments: false)
375
428
  end
376
429
  end
377
430
 
378
- sig { params(indexable_path: IndexablePath, source: T.nilable(String), collect_comments: T::Boolean).void }
379
- def index_single(indexable_path, source = nil, collect_comments: true)
380
- content = source || File.read(indexable_path.full_path)
431
+ sig { params(uri: URI::Generic, source: String, collect_comments: T::Boolean).void }
432
+ def index_single(uri, source, collect_comments: true)
381
433
  dispatcher = Prism::Dispatcher.new
382
434
 
383
- result = Prism.parse(content)
384
- listener = DeclarationListener.new(
385
- self,
386
- dispatcher,
387
- result,
388
- indexable_path.full_path,
389
- collect_comments: collect_comments,
390
- )
435
+ result = Prism.parse(source)
436
+ listener = DeclarationListener.new(self, dispatcher, result, uri, collect_comments: collect_comments)
391
437
  dispatcher.dispatch(result.value)
392
438
 
393
- indexing_errors = listener.indexing_errors.uniq
394
-
395
- require_path = indexable_path.require_path
396
- @require_paths_tree.insert(require_path, indexable_path) if require_path
439
+ require_path = uri.require_path
440
+ @require_paths_tree.insert(require_path, uri) if require_path
397
441
 
398
- if indexing_errors.any?
399
- indexing_errors.each do |error|
400
- $stderr.puts error
401
- end
402
- end
403
- rescue Errno::EISDIR, Errno::ENOENT
404
- # If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore
405
- # it
442
+ indexing_errors = listener.indexing_errors.uniq
443
+ indexing_errors.each { |error| $stderr.puts(error) } if indexing_errors.any?
406
444
  rescue SystemStackError => e
407
445
  if e.backtrace&.first&.include?("prism")
408
- $stderr.puts "Prism error indexing #{indexable_path.full_path}: #{e.message}"
446
+ $stderr.puts "Prism error indexing #{uri}: #{e.message}"
409
447
  else
410
448
  raise
411
449
  end
412
450
  end
413
451
 
452
+ # Indexes a File URI by reading the contents from disk
453
+ sig { params(uri: URI::Generic, collect_comments: T::Boolean).void }
454
+ def index_file(uri, collect_comments: true)
455
+ index_single(uri, File.read(T.must(uri.full_path)), collect_comments: collect_comments)
456
+ rescue Errno::EISDIR, Errno::ENOENT
457
+ # If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore
458
+ # it
459
+ end
460
+
414
461
  # Follows aliases in a namespace. The algorithm keeps checking if the name is an alias and then recursively follows
415
462
  # it. The idea is that we test the name in parts starting from the complete name to the first namespace. For
416
463
  # `Foo::Bar::Baz`, we would test:
@@ -588,29 +635,81 @@ module RubyIndexer
588
635
  entries.select { |e| ancestors.include?(e.owner&.name) }
589
636
  end
590
637
 
638
+ sig { params(variable_name: String, owner_name: String).returns(T.nilable(T::Array[Entry::ClassVariable])) }
639
+ def resolve_class_variable(variable_name, owner_name)
640
+ entries = self[variable_name]&.grep(Entry::ClassVariable)
641
+ return unless entries&.any?
642
+
643
+ ancestors = linearized_attached_ancestors(owner_name)
644
+ return if ancestors.empty?
645
+
646
+ entries.select { |e| ancestors.include?(e.owner&.name) }
647
+ end
648
+
591
649
  # Returns a list of possible candidates for completion of instance variables for a given owner name. The name must
592
650
  # include the `@` prefix
593
- sig { params(name: String, owner_name: String).returns(T::Array[Entry::InstanceVariable]) }
651
+ sig do
652
+ params(name: String, owner_name: String).returns(T::Array[T.any(Entry::InstanceVariable, Entry::ClassVariable)])
653
+ end
594
654
  def instance_variable_completion_candidates(name, owner_name)
595
- entries = T.cast(prefix_search(name).flatten, T::Array[Entry::InstanceVariable])
655
+ entries = T.cast(prefix_search(name).flatten, T::Array[T.any(Entry::InstanceVariable, Entry::ClassVariable)])
656
+ # Avoid wasting time linearizing ancestors if we didn't find anything
657
+ return entries if entries.empty?
658
+
596
659
  ancestors = linearized_ancestors_of(owner_name)
597
660
 
598
- variables = entries.select { |e| ancestors.any?(e.owner&.name) }
661
+ instance_variables, class_variables = entries.partition { |e| e.is_a?(Entry::InstanceVariable) }
662
+ variables = instance_variables.select { |e| ancestors.any?(e.owner&.name) }
663
+
664
+ # Class variables are only owned by the attached class in our representation. If the owner is in a singleton
665
+ # context, we have to search for ancestors of the attached class
666
+ if class_variables.any?
667
+ name_parts = owner_name.split("::")
668
+
669
+ if name_parts.last&.start_with?("<Class:")
670
+ attached_name = T.must(name_parts[0..-2]).join("::")
671
+ attached_ancestors = linearized_ancestors_of(attached_name)
672
+ variables.concat(class_variables.select { |e| attached_ancestors.any?(e.owner&.name) })
673
+ else
674
+ variables.concat(class_variables.select { |e| ancestors.any?(e.owner&.name) })
675
+ end
676
+ end
677
+
599
678
  variables.uniq!(&:name)
600
679
  variables
601
680
  end
602
681
 
603
- # Synchronizes a change made to the given indexable path. This method will ensure that new declarations are indexed,
604
- # removed declarations removed and that the ancestor linearization cache is cleared if necessary
605
- sig { params(indexable: IndexablePath).void }
606
- def handle_change(indexable)
607
- original_entries = @files_to_entries[indexable.full_path]
682
+ sig { params(name: String, owner_name: String).returns(T::Array[Entry::ClassVariable]) }
683
+ def class_variable_completion_candidates(name, owner_name)
684
+ entries = T.cast(prefix_search(name).flatten, T::Array[Entry::ClassVariable])
685
+ # Avoid wasting time linearizing ancestors if we didn't find anything
686
+ return entries if entries.empty?
608
687
 
609
- delete(indexable)
610
- index_single(indexable)
688
+ ancestors = linearized_attached_ancestors(owner_name)
689
+ variables = entries.select { |e| ancestors.any?(e.owner&.name) }
690
+ variables.uniq!(&:name)
691
+ variables
692
+ end
611
693
 
612
- updated_entries = @files_to_entries[indexable.full_path]
694
+ # Synchronizes a change made to the given URI. This method will ensure that new declarations are indexed, removed
695
+ # declarations removed and that the ancestor linearization cache is cleared if necessary. If a block is passed, the
696
+ # consumer of this API has to handle deleting and inserting/updating entries in the index instead of passing the
697
+ # document's source (used to handle unsaved changes to files)
698
+ sig do
699
+ params(uri: URI::Generic, source: T.nilable(String), block: T.nilable(T.proc.params(index: Index).void)).void
700
+ end
701
+ def handle_change(uri, source = nil, &block)
702
+ key = uri.to_s
703
+ original_entries = @uris_to_entries[key]
613
704
 
705
+ if block
706
+ block.call(self)
707
+ else
708
+ delete(uri)
709
+ index_single(uri, T.must(source))
710
+ end
711
+
712
+ updated_entries = @uris_to_entries[key]
614
713
  return unless original_entries && updated_entries
615
714
 
616
715
  # A change in one ancestor may impact several different others, which could be including that ancestor through
@@ -661,7 +760,7 @@ module RubyIndexer
661
760
 
662
761
  singleton = Entry::SingletonClass.new(
663
762
  [full_singleton_name],
664
- attached_ancestor.file_path,
763
+ attached_ancestor.uri,
665
764
  attached_ancestor.location,
666
765
  attached_ancestor.name_location,
667
766
  nil,
@@ -675,12 +774,12 @@ module RubyIndexer
675
774
 
676
775
  sig do
677
776
  type_parameters(:T).params(
678
- path: String,
777
+ uri: String,
679
778
  type: T.nilable(T::Class[T.all(T.type_parameter(:T), Entry)]),
680
779
  ).returns(T.nilable(T.any(T::Array[Entry], T::Array[T.type_parameter(:T)])))
681
780
  end
682
- def entries_for(path, type = nil)
683
- entries = @files_to_entries[path]
781
+ def entries_for(uri, type = nil)
782
+ entries = @uris_to_entries[uri.to_s]
684
783
  return entries unless type
685
784
 
686
785
  entries&.grep(type)
@@ -688,6 +787,20 @@ module RubyIndexer
688
787
 
689
788
  private
690
789
 
790
+ # Always returns the linearized ancestors for the attached class, regardless of whether `name` refers to a singleton
791
+ # or attached namespace
792
+ sig { params(name: String).returns(T::Array[String]) }
793
+ def linearized_attached_ancestors(name)
794
+ name_parts = name.split("::")
795
+
796
+ if name_parts.last&.start_with?("<Class:")
797
+ attached_name = T.must(name_parts[0..-2]).join("::")
798
+ linearized_ancestors_of(attached_name)
799
+ else
800
+ linearized_ancestors_of(name)
801
+ end
802
+ end
803
+
691
804
  # Runs the registered included hooks
692
805
  sig { params(fully_qualified_name: String, nesting: T::Array[String]).void }
693
806
  def run_included_hooks(fully_qualified_name, nesting)
@@ -44,5 +44,17 @@ module RubyIndexer
44
44
  @start_column = start_column
45
45
  @end_column = end_column
46
46
  end
47
+
48
+ sig do
49
+ params(
50
+ other: T.any(Location, Prism::Location),
51
+ ).returns(T::Boolean)
52
+ end
53
+ def ==(other)
54
+ start_line == other.start_line &&
55
+ end_line == other.end_line &&
56
+ start_column == other.start_column &&
57
+ end_column == other.end_column
58
+ end
47
59
  end
48
60
  end