ruby-lsp 0.23.7 → 0.23.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46b72d1b9ba142496f64035b6b1ad54d096c0a28ae52311612bd118bc49e2956
4
- data.tar.gz: 525dc46dcfc71ee57da51955a4034ceace3bbdd1eca7942b542ab2cd32d42ad4
3
+ metadata.gz: 149e7978a6f85c9349fa3922283a151f2e2129d12eb14e29d665ad35a3b909e7
4
+ data.tar.gz: a931c7b049810fe304eaa93ae4a7a8bdd499951fa9f3ca21b6da9df91cc24e12
5
5
  SHA512:
6
- metadata.gz: '06690b76d284ca6abc05fc0e836af32a54c0a3cedea1fd53071648330a3b3b6e6d1a59ef205127f54a88e8cd3ca07c3840f25f1293e37737d705b2ea41719cb1'
7
- data.tar.gz: bd6104ef180f122d0bb9d6d751213c43f0d19d0f203deb50e5d94583ff0aaf85559a040ca0371fc24506ff11a0d2742c14f1277226cc34d3a09d02730d46aed7
6
+ metadata.gz: 6fae90263d8e037d3e58a0f20de4e6eaa671d739f28fa1af90a013a9d0ade3a43fa118b5fe15c3e2a3de025a9c3e4a53dc4584426678619896b98d38cca2dcbc
7
+ data.tar.gz: c634542b0ec26e9df418222b8eab459f42d3f65e42c196c6a3769139073c94f98bfb0da8cb61b18f055629aa9fbf58d6516d13a40b453f27fbc4a6f1514d3d9d
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.23.7
1
+ 0.23.9
@@ -51,7 +51,7 @@ end
51
51
 
52
52
  begin
53
53
  # Wait until the composed Bundle is finished
54
- Process.wait(pid)
54
+ _, status = Process.wait2(pid)
55
55
  rescue Errno::ECHILD
56
56
  # In theory, the child process can finish before we even get to the wait call, but that is not an error
57
57
  end
@@ -105,7 +105,7 @@ end
105
105
  # flow, we are not booting the LSP yet, just checking if the bundle is valid before rebooting
106
106
  if reboot
107
107
  # Use the exit status to signal to the server if composing the bundle succeeded
108
- exit(install_error || setup_error ? 1 : 0)
108
+ exit(install_error || setup_error ? 1 : status&.exitstatus || 0)
109
109
  end
110
110
 
111
111
  # Now that the bundle is set up, we can begin actually launching the server. Note that `Bundler.setup` will have already
@@ -91,7 +91,7 @@ module RubyIndexer
91
91
  def on_class_node_enter(node)
92
92
  constant_path = node.constant_path
93
93
  superclass = node.superclass
94
- nesting = actual_nesting(constant_path.slice)
94
+ nesting = Index.actual_nesting(@stack, constant_path.slice)
95
95
 
96
96
  parent_class = case superclass
97
97
  when Prism::ConstantReadNode, Prism::ConstantPathNode
@@ -144,7 +144,7 @@ module RubyIndexer
144
144
  if current_owner
145
145
  expression = node.expression
146
146
  name = (expression.is_a?(Prism::SelfNode) ? "<Class:#{last_name_in_stack}>" : "<Class:#{expression.slice}>")
147
- real_nesting = actual_nesting(name)
147
+ real_nesting = Index.actual_nesting(@stack, name)
148
148
 
149
149
  existing_entries = T.cast(@index[real_nesting.join("::")], T.nilable(T::Array[Entry::SingletonClass]))
150
150
 
@@ -516,7 +516,7 @@ module RubyIndexer
516
516
  name_loc = Location.from_prism_location(name_location, @code_units_cache)
517
517
 
518
518
  entry = Entry::Module.new(
519
- actual_nesting(name),
519
+ Index.actual_nesting(@stack, name),
520
520
  @uri,
521
521
  location,
522
522
  name_loc,
@@ -536,7 +536,7 @@ module RubyIndexer
536
536
  ).void
537
537
  end
538
538
  def add_class(name_or_nesting, full_location, name_location, parent_class_name: nil, comments: nil)
539
- nesting = name_or_nesting.is_a?(Array) ? name_or_nesting : actual_nesting(name_or_nesting)
539
+ nesting = name_or_nesting.is_a?(Array) ? name_or_nesting : Index.actual_nesting(@stack, name_or_nesting)
540
540
  entry = Entry::Class.new(
541
541
  nesting,
542
542
  @uri,
@@ -1104,20 +1104,6 @@ module RubyIndexer
1104
1104
  end
1105
1105
  end
1106
1106
 
1107
- sig { params(name: String).returns(T::Array[String]) }
1108
- def actual_nesting(name)
1109
- nesting = @stack + [name]
1110
- corrected_nesting = []
1111
-
1112
- nesting.reverse_each do |name|
1113
- corrected_nesting.prepend(name.delete_prefix("::"))
1114
-
1115
- break if name.start_with?("::")
1116
- end
1117
-
1118
- corrected_nesting
1119
- end
1120
-
1121
1107
  sig { params(short_name: String, entry: Entry::Namespace).void }
1122
1108
  def advance_namespace_stack(short_name, entry)
1123
1109
  @visibility_stack.push(VisibilityScope.public_scope)
@@ -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:
@@ -120,8 +159,16 @@ module RubyIndexer
120
159
  )]))
121
160
  end
122
161
  def first_unqualified_const(name)
162
+ # Look for an exact match first
123
163
  _name, entries = @entries.find do |const_name, _entries|
124
- 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
125
172
  end
126
173
 
127
174
  T.cast(
@@ -593,7 +640,7 @@ module RubyIndexer
593
640
  entries = self[variable_name]&.grep(Entry::ClassVariable)
594
641
  return unless entries&.any?
595
642
 
596
- ancestors = linearized_ancestors_of(owner_name)
643
+ ancestors = linearized_attached_ancestors(owner_name)
597
644
  return if ancestors.empty?
598
645
 
599
646
  entries.select { |e| ancestors.include?(e.owner&.name) }
@@ -601,12 +648,33 @@ module RubyIndexer
601
648
 
602
649
  # Returns a list of possible candidates for completion of instance variables for a given owner name. The name must
603
650
  # include the `@` prefix
604
- 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
605
654
  def instance_variable_completion_candidates(name, owner_name)
606
- 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
+
607
659
  ancestors = linearized_ancestors_of(owner_name)
608
660
 
609
- 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
+
610
678
  variables.uniq!(&:name)
611
679
  variables
612
680
  end
@@ -614,8 +682,10 @@ module RubyIndexer
614
682
  sig { params(name: String, owner_name: String).returns(T::Array[Entry::ClassVariable]) }
615
683
  def class_variable_completion_candidates(name, owner_name)
616
684
  entries = T.cast(prefix_search(name).flatten, T::Array[Entry::ClassVariable])
617
- ancestors = linearized_ancestors_of(owner_name)
685
+ # Avoid wasting time linearizing ancestors if we didn't find anything
686
+ return entries if entries.empty?
618
687
 
688
+ ancestors = linearized_attached_ancestors(owner_name)
619
689
  variables = entries.select { |e| ancestors.any?(e.owner&.name) }
620
690
  variables.uniq!(&:name)
621
691
  variables
@@ -717,6 +787,20 @@ module RubyIndexer
717
787
 
718
788
  private
719
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
+
720
804
  # Runs the registered included hooks
721
805
  sig { params(fully_qualified_name: String, nesting: T::Array[String]).void }
722
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
@@ -75,12 +75,14 @@ module RubyIndexer
75
75
  target: Target,
76
76
  index: RubyIndexer::Index,
77
77
  dispatcher: Prism::Dispatcher,
78
+ uri: URI::Generic,
78
79
  include_declarations: T::Boolean,
79
80
  ).void
80
81
  end
81
- def initialize(target, index, dispatcher, include_declarations: true)
82
+ def initialize(target, index, dispatcher, uri, include_declarations: true)
82
83
  @target = target
83
84
  @index = index
85
+ @uri = uri
84
86
  @include_declarations = include_declarations
85
87
  @stack = T.let([], T::Array[String])
86
88
  @references = T.let([], T::Array[Reference])
@@ -126,15 +128,7 @@ module RubyIndexer
126
128
 
127
129
  sig { params(node: Prism::ClassNode).void }
128
130
  def on_class_node_enter(node)
129
- constant_path = node.constant_path
130
- name = constant_path.slice
131
- nesting = actual_nesting(name)
132
-
133
- if @target.is_a?(ConstTarget) && nesting.join("::") == @target.fully_qualified_name
134
- @references << Reference.new(name, constant_path.location, declaration: true)
135
- end
136
-
137
- @stack << name
131
+ @stack << node.constant_path.slice
138
132
  end
139
133
 
140
134
  sig { params(node: Prism::ClassNode).void }
@@ -144,15 +138,7 @@ module RubyIndexer
144
138
 
145
139
  sig { params(node: Prism::ModuleNode).void }
146
140
  def on_module_node_enter(node)
147
- constant_path = node.constant_path
148
- name = constant_path.slice
149
- nesting = actual_nesting(name)
150
-
151
- if @target.is_a?(ConstTarget) && nesting.join("::") == @target.fully_qualified_name
152
- @references << Reference.new(name, constant_path.location, declaration: true)
153
- end
154
-
155
- @stack << name
141
+ @stack << node.constant_path.slice
156
142
  end
157
143
 
158
144
  sig { params(node: Prism::ModuleNode).void }
@@ -175,7 +161,7 @@ module RubyIndexer
175
161
 
176
162
  sig { params(node: Prism::ConstantPathNode).void }
177
163
  def on_constant_path_node_enter(node)
178
- name = constant_name(node)
164
+ name = Index.constant_name(node)
179
165
  return unless name
180
166
 
181
167
  collect_constant_references(name, node.location)
@@ -183,7 +169,7 @@ module RubyIndexer
183
169
 
184
170
  sig { params(node: Prism::ConstantReadNode).void }
185
171
  def on_constant_read_node_enter(node)
186
- name = constant_name(node)
172
+ name = Index.constant_name(node)
187
173
  return unless name
188
174
 
189
175
  collect_constant_references(name, node.location)
@@ -204,7 +190,7 @@ module RubyIndexer
204
190
  target = node.target
205
191
  return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
206
192
 
207
- name = constant_name(target)
193
+ name = Index.constant_name(target)
208
194
  return unless name
209
195
 
210
196
  collect_constant_references(name, target.location)
@@ -215,7 +201,7 @@ module RubyIndexer
215
201
  target = node.target
216
202
  return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
217
203
 
218
- name = constant_name(target)
204
+ name = Index.constant_name(target)
219
205
  return unless name
220
206
 
221
207
  collect_constant_references(name, target.location)
@@ -226,7 +212,7 @@ module RubyIndexer
226
212
  target = node.target
227
213
  return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
228
214
 
229
- name = constant_name(target)
215
+ name = Index.constant_name(target)
230
216
  return unless name
231
217
 
232
218
  collect_constant_references(name, target.location)
@@ -237,7 +223,7 @@ module RubyIndexer
237
223
  target = node.target
238
224
  return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)
239
225
 
240
- name = constant_name(target)
226
+ name = Index.constant_name(target)
241
227
  return unless name
242
228
 
243
229
  collect_constant_references(name, target.location)
@@ -320,20 +306,6 @@ module RubyIndexer
320
306
 
321
307
  private
322
308
 
323
- sig { params(name: String).returns(T::Array[String]) }
324
- def actual_nesting(name)
325
- nesting = @stack + [name]
326
- corrected_nesting = []
327
-
328
- nesting.reverse_each do |name|
329
- corrected_nesting.prepend(name.delete_prefix("::"))
330
-
331
- break if name.start_with?("::")
332
- end
333
-
334
- corrected_nesting
335
- end
336
-
337
309
  sig { params(name: String, location: Prism::Location).void }
338
310
  def collect_constant_references(name, location)
339
311
  return unless @target.is_a?(ConstTarget)
@@ -341,17 +313,26 @@ module RubyIndexer
341
313
  entries = @index.resolve(name, @stack)
342
314
  return unless entries
343
315
 
344
- previous_reference = @references.last
345
-
346
- entries.each do |entry|
347
- next unless entry.name == @target.fully_qualified_name
316
+ # Filter down to all constant declarations that match the expected name and type
317
+ matching_entries = entries.select do |e|
318
+ [
319
+ Entry::Namespace,
320
+ Entry::Constant,
321
+ Entry::ConstantAlias,
322
+ Entry::UnresolvedConstantAlias,
323
+ ].any? { |klass| e.is_a?(klass) } &&
324
+ e.name == @target.fully_qualified_name
325
+ end
348
326
 
349
- # When processing a class/module declaration, we eagerly handle the constant reference. To avoid duplicates,
350
- # when we find the constant node defining the namespace, then we have to check if it wasn't already added
351
- next if previous_reference&.location == location
327
+ return if matching_entries.empty?
352
328
 
353
- @references << Reference.new(name, location, declaration: false)
329
+ # If any of the matching entries have the same location as the constant and were
330
+ # defined in the same file, then it is that constant's declaration
331
+ declaration = matching_entries.any? do |e|
332
+ e.uri == @uri && e.name_location == location
354
333
  end
334
+
335
+ @references << Reference.new(name, location, declaration: declaration)
355
336
  end
356
337
 
357
338
  sig { params(name: String, location: Prism::Location, declaration: T::Boolean).void }
@@ -360,21 +341,5 @@ module RubyIndexer
360
341
 
361
342
  @references << Reference.new(name, location, declaration: declaration)
362
343
  end
363
-
364
- sig do
365
- params(
366
- node: T.any(
367
- Prism::ConstantPathNode,
368
- Prism::ConstantReadNode,
369
- Prism::ConstantPathTargetNode,
370
- ),
371
- ).returns(T.nilable(String))
372
- end
373
- def constant_name(node)
374
- node.full_name
375
- rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
376
- Prism::ConstantPathNode::MissingNodesInConstantPathError
377
- nil
378
- end
379
344
  end
380
345
  end
@@ -1561,6 +1561,23 @@ module RubyIndexer
1561
1561
  assert_equal("Foo::Bar", entry.name)
1562
1562
  end
1563
1563
 
1564
+ def test_first_unqualified_const_prefers_exact_matches
1565
+ index(<<~RUBY)
1566
+ module Foo
1567
+ class ParseResultType
1568
+ end
1569
+ end
1570
+
1571
+ module Namespace
1572
+ class Type
1573
+ end
1574
+ end
1575
+ RUBY
1576
+
1577
+ entry = T.must(@index.first_unqualified_const("Type")&.first)
1578
+ assert_equal("Namespace::Type", entry.name)
1579
+ end
1580
+
1564
1581
  def test_completion_does_not_duplicate_overridden_methods
1565
1582
  index(<<~RUBY)
1566
1583
  class Foo
@@ -2092,5 +2109,57 @@ module RubyIndexer
2092
2109
  refute_nil(entry, "Expected indexer to be able to handle unsaved URIs")
2093
2110
  assert_equal("I added this comment!", entry.comments)
2094
2111
  end
2112
+
2113
+ def test_instance_variable_completion_returns_class_variables_too
2114
+ index(<<~RUBY)
2115
+ class Parent
2116
+ @@abc = 123
2117
+ end
2118
+
2119
+ class Child < Parent
2120
+ @@adf = 123
2121
+
2122
+ def self.do
2123
+ end
2124
+ end
2125
+ RUBY
2126
+
2127
+ abc, adf = @index.instance_variable_completion_candidates("@", "Child::<Class:Child>")
2128
+
2129
+ refute_nil(abc)
2130
+ refute_nil(adf)
2131
+
2132
+ assert_equal("@@abc", abc.name)
2133
+ assert_equal("@@adf", adf.name)
2134
+ end
2135
+
2136
+ def test_class_variable_completion_from_singleton_context
2137
+ index(<<~RUBY)
2138
+ class Foo
2139
+ @@hello = 123
2140
+
2141
+ def self.do
2142
+ end
2143
+ end
2144
+ RUBY
2145
+
2146
+ candidates = @index.class_variable_completion_candidates("@@", "Foo::<Class:Foo>")
2147
+ refute_empty(candidates)
2148
+
2149
+ assert_equal("@@hello", candidates.first&.name)
2150
+ end
2151
+
2152
+ def test_resolve_class_variable_in_singleton_context
2153
+ index(<<~RUBY)
2154
+ class Foo
2155
+ @@hello = 123
2156
+ end
2157
+ RUBY
2158
+
2159
+ candidates = @index.resolve_class_variable("@@hello", "Foo::<Class:Foo>")
2160
+ refute_empty(candidates)
2161
+
2162
+ assert_equal("@@hello", candidates.first&.name)
2163
+ end
2095
2164
  end
2096
2165
  end
@@ -274,6 +274,30 @@ module RubyIndexer
274
274
  assert_equal(8, refs[1].location.start_line)
275
275
  end
276
276
 
277
+ def test_accounts_for_reopened_classes
278
+ refs = find_const_references("Foo", <<~RUBY)
279
+ class Foo
280
+ end
281
+ class Foo
282
+ class Bar
283
+ end
284
+ end
285
+
286
+ Foo.new
287
+ RUBY
288
+
289
+ assert_equal(3, refs.size)
290
+
291
+ assert_equal("Foo", refs[0].name)
292
+ assert_equal(1, refs[0].location.start_line)
293
+
294
+ assert_equal("Foo", refs[1].name)
295
+ assert_equal(3, refs[1].location.start_line)
296
+
297
+ assert_equal("Foo", refs[2].name)
298
+ assert_equal(8, refs[2].location.start_line)
299
+ end
300
+
277
301
  private
278
302
 
279
303
  def find_const_references(const_name, source)
@@ -293,11 +317,12 @@ module RubyIndexer
293
317
 
294
318
  def find_references(target, source)
295
319
  file_path = "/fake.rb"
320
+ uri = URI::Generic.from_path(path: file_path)
296
321
  index = Index.new
297
- index.index_single(URI::Generic.from_path(path: file_path), source)
322
+ index.index_single(uri, source)
298
323
  parse_result = Prism.parse(source)
299
324
  dispatcher = Prism::Dispatcher.new
300
- finder = ReferenceFinder.new(target, index, dispatcher)
325
+ finder = ReferenceFinder.new(target, index, dispatcher, uri)
301
326
  dispatcher.visit(parse_result.value)
302
327
  finder.references
303
328
  end
@@ -22,6 +22,8 @@ require "prism/visitor"
22
22
  require "language_server-protocol"
23
23
  require "rbs"
24
24
  require "fileutils"
25
+ require "open3"
26
+ require "securerandom"
25
27
 
26
28
  require "ruby-lsp"
27
29
  require "ruby_lsp/base_server"
@@ -286,7 +286,7 @@ module RubyLsp
286
286
  when Prism::StringNode
287
287
  first_argument.content
288
288
  when Prism::ConstantReadNode, Prism::ConstantPathNode
289
- constant_name(first_argument)
289
+ RubyIndexer::Index.constant_name(first_argument)
290
290
  end
291
291
 
292
292
  return unless name
@@ -113,7 +113,7 @@ module RubyLsp
113
113
  # no sigil, Sorbet will still provide completion for constants
114
114
  return if @sorbet_level != RubyDocument::SorbetLevel::Ignore
115
115
 
116
- name = constant_name(node)
116
+ name = RubyIndexer::Index.constant_name(node)
117
117
  return if name.nil?
118
118
 
119
119
  range = range_from_location(node.location)
@@ -162,7 +162,7 @@ module RubyLsp
162
162
  if (receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)) &&
163
163
  node.call_operator == "::"
164
164
 
165
- name = constant_name(receiver)
165
+ name = RubyIndexer::Index.constant_name(receiver)
166
166
 
167
167
  if name
168
168
  start_loc = node.location
@@ -118,7 +118,7 @@ module RubyLsp
118
118
 
119
119
  sig { params(node: Prism::ConstantPathNode).void }
120
120
  def on_constant_path_node_enter(node)
121
- name = constant_name(node)
121
+ name = RubyIndexer::Index.constant_name(node)
122
122
  return if name.nil?
123
123
 
124
124
  find_in_index(name)
@@ -126,7 +126,7 @@ module RubyLsp
126
126
 
127
127
  sig { params(node: Prism::ConstantReadNode).void }
128
128
  def on_constant_read_node_enter(node)
129
- name = constant_name(node)
129
+ name = RubyIndexer::Index.constant_name(node)
130
130
  return if name.nil?
131
131
 
132
132
  find_in_index(name)
@@ -124,7 +124,16 @@ module RubyLsp
124
124
  match = comment.location.slice.match(%r{source://.*#\d+$})
125
125
  return unless match
126
126
 
127
- uri = T.cast(URI(T.must(match[0])), URI::Source)
127
+ uri = T.cast(
128
+ begin
129
+ URI(T.must(match[0]))
130
+ rescue URI::Error
131
+ nil
132
+ end,
133
+ T.nilable(URI::Source),
134
+ )
135
+ return unless uri
136
+
128
137
  gem_version = resolve_version(uri)
129
138
  return if gem_version.nil?
130
139
 
@@ -114,7 +114,7 @@ module RubyLsp
114
114
  def on_constant_read_node_enter(node)
115
115
  return if @sorbet_level != RubyDocument::SorbetLevel::Ignore
116
116
 
117
- name = constant_name(node)
117
+ name = RubyIndexer::Index.constant_name(node)
118
118
  return if name.nil?
119
119
 
120
120
  generate_hover(name, node.location)
@@ -131,7 +131,7 @@ module RubyLsp
131
131
  def on_constant_path_node_enter(node)
132
132
  return if @sorbet_level != RubyDocument::SorbetLevel::Ignore
133
133
 
134
- name = constant_name(node)
134
+ name = RubyIndexer::Index.constant_name(node)
135
135
  return if name.nil?
136
136
 
137
137
  generate_hover(name, node.location)
@@ -42,6 +42,10 @@ module RubyLsp
42
42
  refactor_method
43
43
  when CodeActions::TOGGLE_BLOCK_STYLE_TITLE
44
44
  switch_block_style
45
+ when CodeActions::CREATE_ATTRIBUTE_READER,
46
+ CodeActions::CREATE_ATTRIBUTE_WRITER,
47
+ CodeActions::CREATE_ATTRIBUTE_ACCESSOR
48
+ create_attribute_accessor
45
49
  else
46
50
  Error::UnknownCodeAction
47
51
  end
@@ -325,6 +329,90 @@ module RubyLsp
325
329
 
326
330
  indentation ? body_content.gsub(";", "\n") : "#{body_content.gsub("\n", ";")} "
327
331
  end
332
+
333
+ sig { returns(T.any(Interface::CodeAction, Error)) }
334
+ def create_attribute_accessor
335
+ source_range = @code_action.dig(:data, :range)
336
+
337
+ node = if source_range[:start] != source_range[:end]
338
+ @document.locate_first_within_range(
339
+ @code_action.dig(:data, :range),
340
+ node_types: CodeActions::INSTANCE_VARIABLE_NODES,
341
+ )
342
+ end
343
+
344
+ if node.nil?
345
+ node_context = @document.locate_node(
346
+ source_range[:start],
347
+ node_types: CodeActions::INSTANCE_VARIABLE_NODES,
348
+ )
349
+ node = node_context.node
350
+
351
+ return Error::EmptySelection unless CodeActions::INSTANCE_VARIABLE_NODES.include?(node.class)
352
+ end
353
+
354
+ node = T.cast(
355
+ node,
356
+ T.any(
357
+ Prism::InstanceVariableAndWriteNode,
358
+ Prism::InstanceVariableOperatorWriteNode,
359
+ Prism::InstanceVariableOrWriteNode,
360
+ Prism::InstanceVariableReadNode,
361
+ Prism::InstanceVariableTargetNode,
362
+ Prism::InstanceVariableWriteNode,
363
+ ),
364
+ )
365
+
366
+ node_context = @document.locate_node(
367
+ {
368
+ line: node.location.start_line,
369
+ character: node.location.start_character_column,
370
+ },
371
+ node_types: [
372
+ Prism::ClassNode,
373
+ Prism::ModuleNode,
374
+ Prism::SingletonClassNode,
375
+ ],
376
+ )
377
+ closest_node = node_context.node
378
+ return Error::InvalidTargetRange if closest_node.nil?
379
+
380
+ attribute_name = node.name[1..]
381
+ indentation = " " * (closest_node.location.start_column + 2)
382
+ attribute_accessor_source = T.must(
383
+ case @code_action[:title]
384
+ when CodeActions::CREATE_ATTRIBUTE_READER
385
+ "#{indentation}attr_reader :#{attribute_name}\n\n"
386
+ when CodeActions::CREATE_ATTRIBUTE_WRITER
387
+ "#{indentation}attr_writer :#{attribute_name}\n\n"
388
+ when CodeActions::CREATE_ATTRIBUTE_ACCESSOR
389
+ "#{indentation}attr_accessor :#{attribute_name}\n\n"
390
+ end,
391
+ )
392
+
393
+ target_start_line = closest_node.location.start_line
394
+ target_range = {
395
+ start: { line: target_start_line, character: 0 },
396
+ end: { line: target_start_line, character: 0 },
397
+ }
398
+
399
+ Interface::CodeAction.new(
400
+ title: @code_action[:title],
401
+ edit: Interface::WorkspaceEdit.new(
402
+ document_changes: [
403
+ Interface::TextDocumentEdit.new(
404
+ text_document: Interface::OptionalVersionedTextDocumentIdentifier.new(
405
+ uri: @code_action.dig(:data, :uri),
406
+ version: nil,
407
+ ),
408
+ edits: [
409
+ create_text_edit(target_range, attribute_accessor_source),
410
+ ],
411
+ ),
412
+ ],
413
+ ),
414
+ )
415
+ end
328
416
  end
329
417
  end
330
418
  end
@@ -12,6 +12,21 @@ module RubyLsp
12
12
  EXTRACT_TO_VARIABLE_TITLE = "Refactor: Extract Variable"
13
13
  EXTRACT_TO_METHOD_TITLE = "Refactor: Extract Method"
14
14
  TOGGLE_BLOCK_STYLE_TITLE = "Refactor: Toggle block style"
15
+ CREATE_ATTRIBUTE_READER = "Create Attribute Reader"
16
+ CREATE_ATTRIBUTE_WRITER = "Create Attribute Writer"
17
+ CREATE_ATTRIBUTE_ACCESSOR = "Create Attribute Accessor"
18
+
19
+ INSTANCE_VARIABLE_NODES = T.let(
20
+ [
21
+ Prism::InstanceVariableAndWriteNode,
22
+ Prism::InstanceVariableOperatorWriteNode,
23
+ Prism::InstanceVariableOrWriteNode,
24
+ Prism::InstanceVariableReadNode,
25
+ Prism::InstanceVariableTargetNode,
26
+ Prism::InstanceVariableWriteNode,
27
+ ],
28
+ T::Array[T.class_of(Prism::Node)],
29
+ )
15
30
 
16
31
  class << self
17
32
  extend T::Sig
@@ -66,9 +81,50 @@ module RubyLsp
66
81
  data: { range: @range, uri: @uri.to_s },
67
82
  )
68
83
  end
84
+ code_actions.concat(attribute_actions)
69
85
 
70
86
  code_actions
71
87
  end
88
+
89
+ private
90
+
91
+ sig { returns(T::Array[Interface::CodeAction]) }
92
+ def attribute_actions
93
+ return [] unless @document.is_a?(RubyDocument)
94
+
95
+ node = if @range.dig(:start) != @range.dig(:end)
96
+ @document.locate_first_within_range(
97
+ @range,
98
+ node_types: INSTANCE_VARIABLE_NODES,
99
+ )
100
+ end
101
+
102
+ if node.nil?
103
+ node_context = @document.locate_node(
104
+ @range[:start],
105
+ node_types: CodeActions::INSTANCE_VARIABLE_NODES,
106
+ )
107
+ return [] unless INSTANCE_VARIABLE_NODES.include?(node_context.node.class)
108
+ end
109
+
110
+ [
111
+ Interface::CodeAction.new(
112
+ title: CREATE_ATTRIBUTE_READER,
113
+ kind: Constant::CodeActionKind::EMPTY,
114
+ data: { range: @range, uri: @uri.to_s },
115
+ ),
116
+ Interface::CodeAction.new(
117
+ title: CREATE_ATTRIBUTE_WRITER,
118
+ kind: Constant::CodeActionKind::EMPTY,
119
+ data: { range: @range, uri: @uri.to_s },
120
+ ),
121
+ Interface::CodeAction.new(
122
+ title: CREATE_ATTRIBUTE_ACCESSOR,
123
+ kind: Constant::CodeActionKind::EMPTY,
124
+ data: { range: @range, uri: @uri.to_s },
125
+ ),
126
+ ]
127
+ end
72
128
  end
73
129
  end
74
130
  end
@@ -170,7 +170,7 @@ module RubyLsp
170
170
 
171
171
  sig { params(line: Integer, character: Integer).void }
172
172
  def move_cursor_to(line, character)
173
- return unless @client_name.start_with?("Visual Studio Code")
173
+ return unless /Visual Studio Code|Cursor/.match?(@client_name)
174
174
 
175
175
  position = Interface::Position.new(
176
176
  line: line,
@@ -124,7 +124,7 @@ module RubyLsp
124
124
  def create_reference_target(target_node, node_context)
125
125
  case target_node
126
126
  when Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::ConstantPathTargetNode
127
- name = constant_name(target_node)
127
+ name = RubyIndexer::Index.constant_name(target_node)
128
128
  return unless name
129
129
 
130
130
  entries = @global_state.index.resolve(name, node_context.nesting)
@@ -158,6 +158,7 @@ module RubyLsp
158
158
  target,
159
159
  @global_state.index,
160
160
  dispatcher,
161
+ uri,
161
162
  include_declarations: @params.dig(:context, :includeDeclaration) || true,
162
163
  )
163
164
  dispatcher.visit(parse_result.value)
@@ -65,7 +65,7 @@ module RubyLsp
65
65
  T.any(Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::ConstantPathTargetNode),
66
66
  )
67
67
 
68
- name = constant_name(target)
68
+ name = RubyIndexer::Index.constant_name(target)
69
69
  return unless name
70
70
 
71
71
  entries = @global_state.index.resolve(name, node_context.nesting)
@@ -179,7 +179,7 @@ module RubyLsp
179
179
  end
180
180
  def collect_changes(target, parse_result, name, uri)
181
181
  dispatcher = Prism::Dispatcher.new
182
- finder = RubyIndexer::ReferenceFinder.new(target, @global_state.index, dispatcher)
182
+ finder = RubyIndexer::ReferenceFinder.new(target, @global_state.index, dispatcher, uri)
183
183
  dispatcher.visit(parse_result.value)
184
184
 
185
185
  finder.references.map do |reference|
@@ -145,10 +145,7 @@ module RubyLsp
145
145
  ).returns(T.nilable(String))
146
146
  end
147
147
  def constant_name(node)
148
- node.full_name
149
- rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
150
- Prism::ConstantPathNode::MissingNodesInConstantPathError
151
- nil
148
+ RubyIndexer::Index.constant_name(node)
152
149
  end
153
150
 
154
151
  sig { params(node: T.any(Prism::ModuleNode, Prism::ClassNode)).returns(T.nilable(String)) }
@@ -258,11 +258,18 @@ module RubyLsp
258
258
  end
259
259
 
260
260
  sig { returns(T::Boolean) }
261
- def last_edit_may_change_declarations?
261
+ def should_index?
262
262
  # This method controls when we should index documents. If there's no recent edit and the document has just been
263
263
  # opened, we need to index it
264
264
  return true unless @last_edit
265
265
 
266
+ last_edit_may_change_declarations?
267
+ end
268
+
269
+ private
270
+
271
+ sig { returns(T::Boolean) }
272
+ def last_edit_may_change_declarations?
266
273
  case @last_edit
267
274
  when Delete
268
275
  # Not optimized yet. It's not trivial to identify that a declaration has been removed since the source is no
@@ -275,8 +282,6 @@ module RubyLsp
275
282
  end
276
283
  end
277
284
 
278
- private
279
-
280
285
  sig { params(position: T::Hash[Symbol, Integer]).returns(T::Boolean) }
281
286
  def position_may_impact_declarations?(position)
282
287
  node_context = locate_node(position)
@@ -13,7 +13,7 @@ module RubyLsp
13
13
  def process_message(message)
14
14
  case message[:method]
15
15
  when "initialize"
16
- send_log_message("Initializing Ruby LSP v#{VERSION}...")
16
+ send_log_message("Initializing Ruby LSP v#{VERSION} https://github.com/Shopify/ruby-lsp/releases/tag/v#{VERSION}....")
17
17
  run_initialize(message)
18
18
  when "initialized"
19
19
  send_log_message("Finished initializing Ruby LSP!") unless @test_mode
@@ -301,10 +301,19 @@ module RubyLsp
301
301
 
302
302
  # Not every client supports dynamic registration or file watching
303
303
  if @global_state.client_capabilities.supports_watching_files
304
- send_message(Request.register_watched_files(@current_request_id, "**/*.rb"))
305
304
  send_message(Request.register_watched_files(
306
305
  @current_request_id,
307
- Interface::RelativePattern.new(base_uri: @global_state.workspace_uri.to_s, pattern: ".rubocop.yml"),
306
+ "**/*.rb",
307
+ registration_id: "workspace-watcher",
308
+ ))
309
+
310
+ send_message(Request.register_watched_files(
311
+ @current_request_id,
312
+ Interface::RelativePattern.new(
313
+ base_uri: @global_state.workspace_uri.to_s,
314
+ pattern: "{.rubocop.yml,.rubocop}",
315
+ ),
316
+ registration_id: "rubocop-watcher",
308
317
  ))
309
318
  end
310
319
 
@@ -473,11 +482,11 @@ module RubyLsp
473
482
  code_lens = Requests::CodeLens.new(@global_state, uri, dispatcher)
474
483
  inlay_hint = Requests::InlayHints.new(document, T.must(@store.features_configuration.dig(:inlayHint)), dispatcher)
475
484
 
476
- if document.is_a?(RubyDocument) && document.last_edit_may_change_declarations?
485
+ if document.is_a?(RubyDocument) && document.should_index?
477
486
  # Re-index the file as it is modified. This mode of indexing updates entries only. Require path trees are only
478
487
  # updated on save
479
488
  @global_state.synchronize do
480
- send_log_message("Detected that last edit may have modified declarations. Re-indexing #{uri}")
489
+ send_log_message("Determined that document should be indexed: #{uri}")
481
490
 
482
491
  @global_state.index.handle_change(uri) do |index|
483
492
  index.delete(uri, skip_require_paths_tree: true)
@@ -999,6 +1008,11 @@ module RubyLsp
999
1008
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
1000
1009
  def workspace_did_change_watched_files(message)
1001
1010
  changes = message.dig(:params, :changes)
1011
+ # We allow add-ons to register for watching files and we have no restrictions for what they register for. If the
1012
+ # same pattern is registered more than once, the LSP will receive duplicate change notifications. Receiving them
1013
+ # is fine, but we shouldn't process the same file changes more than once
1014
+ changes.uniq!
1015
+
1002
1016
  index = @global_state.index
1003
1017
  changes.each do |change|
1004
1018
  # File change events include folders, but we're only interested in files
@@ -1036,6 +1050,10 @@ module RubyLsp
1036
1050
  when Constant::FileChangeType::DELETED
1037
1051
  index.delete(uri)
1038
1052
  end
1053
+ rescue Errno::ENOENT
1054
+ # If a file is created and then delete immediately afterwards, we will process the created notification before we
1055
+ # receive the deleted one, but the file no longer exists. This may happen when running a test suite that creates
1056
+ # and deletes files automatically.
1039
1057
  end
1040
1058
 
1041
1059
  sig { params(uri: URI::Generic).void }
@@ -1127,7 +1145,12 @@ module RubyLsp
1127
1145
 
1128
1146
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
1129
1147
  def workspace_dependencies(message)
1130
- response = if @global_state.top_level_bundle
1148
+ unless @global_state.top_level_bundle
1149
+ send_message(Result.new(id: message[:id], response: []))
1150
+ return
1151
+ end
1152
+
1153
+ response = begin
1131
1154
  Bundler.with_original_env do
1132
1155
  definition = Bundler.definition
1133
1156
  dep_keys = definition.locked_deps.keys.to_set
@@ -1141,7 +1164,7 @@ module RubyLsp
1141
1164
  }
1142
1165
  end
1143
1166
  end
1144
- else
1167
+ rescue Bundler::GemNotFound
1145
1168
  []
1146
1169
  end
1147
1170
 
@@ -1286,25 +1309,40 @@ module RubyLsp
1286
1309
  addon.handle_window_show_message_response(result[:title])
1287
1310
  end
1288
1311
 
1289
- sig { params(message: T::Hash[Symbol, T.untyped]).void }
1312
+ # NOTE: all servers methods are void because they can produce several messages for the client. The only reason this
1313
+ # method returns the created thread is to that we can join it in tests and avoid flakiness. The implementation is
1314
+ # not supposed to rely on the return of this method
1315
+ sig { params(message: T::Hash[Symbol, T.untyped]).returns(T.nilable(Thread)) }
1290
1316
  def compose_bundle(message)
1291
1317
  already_composed_path = File.join(@global_state.workspace_path, ".ruby-lsp", "bundle_is_composed")
1292
- command = "#{Gem.ruby} #{File.expand_path("../../exe/ruby-lsp-launcher", __dir__)} #{@global_state.workspace_uri}"
1293
1318
  id = message[:id]
1294
1319
 
1295
1320
  begin
1296
- Bundler::LockfileParser.new(Bundler.default_lockfile.read)
1321
+ Bundler.with_original_env do
1322
+ Bundler::LockfileParser.new(Bundler.default_lockfile.read)
1323
+ end
1297
1324
  rescue Bundler::LockfileError => e
1298
1325
  send_message(Error.new(id: id, code: BUNDLE_COMPOSE_FAILED_CODE, message: e.message))
1299
1326
  return
1327
+ rescue Bundler::GemfileNotFound, Errno::ENOENT
1328
+ # We still compose the bundle if there's no Gemfile or if the lockfile got deleted
1300
1329
  end
1301
1330
 
1302
1331
  # We compose the bundle in a thread so that the LSP continues to work while we're checking for its validity. Once
1303
1332
  # we return the response back to the editor, then the restart is triggered
1304
1333
  Thread.new do
1305
1334
  send_log_message("Recomposing the bundle ahead of restart")
1306
- pid = Process.spawn(command)
1307
- _, status = Process.wait2(pid)
1335
+
1336
+ _stdout, stderr, status = Bundler.with_unbundled_env do
1337
+ Open3.capture3(
1338
+ Gem.ruby,
1339
+ "-I",
1340
+ File.dirname(T.must(__dir__)),
1341
+ File.expand_path("../../exe/ruby-lsp-launcher", __dir__),
1342
+ @global_state.workspace_uri.to_s,
1343
+ chdir: @global_state.workspace_path,
1344
+ )
1345
+ end
1308
1346
 
1309
1347
  if status&.exitstatus == 0
1310
1348
  # Create a signal for the restart that it can skip composing the bundle and launch directly
@@ -1313,7 +1351,9 @@ module RubyLsp
1313
1351
  else
1314
1352
  # This special error code makes the extension avoid restarting in case we already know that the composed
1315
1353
  # bundle is not valid
1316
- send_message(Error.new(id: id, code: BUNDLE_COMPOSE_FAILED_CODE, message: "Failed to compose bundle"))
1354
+ send_message(
1355
+ Error.new(id: id, code: BUNDLE_COMPOSE_FAILED_CODE, message: "Failed to compose bundle\n#{stderr}"),
1356
+ )
1317
1357
  end
1318
1358
  end
1319
1359
  end
@@ -241,8 +241,7 @@ module RubyLsp
241
241
 
242
242
  # If either the Gemfile or the lockfile have been modified during the process of setting up the bundle, retry
243
243
  # composing the bundle from scratch
244
-
245
- if @gemfile && @lockfile
244
+ if @gemfile&.exist? && @lockfile&.exist?
246
245
  current_gemfile_hash = Digest::SHA256.hexdigest(@gemfile.read)
247
246
  current_lockfile_hash = Digest::SHA256.hexdigest(@lockfile.read)
248
247
 
@@ -80,7 +80,7 @@ module RubyLsp
80
80
  # When the receiver is a constant reference, we have to try to resolve it to figure out the right
81
81
  # receiver. But since the invocation is directly on the constant, that's the singleton context of that
82
82
  # class/module
83
- receiver_name = constant_name(receiver)
83
+ receiver_name = RubyIndexer::Index.constant_name(receiver)
84
84
  return unless receiver_name
85
85
 
86
86
  resolved_receiver = @index.resolve(receiver_name, node_context.nesting)
@@ -147,21 +147,6 @@ module RubyLsp
147
147
  Type.new("#{parts.join("::")}::<Class:#{parts.last}>")
148
148
  end
149
149
 
150
- sig do
151
- params(
152
- node: T.any(
153
- Prism::ConstantPathNode,
154
- Prism::ConstantReadNode,
155
- ),
156
- ).returns(T.nilable(String))
157
- end
158
- def constant_name(node)
159
- node.full_name
160
- rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
161
- Prism::ConstantPathNode::MissingNodesInConstantPathError
162
- nil
163
- end
164
-
165
150
  sig { params(node_context: NodeContext).returns(T.nilable(Type)) }
166
151
  def infer_receiver_for_class_variables(node_context)
167
152
  nesting_parts = node_context.nesting.dup
@@ -176,11 +176,19 @@ module RubyLsp
176
176
  class << self
177
177
  extend T::Sig
178
178
 
179
- sig { params(id: Integer, pattern: T.any(Interface::RelativePattern, String), kind: Integer).returns(Request) }
179
+ sig do
180
+ params(
181
+ id: Integer,
182
+ pattern: T.any(Interface::RelativePattern, String),
183
+ kind: Integer,
184
+ registration_id: T.nilable(String),
185
+ ).returns(Request)
186
+ end
180
187
  def register_watched_files(
181
188
  id,
182
189
  pattern,
183
- kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE
190
+ kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE,
191
+ registration_id: nil
184
192
  )
185
193
  new(
186
194
  id: id,
@@ -188,7 +196,7 @@ module RubyLsp
188
196
  params: Interface::RegistrationParams.new(
189
197
  registrations: [
190
198
  Interface::Registration.new(
191
- id: "workspace/didChangeWatchedFiles",
199
+ id: registration_id || SecureRandom.uuid,
192
200
  method: "workspace/didChangeWatchedFiles",
193
201
  register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
194
202
  watchers: [
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.23.7
4
+ version: 0.23.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-01-28 00:00:00.000000000 Z
10
+ date: 2025-02-06 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: language_server-protocol