ruby-lsp 0.23.8 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 918079e906832b0289a053cd4bfe849422082c25bd0f4bdada25cab3a532005f
4
- data.tar.gz: 7935b2bdc52a2bb83264376fb98f7ef8d3bfc3c5cb9dadd23642a1ebafbca013
3
+ metadata.gz: 3a0d78055f2f6bf861e581a5b5a2d1321d3a8452b23d638becb6e64da925d842
4
+ data.tar.gz: '0397aa5a123ed08757606db1be3690b50cba847a372f52da25fb64a6fb5816ce'
5
5
  SHA512:
6
- metadata.gz: 9fb084d322308bb2e69c2c0d5cfed2d275fe53f5df40aa2a49a848aab4c771858dfa1b5d5df913516c1f1c90ce5101b2f461865093f5e6787c79886cadc40bdb
7
- data.tar.gz: 06206026d180e9d044edda0a83018ae31895cce3e6d91cb9ae4bdb3c365565a27881dd17ffd6d8861d8126441e327cb8fdf91c41e6003c1d84a4038c8b9003ea
6
+ metadata.gz: a14d101646bd32467724aeedb60892c5c06e775a57d749623cc1e76dbc42ee6d82eca10e2fa1149900365eff59ee57c20300bb043a3fdcf1330961b6bbd94918
7
+ data.tar.gz: 23b339c5854620771e7b505381dad64cf3e8d89ed2850a19c7a299e72d2363202e1e74327c05d27d8fb7fae508abb7141e5a858319b2841c8c914233e28e076e
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.23.8
1
+ 0.23.10
@@ -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
@@ -32,6 +32,9 @@ module RubyLsp
32
32
  sig { returns(URI::Generic) }
33
33
  attr_reader :workspace_uri
34
34
 
35
+ sig { returns(T.nilable(String)) }
36
+ attr_reader :telemetry_machine_id
37
+
35
38
  sig { void }
36
39
  def initialize
37
40
  @workspace_uri = T.let(URI::Generic.from_path(path: Dir.pwd), URI::Generic)
@@ -57,6 +60,7 @@ module RubyLsp
57
60
  @client_capabilities = T.let(ClientCapabilities.new, ClientCapabilities)
58
61
  @enabled_feature_flags = T.let({}, T::Hash[Symbol, T::Boolean])
59
62
  @mutex = T.let(Mutex.new, Mutex)
63
+ @telemetry_machine_id = T.let(nil, T.nilable(String))
60
64
  end
61
65
 
62
66
  sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
@@ -175,6 +179,7 @@ module RubyLsp
175
179
  enabled_flags = options.dig(:initializationOptions, :enabledFeatureFlags)
176
180
  @enabled_feature_flags = enabled_flags if enabled_flags
177
181
 
182
+ @telemetry_machine_id = options.dig(:initializationOptions, :telemetryMachineId)
178
183
  notifications
179
184
  end
180
185
 
@@ -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
@@ -471,6 +471,9 @@ module RubyLsp
471
471
  path_node_to_complete,
472
472
  )
473
473
  end
474
+ rescue Errno::EPERM
475
+ # If the user writes a relative require pointing to a path that the editor has no permissions to read, then glob
476
+ # might fail with EPERM
474
477
  end
475
478
 
476
479
  sig { params(node: Prism::CallNode, name: String).void }
@@ -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
@@ -1018,7 +1032,14 @@ module RubyLsp
1018
1032
  end
1019
1033
  end
1020
1034
 
1021
- Addon.file_watcher_addons.each { |addon| T.unsafe(addon).workspace_did_change_watched_files(changes) }
1035
+ Addon.file_watcher_addons.each do |addon|
1036
+ T.unsafe(addon).workspace_did_change_watched_files(changes)
1037
+ rescue => e
1038
+ send_log_message(
1039
+ "Error in #{addon.name} add-on while processing watched file notifications: #{e.full_message}",
1040
+ type: Constant::MessageType::ERROR,
1041
+ )
1042
+ end
1022
1043
  end
1023
1044
 
1024
1045
  sig { params(index: RubyIndexer::Index, file_path: String, change_type: Integer).void }
@@ -1030,9 +1051,13 @@ module RubyLsp
1030
1051
 
1031
1052
  case change_type
1032
1053
  when Constant::FileChangeType::CREATED
1033
- index.index_single(uri, content)
1054
+ # If we receive a late created notification for a file that has already been claimed by the client, we want to
1055
+ # handle change for that URI so that the require path tree is updated
1056
+ @store.key?(uri) ? index.handle_change(uri, content) : index.index_single(uri, content)
1034
1057
  when Constant::FileChangeType::CHANGED
1035
- index.handle_change(uri, content)
1058
+ # We only handle changes on file watched notifications if the client is not the one managing this URI.
1059
+ # Otherwise, these changes are handled when running the combined requests
1060
+ index.handle_change(uri, content) unless @store.key?(uri)
1036
1061
  when Constant::FileChangeType::DELETED
1037
1062
  index.delete(uri)
1038
1063
  end
@@ -1131,7 +1156,12 @@ module RubyLsp
1131
1156
 
1132
1157
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
1133
1158
  def workspace_dependencies(message)
1134
- response = if @global_state.top_level_bundle
1159
+ unless @global_state.top_level_bundle
1160
+ send_message(Result.new(id: message[:id], response: []))
1161
+ return
1162
+ end
1163
+
1164
+ response = begin
1135
1165
  Bundler.with_original_env do
1136
1166
  definition = Bundler.definition
1137
1167
  dep_keys = definition.locked_deps.keys.to_set
@@ -1145,7 +1175,7 @@ module RubyLsp
1145
1175
  }
1146
1176
  end
1147
1177
  end
1148
- else
1178
+ rescue Bundler::GemNotFound
1149
1179
  []
1150
1180
  end
1151
1181
 
@@ -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.10
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-10 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: language_server-protocol