ruby-lsp 0.23.8 → 0.23.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 918079e906832b0289a053cd4bfe849422082c25bd0f4bdada25cab3a532005f
4
- data.tar.gz: 7935b2bdc52a2bb83264376fb98f7ef8d3bfc3c5cb9dadd23642a1ebafbca013
3
+ metadata.gz: 149e7978a6f85c9349fa3922283a151f2e2129d12eb14e29d665ad35a3b909e7
4
+ data.tar.gz: a931c7b049810fe304eaa93ae4a7a8bdd499951fa9f3ca21b6da9df91cc24e12
5
5
  SHA512:
6
- metadata.gz: 9fb084d322308bb2e69c2c0d5cfed2d275fe53f5df40aa2a49a848aab4c771858dfa1b5d5df913516c1f1c90ce5101b2f461865093f5e6787c79886cadc40bdb
7
- data.tar.gz: 06206026d180e9d044edda0a83018ae31895cce3e6d91cb9ae4bdb3c365565a27881dd17ffd6d8861d8126441e327cb8fdf91c41e6003c1d84a4038c8b9003ea
6
+ metadata.gz: 6fae90263d8e037d3e58a0f20de4e6eaa671d739f28fa1af90a013a9d0ade3a43fa118b5fe15c3e2a3de025a9c3e4a53dc4584426678619896b98d38cca2dcbc
7
+ data.tar.gz: c634542b0ec26e9df418222b8eab459f42d3f65e42c196c6a3769139073c94f98bfb0da8cb61b18f055629aa9fbf58d6516d13a40b453f27fbc4a6f1514d3d9d
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.23.8
1
+ 0.23.9
@@ -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:
@@ -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
@@ -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
@@ -23,6 +23,7 @@ require "language_server-protocol"
23
23
  require "rbs"
24
24
  require "fileutils"
25
25
  require "open3"
26
+ require "securerandom"
26
27
 
27
28
  require "ruby-lsp"
28
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
@@ -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
@@ -1131,7 +1145,12 @@ module RubyLsp
1131
1145
 
1132
1146
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
1133
1147
  def workspace_dependencies(message)
1134
- 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
1135
1154
  Bundler.with_original_env do
1136
1155
  definition = Bundler.definition
1137
1156
  dep_keys = definition.locked_deps.keys.to_set
@@ -1145,7 +1164,7 @@ module RubyLsp
1145
1164
  }
1146
1165
  end
1147
1166
  end
1148
- else
1167
+ rescue Bundler::GemNotFound
1149
1168
  []
1150
1169
  end
1151
1170
 
@@ -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.8
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-31 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