ruby-lsp 0.23.1 → 0.23.3

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.
@@ -40,19 +40,24 @@ module RubyLsp
40
40
  sig { returns(Encoding) }
41
41
  attr_reader :encoding
42
42
 
43
+ sig { returns(T.nilable(Edit)) }
44
+ attr_reader :last_edit
45
+
43
46
  sig { returns(T.any(Interface::SemanticTokens, Object)) }
44
47
  attr_accessor :semantic_tokens
45
48
 
46
- sig { params(source: String, version: Integer, uri: URI::Generic, encoding: Encoding).void }
47
- def initialize(source:, version:, uri:, encoding: Encoding::UTF_8)
49
+ sig { params(source: String, version: Integer, uri: URI::Generic, global_state: GlobalState).void }
50
+ def initialize(source:, version:, uri:, global_state:)
51
+ @source = source
52
+ @version = version
53
+ @global_state = global_state
48
54
  @cache = T.let(Hash.new(EMPTY_CACHE), T::Hash[String, T.untyped])
49
55
  @semantic_tokens = T.let(EMPTY_CACHE, T.any(Interface::SemanticTokens, Object))
50
- @encoding = T.let(encoding, Encoding)
51
- @source = T.let(source, String)
52
- @version = T.let(version, Integer)
56
+ @encoding = T.let(global_state.encoding, Encoding)
53
57
  @uri = T.let(uri, URI::Generic)
54
58
  @needs_parsing = T.let(true, T::Boolean)
55
59
  @parse_result = T.let(T.unsafe(nil), ParseResultType)
60
+ @last_edit = T.let(nil, T.nilable(Edit))
56
61
  parse!
57
62
  end
58
63
 
@@ -64,7 +69,6 @@ module RubyLsp
64
69
  sig { abstract.returns(LanguageId) }
65
70
  def language_id; end
66
71
 
67
- # TODO: remove this method once all non-positional requests have been migrated to the listener pattern
68
72
  sig do
69
73
  type_parameters(:T)
70
74
  .params(
@@ -106,6 +110,19 @@ module RubyLsp
106
110
  @version = version
107
111
  @needs_parsing = true
108
112
  @cache.clear
113
+
114
+ last_edit = edits.last
115
+ return unless last_edit
116
+
117
+ last_edit_range = last_edit[:range]
118
+
119
+ @last_edit = if last_edit_range[:start] == last_edit_range[:end]
120
+ Insert.new(last_edit_range)
121
+ elsif last_edit[:text].empty?
122
+ Delete.new(last_edit_range)
123
+ else
124
+ Replace.new(last_edit_range)
125
+ end
109
126
  end
110
127
 
111
128
  # Returns `true` if the document was parsed and `false` if nothing needed parsing
@@ -115,16 +132,52 @@ module RubyLsp
115
132
  sig { abstract.returns(T::Boolean) }
116
133
  def syntax_error?; end
117
134
 
135
+ sig { returns(T::Boolean) }
136
+ def past_expensive_limit?
137
+ @source.length > MAXIMUM_CHARACTERS_FOR_EXPENSIVE_FEATURES
138
+ end
139
+
140
+ sig do
141
+ params(
142
+ start_pos: T::Hash[Symbol, T.untyped],
143
+ end_pos: T.nilable(T::Hash[Symbol, T.untyped]),
144
+ ).returns([Integer, T.nilable(Integer)])
145
+ end
146
+ def find_index_by_position(start_pos, end_pos = nil)
147
+ @global_state.synchronize do
148
+ scanner = create_scanner
149
+ start_index = scanner.find_char_position(start_pos)
150
+ end_index = scanner.find_char_position(end_pos) if end_pos
151
+ [start_index, end_index]
152
+ end
153
+ end
154
+
155
+ private
156
+
118
157
  sig { returns(Scanner) }
119
158
  def create_scanner
120
159
  Scanner.new(@source, @encoding)
121
160
  end
122
161
 
123
- sig { returns(T::Boolean) }
124
- def past_expensive_limit?
125
- @source.length > MAXIMUM_CHARACTERS_FOR_EXPENSIVE_FEATURES
162
+ class Edit
163
+ extend T::Sig
164
+ extend T::Helpers
165
+
166
+ abstract!
167
+
168
+ sig { returns(T::Hash[Symbol, T.untyped]) }
169
+ attr_reader :range
170
+
171
+ sig { params(range: T::Hash[Symbol, T.untyped]).void }
172
+ def initialize(range)
173
+ @range = range
174
+ end
126
175
  end
127
176
 
177
+ class Insert < Edit; end
178
+ class Replace < Edit; end
179
+ class Delete < Edit; end
180
+
128
181
  class Scanner
129
182
  extend T::Sig
130
183
 
@@ -19,8 +19,8 @@ module RubyLsp
19
19
  end
20
20
  attr_reader :code_units_cache
21
21
 
22
- sig { params(source: String, version: Integer, uri: URI::Generic, encoding: Encoding).void }
23
- def initialize(source:, version:, uri:, encoding: Encoding::UTF_8)
22
+ sig { params(source: String, version: Integer, uri: URI::Generic, global_state: GlobalState).void }
23
+ def initialize(source:, version:, uri:, global_state:)
24
24
  # This has to be initialized before calling super because we call `parse` in the parent constructor, which
25
25
  # overrides this with the proper virtual host language source
26
26
  @host_language_source = T.let("", String)
@@ -63,9 +63,11 @@ module RubyLsp
63
63
  ).returns(NodeContext)
64
64
  end
65
65
  def locate_node(position, node_types: [])
66
+ char_position, _ = find_index_by_position(position)
67
+
66
68
  RubyDocument.locate(
67
69
  @parse_result.value,
68
- create_scanner.find_char_position(position),
70
+ char_position,
69
71
  code_units_cache: @code_units_cache,
70
72
  node_types: node_types,
71
73
  )
@@ -29,6 +29,9 @@ module RubyLsp
29
29
  sig { returns(ClientCapabilities) }
30
30
  attr_reader :client_capabilities
31
31
 
32
+ sig { returns(URI::Generic) }
33
+ attr_reader :workspace_uri
34
+
32
35
  sig { void }
33
36
  def initialize
34
37
  @workspace_uri = T.let(URI::Generic.from_path(path: Dir.pwd), URI::Generic)
@@ -53,6 +56,12 @@ module RubyLsp
53
56
  )
54
57
  @client_capabilities = T.let(ClientCapabilities.new, ClientCapabilities)
55
58
  @enabled_feature_flags = T.let({}, T::Hash[Symbol, T::Boolean])
59
+ @mutex = T.let(Mutex.new, Mutex)
60
+ end
61
+
62
+ sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
63
+ def synchronize(&block)
64
+ @mutex.synchronize(&block)
56
65
  end
57
66
 
58
67
  sig { params(addon_name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
@@ -330,7 +330,8 @@ module RubyLsp
330
330
 
331
331
  methods.each do |target_method|
332
332
  uri = target_method.uri
333
- next if sorbet_level_true_or_higher?(@sorbet_level) && not_in_dependencies?(T.must(uri.full_path))
333
+ full_path = uri.full_path
334
+ next if sorbet_level_true_or_higher?(@sorbet_level) && (!full_path || not_in_dependencies?(full_path))
334
335
 
335
336
  @response_builder << Interface::LocationLink.new(
336
337
  target_uri: uri.to_s,
@@ -403,7 +404,11 @@ module RubyLsp
403
404
  # additional behavior on top of jumping to RBIs. The only sigil where Sorbet cannot handle constants is typed
404
405
  # ignore
405
406
  uri = entry.uri
406
- next if @sorbet_level != RubyDocument::SorbetLevel::Ignore && not_in_dependencies?(T.must(uri.full_path))
407
+ full_path = uri.full_path
408
+
409
+ if @sorbet_level != RubyDocument::SorbetLevel::Ignore && (!full_path || not_in_dependencies?(full_path))
410
+ next
411
+ end
407
412
 
408
413
  @response_builder << Interface::LocationLink.new(
409
414
  target_uri: uri.to_s,
@@ -8,8 +8,8 @@ module RubyLsp
8
8
 
9
9
  ParseResultType = type_member { { fixed: T::Array[RBS::AST::Declarations::Base] } }
10
10
 
11
- sig { params(source: String, version: Integer, uri: URI::Generic, encoding: Encoding).void }
12
- def initialize(source:, version:, uri:, encoding: Encoding::UTF_8)
11
+ sig { params(source: String, version: Integer, uri: URI::Generic, global_state: GlobalState).void }
12
+ def initialize(source:, version:, uri:, global_state:)
13
13
  @syntax_error = T.let(false, T::Boolean)
14
14
  super
15
15
  end
@@ -92,9 +92,7 @@ module RubyLsp
92
92
  source_range = @code_action.dig(:data, :range)
93
93
  return Error::EmptySelection if source_range[:start] == source_range[:end]
94
94
 
95
- scanner = @document.create_scanner
96
- start_index = scanner.find_char_position(source_range[:start])
97
- end_index = scanner.find_char_position(source_range[:end])
95
+ start_index, end_index = @document.find_index_by_position(source_range[:start], source_range[:end])
98
96
  extracted_source = T.must(@document.source[start_index...end_index])
99
97
 
100
98
  # Find the closest statements node, so that we place the refactor in a valid position
@@ -192,9 +190,7 @@ module RubyLsp
192
190
  source_range = @code_action.dig(:data, :range)
193
191
  return Error::EmptySelection if source_range[:start] == source_range[:end]
194
192
 
195
- scanner = @document.create_scanner
196
- start_index = scanner.find_char_position(source_range[:start])
197
- end_index = scanner.find_char_position(source_range[:end])
193
+ start_index, end_index = @document.find_index_by_position(source_range[:start], source_range[:end])
198
194
  extracted_source = T.must(@document.source[start_index...end_index])
199
195
 
200
196
  # Find the closest method declaration node, so that we place the refactor in a valid position
@@ -40,7 +40,8 @@ module RubyLsp
40
40
  @dispatcher = dispatcher
41
41
  # Completion always receives the position immediately after the character that was just typed. Here we adjust it
42
42
  # back by 1, so that we find the right node
43
- char_position = document.create_scanner.find_char_position(params[:position]) - 1
43
+ char_position, _ = document.find_index_by_position(params[:position])
44
+ char_position -= 1
44
45
  delegate_request_if_needed!(global_state, document, char_position)
45
46
 
46
47
  node_context = RubyDocument.locate(
@@ -29,7 +29,7 @@ module RubyLsp
29
29
  )
30
30
  @dispatcher = dispatcher
31
31
 
32
- char_position = document.create_scanner.find_char_position(position)
32
+ char_position, _ = document.find_index_by_position(position)
33
33
  delegate_request_if_needed!(global_state, document, char_position)
34
34
 
35
35
  node_context = RubyDocument.locate(
@@ -25,7 +25,7 @@ module RubyLsp
25
25
  end
26
26
  def initialize(global_state, document, position, dispatcher)
27
27
  super()
28
- char_position = document.create_scanner.find_char_position(position)
28
+ char_position, _ = document.find_index_by_position(position)
29
29
  delegate_request_if_needed!(global_state, document, char_position)
30
30
 
31
31
  node_context = RubyDocument.locate(
@@ -34,7 +34,7 @@ module RubyLsp
34
34
  def initialize(document, global_state, position, dispatcher, sorbet_level)
35
35
  super()
36
36
 
37
- char_position = document.create_scanner.find_char_position(position)
37
+ char_position, _ = document.find_index_by_position(position)
38
38
  delegate_request_if_needed!(global_state, document, char_position)
39
39
 
40
40
  node_context = RubyDocument.locate(
@@ -24,7 +24,7 @@ module RubyLsp
24
24
 
25
25
  sig { override.returns(T.nilable(Interface::Range)) }
26
26
  def perform
27
- char_position = @document.create_scanner.find_char_position(@position)
27
+ char_position, _ = @document.find_index_by_position(@position)
28
28
 
29
29
  node_context = RubyDocument.locate(
30
30
  @document.parse_result.value,
@@ -30,7 +30,7 @@ module RubyLsp
30
30
  sig { override.returns(T::Array[Interface::Location]) }
31
31
  def perform
32
32
  position = @params[:position]
33
- char_position = @document.create_scanner.find_char_position(position)
33
+ char_position, _ = @document.find_index_by_position(position)
34
34
 
35
35
  node_context = RubyDocument.locate(
36
36
  @document.parse_result.value,
@@ -40,7 +40,7 @@ module RubyLsp
40
40
 
41
41
  sig { override.returns(T.nilable(Interface::WorkspaceEdit)) }
42
42
  def perform
43
- char_position = @document.create_scanner.find_char_position(@position)
43
+ char_position, _ = @document.find_index_by_position(@position)
44
44
 
45
45
  node_context = RubyDocument.locate(
46
46
  @document.parse_result.value,
@@ -116,8 +116,8 @@ module RubyLsp
116
116
  T.must(@global_state.index[fully_qualified_name]).each do |entry|
117
117
  # Do not rename files that are not part of the workspace
118
118
  uri = entry.uri
119
- file_path = T.must(uri.full_path)
120
- next unless file_path.start_with?(@global_state.workspace_path)
119
+ file_path = uri.full_path
120
+ next unless file_path&.start_with?(@global_state.workspace_path)
121
121
 
122
122
  case entry
123
123
  when RubyIndexer::Entry::Class, RubyIndexer::Entry::Module, RubyIndexer::Entry::Constant,
@@ -31,10 +31,7 @@ module RubyLsp
31
31
  sig { returns(String) }
32
32
  def ast_for_range
33
33
  range = T.must(@range)
34
-
35
- scanner = @document.create_scanner
36
- start_char = scanner.find_char_position(range[:start])
37
- end_char = scanner.find_char_position(range[:end])
34
+ start_char, end_char = @document.find_index_by_position(range[:start], range[:end])
38
35
 
39
36
  queue = @tree.statements.body.dup
40
37
  found_nodes = []
@@ -36,7 +36,7 @@ module RubyLsp
36
36
  def initialize(document, global_state, position, context, dispatcher, sorbet_level) # rubocop:disable Metrics/ParameterLists
37
37
  super()
38
38
 
39
- char_position = document.create_scanner.find_char_position(position)
39
+ char_position, _ = document.find_index_by_position(position)
40
40
  delegate_request_if_needed!(global_state, document, char_position)
41
41
 
42
42
  node_context = RubyDocument.locate(
@@ -22,10 +22,10 @@ module RubyLsp
22
22
  def perform
23
23
  @index.fuzzy_search(@query).filter_map do |entry|
24
24
  uri = entry.uri
25
- file_path = T.must(uri.full_path)
25
+ file_path = uri.full_path
26
26
 
27
27
  # We only show symbols declared in the workspace
28
- in_dependencies = !not_in_dependencies?(file_path)
28
+ in_dependencies = file_path && !not_in_dependencies?(file_path)
29
29
  next if in_dependencies
30
30
 
31
31
  # We should never show private symbols when searching the entire workspace
@@ -8,6 +8,22 @@ module RubyLsp
8
8
 
9
9
  ParseResultType = type_member { { fixed: Prism::ParseResult } }
10
10
 
11
+ METHODS_THAT_CHANGE_DECLARATIONS = [
12
+ :private_constant,
13
+ :attr_reader,
14
+ :attr_writer,
15
+ :attr_accessor,
16
+ :alias_method,
17
+ :include,
18
+ :prepend,
19
+ :extend,
20
+ :public,
21
+ :protected,
22
+ :private,
23
+ :module_function,
24
+ :private_class_method,
25
+ ].freeze
26
+
11
27
  class SorbetLevel < T::Enum
12
28
  enums do
13
29
  None = new("none")
@@ -142,8 +158,8 @@ module RubyLsp
142
158
  end
143
159
  attr_reader :code_units_cache
144
160
 
145
- sig { params(source: String, version: Integer, uri: URI::Generic, encoding: Encoding).void }
146
- def initialize(source:, version:, uri:, encoding: Encoding::UTF_8)
161
+ sig { params(source: String, version: Integer, uri: URI::Generic, global_state: GlobalState).void }
162
+ def initialize(source:, version:, uri:, global_state:)
147
163
  super
148
164
  @code_units_cache = T.let(@parse_result.code_units_cache(@encoding), T.any(
149
165
  T.proc.params(arg0: Integer).returns(Integer),
@@ -198,9 +214,8 @@ module RubyLsp
198
214
  ).returns(T.nilable(Prism::Node))
199
215
  end
200
216
  def locate_first_within_range(range, node_types: [])
201
- scanner = create_scanner
202
- start_position = scanner.find_char_position(range[:start])
203
- end_position = scanner.find_char_position(range[:end])
217
+ start_position, end_position = find_index_by_position(range[:start], range[:end])
218
+
204
219
  desired_range = (start_position...end_position)
205
220
  queue = T.let(@parse_result.value.child_nodes.compact, T::Array[T.nilable(Prism::Node)])
206
221
 
@@ -232,12 +247,66 @@ module RubyLsp
232
247
  ).returns(NodeContext)
233
248
  end
234
249
  def locate_node(position, node_types: [])
250
+ char_position, _ = find_index_by_position(position)
251
+
235
252
  RubyDocument.locate(
236
253
  @parse_result.value,
237
- create_scanner.find_char_position(position),
254
+ char_position,
238
255
  code_units_cache: @code_units_cache,
239
256
  node_types: node_types,
240
257
  )
241
258
  end
259
+
260
+ sig { returns(T::Boolean) }
261
+ def last_edit_may_change_declarations?
262
+ # This method controls when we should index documents. If there's no recent edit and the document has just been
263
+ # opened, we need to index it
264
+ return true unless @last_edit
265
+
266
+ case @last_edit
267
+ when Delete
268
+ # Not optimized yet. It's not trivial to identify that a declaration has been removed since the source is no
269
+ # longer there and we don't remember the deleted text
270
+ true
271
+ when Insert, Replace
272
+ position_may_impact_declarations?(@last_edit.range[:start])
273
+ else
274
+ false
275
+ end
276
+ end
277
+
278
+ private
279
+
280
+ sig { params(position: T::Hash[Symbol, Integer]).returns(T::Boolean) }
281
+ def position_may_impact_declarations?(position)
282
+ node_context = locate_node(position)
283
+ node_at_edit = node_context.node
284
+
285
+ # Adjust to the parent when editing the constant of a class/module declaration
286
+ if node_at_edit.is_a?(Prism::ConstantReadNode) || node_at_edit.is_a?(Prism::ConstantPathNode)
287
+ node_at_edit = node_context.parent
288
+ end
289
+
290
+ case node_at_edit
291
+ when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefNode,
292
+ Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathOperatorWriteNode,
293
+ Prism::ConstantPathAndWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantWriteNode,
294
+ Prism::ConstantAndWriteNode, Prism::ConstantOperatorWriteNode, Prism::GlobalVariableAndWriteNode,
295
+ Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableTargetNode,
296
+ Prism::GlobalVariableWriteNode, Prism::InstanceVariableWriteNode, Prism::InstanceVariableAndWriteNode,
297
+ Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode,
298
+ Prism::InstanceVariableTargetNode, Prism::AliasMethodNode
299
+ true
300
+ when Prism::MultiWriteNode
301
+ [*node_at_edit.lefts, *node_at_edit.rest, *node_at_edit.rights].any? do |node|
302
+ node.is_a?(Prism::ConstantTargetNode) || node.is_a?(Prism::ConstantPathTargetNode)
303
+ end
304
+ when Prism::CallNode
305
+ receiver = node_at_edit.receiver
306
+ (!receiver || receiver.is_a?(Prism::SelfNode)) && METHODS_THAT_CHANGE_DECLARATIONS.include?(node_at_edit.name)
307
+ else
308
+ false
309
+ end
310
+ end
242
311
  end
243
312
  end
@@ -107,7 +107,7 @@ module RubyLsp
107
107
  ),
108
108
  )
109
109
  when "$/cancelRequest"
110
- @mutex.synchronize { @cancelled_requests << message[:params][:id] }
110
+ @global_state.synchronize { @cancelled_requests << message[:params][:id] }
111
111
  when nil
112
112
  process_response(message) if message[:result]
113
113
  end
@@ -298,29 +298,11 @@ module RubyLsp
298
298
 
299
299
  # Not every client supports dynamic registration or file watching
300
300
  if @global_state.client_capabilities.supports_watching_files
301
- send_message(
302
- Request.new(
303
- id: @current_request_id,
304
- method: "client/registerCapability",
305
- params: Interface::RegistrationParams.new(
306
- registrations: [
307
- # Register watching Ruby files
308
- Interface::Registration.new(
309
- id: "workspace/didChangeWatchedFiles",
310
- method: "workspace/didChangeWatchedFiles",
311
- register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
312
- watchers: [
313
- Interface::FileSystemWatcher.new(
314
- glob_pattern: "**/*.rb",
315
- kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE,
316
- ),
317
- ],
318
- ),
319
- ),
320
- ],
321
- ),
322
- ),
323
- )
301
+ send_message(Request.register_watched_files(@current_request_id, "**/*.rb"))
302
+ send_message(Request.register_watched_files(
303
+ @current_request_id,
304
+ Interface::RelativePattern.new(base_uri: @global_state.workspace_uri.to_s, pattern: ".rubocop.yml"),
305
+ ))
324
306
  end
325
307
 
326
308
  process_indexing_configuration(options.dig(:initializationOptions, :indexing))
@@ -377,7 +359,7 @@ module RubyLsp
377
359
 
378
360
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
379
361
  def text_document_did_open(message)
380
- @mutex.synchronize do
362
+ @global_state.synchronize do
381
363
  text_document = message.dig(:params, :textDocument)
382
364
  language_id = case text_document[:languageId]
383
365
  when "erb", "eruby"
@@ -392,7 +374,6 @@ module RubyLsp
392
374
  uri: text_document[:uri],
393
375
  source: text_document[:text],
394
376
  version: text_document[:version],
395
- encoding: @global_state.encoding,
396
377
  language_id: language_id,
397
378
  )
398
379
 
@@ -417,17 +398,12 @@ module RubyLsp
417
398
 
418
399
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
419
400
  def text_document_did_close(message)
420
- @mutex.synchronize do
401
+ @global_state.synchronize do
421
402
  uri = message.dig(:params, :textDocument, :uri)
422
403
  @store.delete(uri)
423
404
 
424
405
  # Clear diagnostics for the closed file, so that they no longer appear in the problems tab
425
- send_message(
426
- Notification.new(
427
- method: "textDocument/publishDiagnostics",
428
- params: Interface::PublishDiagnosticsParams.new(uri: uri.to_s, diagnostics: []),
429
- ),
430
- )
406
+ send_message(Notification.publish_diagnostics(uri.to_s, []))
431
407
  end
432
408
  end
433
409
 
@@ -436,7 +412,7 @@ module RubyLsp
436
412
  params = message[:params]
437
413
  text_document = params[:textDocument]
438
414
 
439
- @mutex.synchronize do
415
+ @global_state.synchronize do
440
416
  @store.push_edits(uri: text_document[:uri], edits: params[:contentChanges], version: text_document[:version])
441
417
  end
442
418
  end
@@ -493,7 +469,22 @@ module RubyLsp
493
469
  document_link = Requests::DocumentLink.new(uri, parse_result.comments, dispatcher)
494
470
  code_lens = Requests::CodeLens.new(@global_state, uri, dispatcher)
495
471
  inlay_hint = Requests::InlayHints.new(document, T.must(@store.features_configuration.dig(:inlayHint)), dispatcher)
496
- dispatcher.dispatch(parse_result.value)
472
+
473
+ if document.is_a?(RubyDocument) && document.last_edit_may_change_declarations?
474
+ # Re-index the file as it is modified. This mode of indexing updates entries only. Require path trees are only
475
+ # updated on save
476
+ @global_state.synchronize do
477
+ send_log_message("Detected that last edit may have modified declarations. Re-indexing #{uri}")
478
+
479
+ @global_state.index.handle_change(uri) do |index|
480
+ index.delete(uri, skip_require_paths_tree: true)
481
+ RubyIndexer::DeclarationListener.new(index, dispatcher, parse_result, uri, collect_comments: true)
482
+ dispatcher.dispatch(parse_result.value)
483
+ end
484
+ end
485
+ else
486
+ dispatcher.dispatch(parse_result.value)
487
+ end
497
488
 
498
489
  # Store all responses retrieve in this round of visits in the cache and then return the response for the request
499
490
  # we actually received
@@ -1011,26 +1002,55 @@ module RubyLsp
1011
1002
  uri = URI(change[:uri])
1012
1003
  file_path = uri.to_standardized_path
1013
1004
  next if file_path.nil? || File.directory?(file_path)
1014
- next unless file_path.end_with?(".rb")
1015
1005
 
1016
- load_path_entry = $LOAD_PATH.find { |load_path| file_path.start_with?(load_path) }
1017
- uri.add_require_path_from_load_entry(load_path_entry) if load_path_entry
1006
+ if file_path.end_with?(".rb")
1007
+ handle_ruby_file_change(index, file_path, change[:type])
1008
+ next
1009
+ end
1018
1010
 
1019
- content = File.read(file_path)
1011
+ file_name = File.basename(file_path)
1020
1012
 
1021
- case change[:type]
1022
- when Constant::FileChangeType::CREATED
1023
- index.index_single(uri, content)
1024
- when Constant::FileChangeType::CHANGED
1025
- index.handle_change(uri, content)
1026
- when Constant::FileChangeType::DELETED
1027
- index.delete(uri)
1013
+ if file_name == ".rubocop.yml" || file_name == ".rubocop"
1014
+ handle_rubocop_config_change(uri)
1028
1015
  end
1029
1016
  end
1030
1017
 
1031
1018
  Addon.file_watcher_addons.each { |addon| T.unsafe(addon).workspace_did_change_watched_files(changes) }
1032
1019
  end
1033
1020
 
1021
+ sig { params(index: RubyIndexer::Index, file_path: String, change_type: Integer).void }
1022
+ def handle_ruby_file_change(index, file_path, change_type)
1023
+ load_path_entry = $LOAD_PATH.find { |load_path| file_path.start_with?(load_path) }
1024
+ uri = URI::Generic.from_path(load_path_entry: load_path_entry, path: file_path)
1025
+
1026
+ content = File.read(file_path)
1027
+
1028
+ case change_type
1029
+ when Constant::FileChangeType::CREATED
1030
+ index.index_single(uri, content)
1031
+ when Constant::FileChangeType::CHANGED
1032
+ index.handle_change(uri, content)
1033
+ when Constant::FileChangeType::DELETED
1034
+ index.delete(uri)
1035
+ end
1036
+ end
1037
+
1038
+ sig { params(uri: URI::Generic).void }
1039
+ def handle_rubocop_config_change(uri)
1040
+ return unless defined?(Requests::Support::RuboCopFormatter)
1041
+
1042
+ send_log_message("Reloading RuboCop since #{uri} changed")
1043
+ @global_state.register_formatter("rubocop", Requests::Support::RuboCopFormatter.new)
1044
+
1045
+ # Clear all existing diagnostics since the config changed. This has to happen under a mutex because the `state`
1046
+ # hash cannot be mutated during iteration or that will throw an error
1047
+ @global_state.synchronize do
1048
+ @store.each do |uri, _document|
1049
+ send_message(Notification.publish_diagnostics(uri.to_s, []))
1050
+ end
1051
+ end
1052
+ end
1053
+
1034
1054
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
1035
1055
  def workspace_symbol(message)
1036
1056
  send_message(
@@ -1240,10 +1260,10 @@ module RubyLsp
1240
1260
  return
1241
1261
  end
1242
1262
 
1243
- return unless indexing_options
1244
-
1245
1263
  configuration = @global_state.index.configuration
1246
1264
  configuration.workspace_path = @global_state.workspace_path
1265
+ return unless indexing_options
1266
+
1247
1267
  # The index expects snake case configurations, but VS Code standardizes on camel case settings
1248
1268
  configuration.apply_config(indexing_options.transform_keys { |key| key.to_s.gsub(/([A-Z])/, "_\\1").downcase })
1249
1269
  end