ruby-lsp 0.23.1 → 0.23.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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