ruby-lsp 0.22.1 → 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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +12 -11
  5. data/exe/ruby-lsp-check +5 -5
  6. data/exe/ruby-lsp-launcher +41 -15
  7. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +26 -20
  8. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +191 -100
  9. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +60 -30
  10. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +174 -61
  11. data/lib/ruby_indexer/lib/ruby_indexer/location.rb +12 -0
  12. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +16 -14
  13. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +82 -61
  14. data/lib/{core_ext → ruby_indexer/lib/ruby_indexer}/uri.rb +29 -3
  15. data/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb +36 -0
  16. data/lib/ruby_indexer/ruby_indexer.rb +2 -1
  17. data/lib/ruby_indexer/test/class_variables_test.rb +140 -0
  18. data/lib/ruby_indexer/test/classes_and_modules_test.rb +30 -6
  19. data/lib/ruby_indexer/test/configuration_test.rb +116 -51
  20. data/lib/ruby_indexer/test/enhancements_test.rb +2 -2
  21. data/lib/ruby_indexer/test/index_test.rb +143 -44
  22. data/lib/ruby_indexer/test/instance_variables_test.rb +20 -0
  23. data/lib/ruby_indexer/test/method_test.rb +86 -8
  24. data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
  25. data/lib/ruby_indexer/test/reference_finder_test.rb +90 -2
  26. data/lib/ruby_indexer/test/test_case.rb +2 -2
  27. data/lib/ruby_indexer/test/uri_test.rb +72 -0
  28. data/lib/ruby_lsp/addon.rb +9 -0
  29. data/lib/ruby_lsp/base_server.rb +17 -18
  30. data/lib/ruby_lsp/client_capabilities.rb +7 -1
  31. data/lib/ruby_lsp/document.rb +72 -10
  32. data/lib/ruby_lsp/erb_document.rb +5 -3
  33. data/lib/ruby_lsp/global_state.rb +42 -3
  34. data/lib/ruby_lsp/internal.rb +3 -1
  35. data/lib/ruby_lsp/listeners/code_lens.rb +9 -5
  36. data/lib/ruby_lsp/listeners/completion.rb +78 -6
  37. data/lib/ruby_lsp/listeners/definition.rb +80 -19
  38. data/lib/ruby_lsp/listeners/document_highlight.rb +3 -2
  39. data/lib/ruby_lsp/listeners/document_link.rb +21 -3
  40. data/lib/ruby_lsp/listeners/document_symbol.rb +12 -1
  41. data/lib/ruby_lsp/listeners/folding_ranges.rb +1 -1
  42. data/lib/ruby_lsp/listeners/hover.rb +59 -2
  43. data/lib/ruby_lsp/load_sorbet.rb +3 -3
  44. data/lib/ruby_lsp/rbs_document.rb +2 -2
  45. data/lib/ruby_lsp/requests/code_action_resolve.rb +90 -6
  46. data/lib/ruby_lsp/requests/code_actions.rb +57 -1
  47. data/lib/ruby_lsp/requests/completion.rb +8 -1
  48. data/lib/ruby_lsp/requests/completion_resolve.rb +2 -1
  49. data/lib/ruby_lsp/requests/definition.rb +7 -1
  50. data/lib/ruby_lsp/requests/diagnostics.rb +1 -1
  51. data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
  52. data/lib/ruby_lsp/requests/folding_ranges.rb +2 -6
  53. data/lib/ruby_lsp/requests/formatting.rb +2 -6
  54. data/lib/ruby_lsp/requests/hover.rb +1 -1
  55. data/lib/ruby_lsp/requests/on_type_formatting.rb +2 -2
  56. data/lib/ruby_lsp/requests/prepare_rename.rb +51 -0
  57. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +1 -1
  58. data/lib/ruby_lsp/requests/references.rb +29 -2
  59. data/lib/ruby_lsp/requests/rename.rb +17 -7
  60. data/lib/ruby_lsp/requests/semantic_highlighting.rb +1 -1
  61. data/lib/ruby_lsp/requests/show_syntax_tree.rb +1 -4
  62. data/lib/ruby_lsp/requests/signature_help.rb +1 -1
  63. data/lib/ruby_lsp/requests/support/common.rb +2 -9
  64. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +3 -3
  65. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +13 -13
  66. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +1 -1
  67. data/lib/ruby_lsp/requests/workspace_symbol.rb +4 -3
  68. data/lib/ruby_lsp/ruby_document.rb +80 -6
  69. data/lib/ruby_lsp/scripts/compose_bundle.rb +1 -1
  70. data/lib/ruby_lsp/server.rb +205 -61
  71. data/lib/ruby_lsp/setup_bundler.rb +50 -43
  72. data/lib/ruby_lsp/store.rb +7 -7
  73. data/lib/ruby_lsp/test_helper.rb +45 -11
  74. data/lib/ruby_lsp/type_inferrer.rb +60 -31
  75. data/lib/ruby_lsp/utils.rb +63 -3
  76. metadata +8 -8
  77. data/lib/ruby_indexer/lib/ruby_indexer/indexable_path.rb +0 -29
@@ -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,71 @@ 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 should_index?
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
+ last_edit_may_change_declarations?
267
+ end
268
+
269
+ private
270
+
271
+ sig { returns(T::Boolean) }
272
+ def last_edit_may_change_declarations?
273
+ case @last_edit
274
+ when Delete
275
+ # Not optimized yet. It's not trivial to identify that a declaration has been removed since the source is no
276
+ # longer there and we don't remember the deleted text
277
+ true
278
+ when Insert, Replace
279
+ position_may_impact_declarations?(@last_edit.range[:start])
280
+ else
281
+ false
282
+ end
283
+ end
284
+
285
+ sig { params(position: T::Hash[Symbol, Integer]).returns(T::Boolean) }
286
+ def position_may_impact_declarations?(position)
287
+ node_context = locate_node(position)
288
+ node_at_edit = node_context.node
289
+
290
+ # Adjust to the parent when editing the constant of a class/module declaration
291
+ if node_at_edit.is_a?(Prism::ConstantReadNode) || node_at_edit.is_a?(Prism::ConstantPathNode)
292
+ node_at_edit = node_context.parent
293
+ end
294
+
295
+ case node_at_edit
296
+ when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefNode,
297
+ Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathOperatorWriteNode,
298
+ Prism::ConstantPathAndWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantWriteNode,
299
+ Prism::ConstantAndWriteNode, Prism::ConstantOperatorWriteNode, Prism::GlobalVariableAndWriteNode,
300
+ Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableTargetNode,
301
+ Prism::GlobalVariableWriteNode, Prism::InstanceVariableWriteNode, Prism::InstanceVariableAndWriteNode,
302
+ Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode,
303
+ Prism::InstanceVariableTargetNode, Prism::AliasMethodNode
304
+ true
305
+ when Prism::MultiWriteNode
306
+ [*node_at_edit.lefts, *node_at_edit.rest, *node_at_edit.rights].any? do |node|
307
+ node.is_a?(Prism::ConstantTargetNode) || node.is_a?(Prism::ConstantPathTargetNode)
308
+ end
309
+ when Prism::CallNode
310
+ receiver = node_at_edit.receiver
311
+ (!receiver || receiver.is_a?(Prism::SelfNode)) && METHODS_THAT_CHANGE_DECLARATIONS.include?(node_at_edit.name)
312
+ else
313
+ false
314
+ end
315
+ end
242
316
  end
243
317
  end
@@ -5,7 +5,7 @@ def compose(raw_initialize)
5
5
  require_relative "../setup_bundler"
6
6
  require "json"
7
7
  require "uri"
8
- require_relative "../../core_ext/uri"
8
+ require "ruby_indexer/lib/ruby_indexer/uri"
9
9
 
10
10
  initialize_request = JSON.parse(raw_initialize, symbolize_names: true)
11
11
  workspace_uri = initialize_request.dig(:params, :workspaceFolders, 0, :uri)
@@ -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
@@ -71,6 +71,8 @@ module RubyLsp
71
71
  text_document_prepare_type_hierarchy(message)
72
72
  when "textDocument/rename"
73
73
  text_document_rename(message)
74
+ when "textDocument/prepareRename"
75
+ text_document_prepare_rename(message)
74
76
  when "textDocument/references"
75
77
  text_document_references(message)
76
78
  when "typeHierarchy/supertypes"
@@ -104,8 +106,10 @@ module RubyLsp
104
106
  end,
105
107
  ),
106
108
  )
109
+ when "rubyLsp/composeBundle"
110
+ compose_bundle(message)
107
111
  when "$/cancelRequest"
108
- @mutex.synchronize { @cancelled_requests << message[:params][:id] }
112
+ @global_state.synchronize { @cancelled_requests << message[:params][:id] }
109
113
  when nil
110
114
  process_response(message) if message[:result]
111
115
  end
@@ -117,12 +121,24 @@ module RubyLsp
117
121
  # If a document is deleted before we are able to process all of its enqueued requests, we will try to read it
118
122
  # from disk and it raise this error. This is expected, so we don't include the `data` attribute to avoid
119
123
  # reporting these to our telemetry
120
- if e.is_a?(Store::NonExistingDocumentError)
124
+ case e
125
+ when Store::NonExistingDocumentError
121
126
  send_message(Error.new(
122
127
  id: message[:id],
123
128
  code: Constant::ErrorCodes::INVALID_PARAMS,
124
129
  message: e.full_message,
125
130
  ))
131
+ when Document::LocationNotFoundError
132
+ send_message(Error.new(
133
+ id: message[:id],
134
+ code: Constant::ErrorCodes::REQUEST_FAILED,
135
+ message: <<~MESSAGE,
136
+ Request #{message[:method]} failed to find the target position.
137
+ The file might have been modified while the server was in the middle of searching for the target.
138
+ If you experience this regularly, please report any findings and extra information on
139
+ https://github.com/Shopify/ruby-lsp/issues/2446
140
+ MESSAGE
141
+ ))
126
142
  else
127
143
  send_message(Error.new(
128
144
  id: message[:id],
@@ -237,6 +253,7 @@ module RubyLsp
237
253
  completion_provider = Requests::Completion.provider if enabled_features["completion"]
238
254
  signature_help_provider = Requests::SignatureHelp.provider if enabled_features["signatureHelp"]
239
255
  type_hierarchy_provider = Requests::PrepareTypeHierarchy.provider if enabled_features["typeHierarchy"]
256
+ rename_provider = Requests::Rename.provider unless @global_state.has_type_checker
240
257
 
241
258
  response = {
242
259
  capabilities: Interface::ServerCapabilities.new(
@@ -263,11 +280,12 @@ module RubyLsp
263
280
  workspace_symbol_provider: enabled_features["workspaceSymbol"] && !@global_state.has_type_checker,
264
281
  signature_help_provider: signature_help_provider,
265
282
  type_hierarchy_provider: type_hierarchy_provider,
266
- rename_provider: !@global_state.has_type_checker,
283
+ rename_provider: rename_provider,
267
284
  references_provider: !@global_state.has_type_checker,
268
285
  document_range_formatting_provider: true,
269
286
  experimental: {
270
287
  addon_detection: true,
288
+ compose_bundle: true,
271
289
  },
272
290
  ),
273
291
  serverInfo: {
@@ -283,29 +301,20 @@ module RubyLsp
283
301
 
284
302
  # Not every client supports dynamic registration or file watching
285
303
  if @global_state.client_capabilities.supports_watching_files
286
- send_message(
287
- Request.new(
288
- id: @current_request_id,
289
- method: "client/registerCapability",
290
- params: Interface::RegistrationParams.new(
291
- registrations: [
292
- # Register watching Ruby files
293
- Interface::Registration.new(
294
- id: "workspace/didChangeWatchedFiles",
295
- method: "workspace/didChangeWatchedFiles",
296
- register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
297
- watchers: [
298
- Interface::FileSystemWatcher.new(
299
- glob_pattern: "**/*.rb",
300
- kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE,
301
- ),
302
- ],
303
- ),
304
- ),
305
- ],
306
- ),
304
+ send_message(Request.register_watched_files(
305
+ @current_request_id,
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}",
307
315
  ),
308
- )
316
+ registration_id: "rubocop-watcher",
317
+ ))
309
318
  end
310
319
 
311
320
  process_indexing_configuration(options.dig(:initializationOptions, :indexing))
@@ -341,8 +350,8 @@ module RubyLsp
341
350
  unless @setup_error
342
351
  if defined?(Requests::Support::RuboCopFormatter)
343
352
  begin
344
- @global_state.register_formatter("rubocop", Requests::Support::RuboCopFormatter.new)
345
- rescue RuboCop::Error => e
353
+ @global_state.register_formatter("rubocop_internal", Requests::Support::RuboCopFormatter.new)
354
+ rescue ::RuboCop::Error => e
346
355
  # The user may have provided unknown config switches in .rubocop or
347
356
  # is trying to load a non-existent config file.
348
357
  send_message(Notification.window_show_message(
@@ -362,7 +371,7 @@ module RubyLsp
362
371
 
363
372
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
364
373
  def text_document_did_open(message)
365
- @mutex.synchronize do
374
+ @global_state.synchronize do
366
375
  text_document = message.dig(:params, :textDocument)
367
376
  language_id = case text_document[:languageId]
368
377
  when "erb", "eruby"
@@ -377,7 +386,6 @@ module RubyLsp
377
386
  uri: text_document[:uri],
378
387
  source: text_document[:text],
379
388
  version: text_document[:version],
380
- encoding: @global_state.encoding,
381
389
  language_id: language_id,
382
390
  )
383
391
 
@@ -402,17 +410,12 @@ module RubyLsp
402
410
 
403
411
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
404
412
  def text_document_did_close(message)
405
- @mutex.synchronize do
413
+ @global_state.synchronize do
406
414
  uri = message.dig(:params, :textDocument, :uri)
407
415
  @store.delete(uri)
408
416
 
409
417
  # Clear diagnostics for the closed file, so that they no longer appear in the problems tab
410
- send_message(
411
- Notification.new(
412
- method: "textDocument/publishDiagnostics",
413
- params: Interface::PublishDiagnosticsParams.new(uri: uri.to_s, diagnostics: []),
414
- ),
415
- )
418
+ send_message(Notification.publish_diagnostics(uri.to_s, []))
416
419
  end
417
420
  end
418
421
 
@@ -421,7 +424,7 @@ module RubyLsp
421
424
  params = message[:params]
422
425
  text_document = params[:textDocument]
423
426
 
424
- @mutex.synchronize do
427
+ @global_state.synchronize do
425
428
  @store.push_edits(uri: text_document[:uri], edits: params[:contentChanges], version: text_document[:version])
426
429
  end
427
430
  end
@@ -478,7 +481,22 @@ module RubyLsp
478
481
  document_link = Requests::DocumentLink.new(uri, parse_result.comments, dispatcher)
479
482
  code_lens = Requests::CodeLens.new(@global_state, uri, dispatcher)
480
483
  inlay_hint = Requests::InlayHints.new(document, T.must(@store.features_configuration.dig(:inlayHint)), dispatcher)
481
- dispatcher.dispatch(parse_result.value)
484
+
485
+ if document.is_a?(RubyDocument) && document.should_index?
486
+ # Re-index the file as it is modified. This mode of indexing updates entries only. Require path trees are only
487
+ # updated on save
488
+ @global_state.synchronize do
489
+ send_log_message("Determined that document should be indexed: #{uri}")
490
+
491
+ @global_state.index.handle_change(uri) do |index|
492
+ index.delete(uri, skip_require_paths_tree: true)
493
+ RubyIndexer::DeclarationListener.new(index, dispatcher, parse_result, uri, collect_comments: true)
494
+ dispatcher.dispatch(parse_result.value)
495
+ end
496
+ end
497
+ else
498
+ dispatcher.dispatch(parse_result.value)
499
+ end
482
500
 
483
501
  # Store all responses retrieve in this round of visits in the cache and then return the response for the request
484
502
  # we actually received
@@ -727,6 +745,24 @@ module RubyLsp
727
745
  send_message(Error.new(id: message[:id], code: Constant::ErrorCodes::REQUEST_FAILED, message: e.message))
728
746
  end
729
747
 
748
+ sig { params(message: T::Hash[Symbol, T.untyped]).void }
749
+ def text_document_prepare_rename(message)
750
+ params = message[:params]
751
+ document = @store.get(params.dig(:textDocument, :uri))
752
+
753
+ unless document.is_a?(RubyDocument)
754
+ send_empty_response(message[:id])
755
+ return
756
+ end
757
+
758
+ send_message(
759
+ Result.new(
760
+ id: message[:id],
761
+ response: Requests::PrepareRename.new(document, params[:position]).perform,
762
+ ),
763
+ )
764
+ end
765
+
730
766
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
731
767
  def text_document_references(message)
732
768
  params = message[:params]
@@ -972,28 +1008,83 @@ module RubyLsp
972
1008
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
973
1009
  def workspace_did_change_watched_files(message)
974
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
+
975
1016
  index = @global_state.index
976
1017
  changes.each do |change|
977
1018
  # File change events include folders, but we're only interested in files
978
1019
  uri = URI(change[:uri])
979
1020
  file_path = uri.to_standardized_path
980
1021
  next if file_path.nil? || File.directory?(file_path)
981
- next unless file_path.end_with?(".rb")
982
-
983
- load_path_entry = $LOAD_PATH.find { |load_path| file_path.start_with?(load_path) }
984
- indexable = RubyIndexer::IndexablePath.new(load_path_entry, file_path)
985
-
986
- case change[:type]
987
- when Constant::FileChangeType::CREATED
988
- index.index_single(indexable)
989
- when Constant::FileChangeType::CHANGED
990
- index.handle_change(indexable)
991
- when Constant::FileChangeType::DELETED
992
- index.delete(indexable)
1022
+
1023
+ if file_path.end_with?(".rb")
1024
+ handle_ruby_file_change(index, file_path, change[:type])
1025
+ next
1026
+ end
1027
+
1028
+ file_name = File.basename(file_path)
1029
+
1030
+ if file_name == ".rubocop.yml" || file_name == ".rubocop"
1031
+ handle_rubocop_config_change(uri)
993
1032
  end
994
1033
  end
995
1034
 
996
- 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
1043
+ end
1044
+
1045
+ sig { params(index: RubyIndexer::Index, file_path: String, change_type: Integer).void }
1046
+ def handle_ruby_file_change(index, file_path, change_type)
1047
+ load_path_entry = $LOAD_PATH.find { |load_path| file_path.start_with?(load_path) }
1048
+ uri = URI::Generic.from_path(load_path_entry: load_path_entry, path: file_path)
1049
+
1050
+ content = File.read(file_path)
1051
+
1052
+ case change_type
1053
+ when Constant::FileChangeType::CREATED
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)
1057
+ when Constant::FileChangeType::CHANGED
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)
1061
+ when Constant::FileChangeType::DELETED
1062
+ index.delete(uri)
1063
+ end
1064
+ rescue Errno::ENOENT
1065
+ # If a file is created and then delete immediately afterwards, we will process the created notification before we
1066
+ # receive the deleted one, but the file no longer exists. This may happen when running a test suite that creates
1067
+ # and deletes files automatically.
1068
+ end
1069
+
1070
+ sig { params(uri: URI::Generic).void }
1071
+ def handle_rubocop_config_change(uri)
1072
+ return unless defined?(Requests::Support::RuboCopFormatter)
1073
+
1074
+ # Register a new runner to reload configurations
1075
+ @global_state.register_formatter("rubocop_internal", Requests::Support::RuboCopFormatter.new)
1076
+
1077
+ # Clear all document caches for pull diagnostics
1078
+ @global_state.synchronize do
1079
+ @store.each do |_uri, document|
1080
+ document.cache_set("textDocument/diagnostic", Document::EMPTY_CACHE)
1081
+ end
1082
+ end
1083
+
1084
+ # Request a pull diagnostic refresh from the editor
1085
+ if @global_state.client_capabilities.supports_diagnostic_refresh
1086
+ send_message(Request.new(id: @current_request_id, method: "workspace/diagnostic/refresh", params: nil))
1087
+ end
997
1088
  end
998
1089
 
999
1090
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
@@ -1065,7 +1156,12 @@ module RubyLsp
1065
1156
 
1066
1157
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
1067
1158
  def workspace_dependencies(message)
1068
- 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
1069
1165
  Bundler.with_original_env do
1070
1166
  definition = Bundler.definition
1071
1167
  dep_keys = definition.locked_deps.keys.to_set
@@ -1079,7 +1175,7 @@ module RubyLsp
1079
1175
  }
1080
1176
  end
1081
1177
  end
1082
- else
1178
+ rescue Bundler::GemNotFound
1083
1179
  []
1084
1180
  end
1085
1181
 
@@ -1088,7 +1184,7 @@ module RubyLsp
1088
1184
 
1089
1185
  sig { override.void }
1090
1186
  def shutdown
1091
- Addon.addons.each(&:deactivate)
1187
+ Addon.unload_addons
1092
1188
  end
1093
1189
 
1094
1190
  sig { void }
@@ -1156,16 +1252,15 @@ module RubyLsp
1156
1252
  sig { void }
1157
1253
  def check_formatter_is_available
1158
1254
  return if @setup_error
1159
- # Warn of an unavailable `formatter` setting, e.g. `rubocop` on a project which doesn't have RuboCop.
1160
- # Syntax Tree will always be available via Ruby LSP so we don't need to check for it.
1161
- return unless @global_state.formatter == "rubocop"
1255
+ # Warn of an unavailable `formatter` setting, e.g. `rubocop_internal` on a project which doesn't have RuboCop.
1256
+ return unless @global_state.formatter == "rubocop_internal"
1162
1257
 
1163
1258
  unless defined?(RubyLsp::Requests::Support::RuboCopRunner)
1164
1259
  @global_state.formatter = "none"
1165
1260
 
1166
1261
  send_message(
1167
1262
  Notification.window_show_message(
1168
- "Ruby LSP formatter is set to `rubocop` but RuboCop was not found in the Gemfile or gemspec.",
1263
+ "Ruby LSP formatter is set to `rubocop_internal` but RuboCop was not found in the Gemfile or gemspec.",
1169
1264
  type: Constant::MessageType::ERROR,
1170
1265
  ),
1171
1266
  )
@@ -1205,10 +1300,10 @@ module RubyLsp
1205
1300
  return
1206
1301
  end
1207
1302
 
1208
- return unless indexing_options
1209
-
1210
1303
  configuration = @global_state.index.configuration
1211
1304
  configuration.workspace_path = @global_state.workspace_path
1305
+ return unless indexing_options
1306
+
1212
1307
  # The index expects snake case configurations, but VS Code standardizes on camel case settings
1213
1308
  configuration.apply_config(indexing_options.transform_keys { |key| key.to_s.gsub(/([A-Z])/, "_\\1").downcase })
1214
1309
  end
@@ -1224,5 +1319,54 @@ module RubyLsp
1224
1319
 
1225
1320
  addon.handle_window_show_message_response(result[:title])
1226
1321
  end
1322
+
1323
+ # NOTE: all servers methods are void because they can produce several messages for the client. The only reason this
1324
+ # method returns the created thread is to that we can join it in tests and avoid flakiness. The implementation is
1325
+ # not supposed to rely on the return of this method
1326
+ sig { params(message: T::Hash[Symbol, T.untyped]).returns(T.nilable(Thread)) }
1327
+ def compose_bundle(message)
1328
+ already_composed_path = File.join(@global_state.workspace_path, ".ruby-lsp", "bundle_is_composed")
1329
+ id = message[:id]
1330
+
1331
+ begin
1332
+ Bundler.with_original_env do
1333
+ Bundler::LockfileParser.new(Bundler.default_lockfile.read)
1334
+ end
1335
+ rescue Bundler::LockfileError => e
1336
+ send_message(Error.new(id: id, code: BUNDLE_COMPOSE_FAILED_CODE, message: e.message))
1337
+ return
1338
+ rescue Bundler::GemfileNotFound, Errno::ENOENT
1339
+ # We still compose the bundle if there's no Gemfile or if the lockfile got deleted
1340
+ end
1341
+
1342
+ # We compose the bundle in a thread so that the LSP continues to work while we're checking for its validity. Once
1343
+ # we return the response back to the editor, then the restart is triggered
1344
+ Thread.new do
1345
+ send_log_message("Recomposing the bundle ahead of restart")
1346
+
1347
+ _stdout, stderr, status = Bundler.with_unbundled_env do
1348
+ Open3.capture3(
1349
+ Gem.ruby,
1350
+ "-I",
1351
+ File.dirname(T.must(__dir__)),
1352
+ File.expand_path("../../exe/ruby-lsp-launcher", __dir__),
1353
+ @global_state.workspace_uri.to_s,
1354
+ chdir: @global_state.workspace_path,
1355
+ )
1356
+ end
1357
+
1358
+ if status&.exitstatus == 0
1359
+ # Create a signal for the restart that it can skip composing the bundle and launch directly
1360
+ FileUtils.touch(already_composed_path)
1361
+ send_message(Result.new(id: id, response: { success: true }))
1362
+ else
1363
+ # This special error code makes the extension avoid restarting in case we already know that the composed
1364
+ # bundle is not valid
1365
+ send_message(
1366
+ Error.new(id: id, code: BUNDLE_COMPOSE_FAILED_CODE, message: "Failed to compose bundle\n#{stderr}"),
1367
+ )
1368
+ end
1369
+ end
1370
+ end
1227
1371
  end
1228
1372
  end