ruby-lsp 0.23.23 → 0.26.0

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/exe/ruby-lsp +10 -4
  4. data/exe/ruby-lsp-check +0 -4
  5. data/exe/ruby-lsp-launcher +25 -11
  6. data/exe/ruby-lsp-test-exec +3 -15
  7. data/lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb +0 -1
  8. data/lib/rubocop/cop/ruby_lsp/use_register_with_handler_method.rb +0 -1
  9. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +7 -1
  10. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +1 -4
  11. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +10 -19
  12. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +27 -7
  13. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +12 -8
  14. data/lib/ruby_indexer/test/configuration_test.rb +1 -2
  15. data/lib/ruby_indexer/test/index_test.rb +36 -0
  16. data/lib/ruby_indexer/test/instance_variables_test.rb +24 -0
  17. data/lib/ruby_indexer/test/method_test.rb +17 -0
  18. data/lib/ruby_indexer/test/rbs_indexer_test.rb +2 -2
  19. data/lib/ruby_indexer/test/reference_finder_test.rb +79 -14
  20. data/lib/ruby_lsp/addon.rb +44 -15
  21. data/lib/ruby_lsp/base_server.rb +34 -26
  22. data/lib/ruby_lsp/document.rb +162 -52
  23. data/lib/ruby_lsp/erb_document.rb +8 -3
  24. data/lib/ruby_lsp/global_state.rb +21 -0
  25. data/lib/ruby_lsp/internal.rb +0 -2
  26. data/lib/ruby_lsp/listeners/completion.rb +14 -3
  27. data/lib/ruby_lsp/listeners/hover.rb +7 -0
  28. data/lib/ruby_lsp/listeners/inlay_hints.rb +5 -3
  29. data/lib/ruby_lsp/listeners/spec_style.rb +7 -8
  30. data/lib/ruby_lsp/listeners/test_discovery.rb +18 -15
  31. data/lib/ruby_lsp/listeners/test_style.rb +14 -13
  32. data/lib/ruby_lsp/requests/code_action_resolve.rb +3 -3
  33. data/lib/ruby_lsp/requests/code_lens.rb +9 -3
  34. data/lib/ruby_lsp/requests/completion.rb +1 -1
  35. data/lib/ruby_lsp/requests/definition.rb +1 -1
  36. data/lib/ruby_lsp/requests/discover_tests.rb +2 -2
  37. data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
  38. data/lib/ruby_lsp/requests/hover.rb +1 -1
  39. data/lib/ruby_lsp/requests/inlay_hints.rb +3 -3
  40. data/lib/ruby_lsp/requests/on_type_formatting.rb +1 -1
  41. data/lib/ruby_lsp/requests/prepare_rename.rb +1 -1
  42. data/lib/ruby_lsp/requests/references.rb +10 -6
  43. data/lib/ruby_lsp/requests/rename.rb +8 -6
  44. data/lib/ruby_lsp/requests/request.rb +6 -7
  45. data/lib/ruby_lsp/requests/selection_ranges.rb +1 -1
  46. data/lib/ruby_lsp/requests/show_syntax_tree.rb +1 -1
  47. data/lib/ruby_lsp/requests/signature_help.rb +1 -1
  48. data/lib/ruby_lsp/requests/support/common.rb +1 -3
  49. data/lib/ruby_lsp/requests/support/formatter.rb +16 -15
  50. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +2 -2
  51. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +13 -3
  52. data/lib/ruby_lsp/response_builders/response_builder.rb +6 -8
  53. data/lib/ruby_lsp/ruby_document.rb +10 -5
  54. data/lib/ruby_lsp/server.rb +89 -108
  55. data/lib/ruby_lsp/setup_bundler.rb +59 -25
  56. data/lib/ruby_lsp/static_docs.rb +1 -0
  57. data/lib/ruby_lsp/store.rb +0 -10
  58. data/lib/ruby_lsp/test_helper.rb +1 -4
  59. data/lib/ruby_lsp/test_reporters/lsp_reporter.rb +13 -8
  60. data/lib/ruby_lsp/test_reporters/minitest_reporter.rb +17 -4
  61. data/lib/ruby_lsp/utils.rb +47 -11
  62. data/static_docs/break.md +103 -0
  63. metadata +2 -16
  64. data/lib/ruby_lsp/load_sorbet.rb +0 -62
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module RubyLsp
5
- #: [ParseResultType = Prism::ParseResult]
5
+ #: [ParseResultType = Prism::ParseLexResult]
6
6
  class RubyDocument < Document
7
7
  METHODS_THAT_CHANGE_DECLARATIONS = [
8
8
  :private_constant,
@@ -26,7 +26,7 @@ module RubyLsp
26
26
  queue = node.child_nodes.compact #: Array[Prism::Node?]
27
27
  closest = node
28
28
  parent = nil #: Prism::Node?
29
- nesting_nodes = [] #: Array[(Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode | Prism::DefNode | Prism::BlockNode | Prism::LambdaNode | Prism::ProgramNode)] # rubocop:disable Layout/LineLength
29
+ nesting_nodes = [] #: Array[(Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode | Prism::DefNode | Prism::BlockNode | Prism::LambdaNode | Prism::ProgramNode)]
30
30
 
31
31
  nesting_nodes << node if node.is_a?(Prism::ProgramNode)
32
32
  call_node = nil #: Prism::CallNode?
@@ -129,11 +129,16 @@ module RubyLsp
129
129
  return false unless @needs_parsing
130
130
 
131
131
  @needs_parsing = false
132
- @parse_result = Prism.parse(@source)
132
+ @parse_result = Prism.parse_lex(@source)
133
133
  @code_units_cache = @parse_result.code_units_cache(@encoding)
134
134
  true
135
135
  end
136
136
 
137
+ #: -> Prism::ProgramNode
138
+ def ast
139
+ @parse_result.value.first
140
+ end
141
+
137
142
  # @override
138
143
  #: -> bool
139
144
  def syntax_error?
@@ -151,7 +156,7 @@ module RubyLsp
151
156
  start_position, end_position = find_index_by_position(range[:start], range[:end])
152
157
 
153
158
  desired_range = (start_position...end_position)
154
- queue = @parse_result.value.child_nodes.compact #: Array[Prism::Node?]
159
+ queue = ast.child_nodes.compact #: Array[Prism::Node?]
155
160
 
156
161
  until queue.empty?
157
162
  candidate = queue.shift
@@ -179,7 +184,7 @@ module RubyLsp
179
184
  char_position, _ = find_index_by_position(position)
180
185
 
181
186
  RubyDocument.locate(
182
- @parse_result.value,
187
+ ast,
183
188
  char_position,
184
189
  code_units_cache: @code_units_cache,
185
190
  node_types: node_types,
@@ -94,13 +94,10 @@ module RubyLsp
94
94
  id: message[:id],
95
95
  response:
96
96
  Addon.addons.map do |addon|
97
- version_method = addon.method(:version)
98
-
99
- # If the add-on doesn't define a `version` method, we'd be calling the abstract method defined by
100
- # Sorbet, which would raise an error.
101
- # Therefore, we only call the method if it's defined by the add-on itself
102
- if version_method.owner != Addon
103
- version = addon.version
97
+ version = begin
98
+ addon.version
99
+ rescue AbstractMethodInvokedError
100
+ nil
104
101
  end
105
102
 
106
103
  { name: addon.name, version: version, errored: addon.error? }
@@ -124,30 +121,18 @@ module RubyLsp
124
121
  end
125
122
  rescue DelegateRequestError
126
123
  send_message(Error.new(id: message[:id], code: DelegateRequestError::CODE, message: "DELEGATE_REQUEST"))
127
- rescue StandardError, LoadError => e
124
+ rescue StandardError, LoadError, SystemExit => e
128
125
  # If an error occurred in a request, we have to return an error response or else the editor will hang
129
126
  if message[:id]
130
127
  # If a document is deleted before we are able to process all of its enqueued requests, we will try to read it
131
128
  # from disk and it raise this error. This is expected, so we don't include the `data` attribute to avoid
132
129
  # reporting these to our telemetry
133
- case e
134
- when Store::NonExistingDocumentError
130
+ if e.is_a?(Store::NonExistingDocumentError)
135
131
  send_message(Error.new(
136
132
  id: message[:id],
137
133
  code: Constant::ErrorCodes::INVALID_PARAMS,
138
134
  message: e.full_message,
139
135
  ))
140
- when Document::LocationNotFoundError
141
- send_message(Error.new(
142
- id: message[:id],
143
- code: Constant::ErrorCodes::REQUEST_FAILED,
144
- message: <<~MESSAGE,
145
- Request #{message[:method]} failed to find the target position.
146
- The file might have been modified while the server was in the middle of searching for the target.
147
- If you experience this regularly, please report any findings and extra information on
148
- https://github.com/Shopify/ruby-lsp/issues/2446
149
- MESSAGE
150
- ))
151
136
  else
152
137
  send_message(Error.new(
153
138
  id: message[:id],
@@ -182,6 +167,7 @@ module RubyLsp
182
167
  return if @setup_error
183
168
 
184
169
  errors = Addon.load_addons(@global_state, @outgoing_queue, include_project_addons: include_project_addons)
170
+ return if test_mode?
185
171
 
186
172
  if errors.any?
187
173
  send_log_message(
@@ -194,21 +180,13 @@ module RubyLsp
194
180
 
195
181
  if errored_addons.any?
196
182
  send_message(
197
- Notification.new(
198
- method: "window/showMessage",
199
- params: Interface::ShowMessageParams.new(
200
- type: Constant::MessageType::WARNING,
201
- message: "Error loading add-ons:\n\n#{errored_addons.map(&:formatted_errors).join("\n\n")}",
202
- ),
183
+ Notification.window_show_message(
184
+ "Error loading add-ons:\n\n#{errored_addons.map(&:formatted_errors).join("\n\n")}",
185
+ type: Constant::MessageType::WARNING,
203
186
  ),
204
187
  )
205
188
 
206
- unless @test_mode
207
- send_log_message(
208
- errored_addons.map(&:errors_details).join("\n\n"),
209
- type: Constant::MessageType::WARNING,
210
- )
211
- end
189
+ send_log_message(errored_addons.map(&:errors_details).join("\n\n"), type: Constant::MessageType::WARNING)
212
190
  end
213
191
  end
214
192
 
@@ -224,10 +202,6 @@ module RubyLsp
224
202
 
225
203
  configured_features = options.dig(:initializationOptions, :enabledFeatures)
226
204
 
227
- configured_hints = options.dig(:initializationOptions, :featuresConfiguration, :inlayHint)
228
- @store.features_configuration.dig(:inlayHint) #: as !nil
229
- .configuration.merge!(configured_hints) if configured_hints
230
-
231
205
  enabled_features = case configured_features
232
206
  when Array
233
207
  # If the configuration is using an array, then absent features are disabled and present ones are enabled. That's
@@ -381,56 +355,53 @@ module RubyLsp
381
355
 
382
356
  perform_initial_indexing
383
357
  check_formatter_is_available
358
+ update_server if @global_state.enabled_feature?(:launcher)
384
359
  end
385
360
 
386
361
  #: (Hash[Symbol, untyped] message) -> void
387
362
  def text_document_did_open(message)
388
- @global_state.synchronize do
389
- text_document = message.dig(:params, :textDocument)
390
- language_id = case text_document[:languageId]
391
- when "erb", "eruby"
392
- :erb
393
- when "rbs"
394
- :rbs
395
- else
396
- :ruby
397
- end
363
+ text_document = message.dig(:params, :textDocument)
364
+ language_id = case text_document[:languageId]
365
+ when "erb", "eruby"
366
+ :erb
367
+ when "rbs"
368
+ :rbs
369
+ else
370
+ :ruby
371
+ end
398
372
 
399
- document = @store.set(
400
- uri: text_document[:uri],
401
- source: text_document[:text],
402
- version: text_document[:version],
403
- language_id: language_id,
404
- )
373
+ document = @store.set(
374
+ uri: text_document[:uri],
375
+ source: text_document[:text],
376
+ version: text_document[:version],
377
+ language_id: language_id,
378
+ )
405
379
 
406
- if document.past_expensive_limit? && text_document[:uri].scheme == "file"
407
- log_message = <<~MESSAGE
408
- The file #{text_document[:uri].path} is too long. For performance reasons, semantic highlighting and
409
- diagnostics will be disabled.
410
- MESSAGE
380
+ if document.past_expensive_limit? && text_document[:uri].scheme == "file"
381
+ log_message = <<~MESSAGE
382
+ The file #{text_document[:uri].path} is too long. For performance reasons, semantic highlighting and
383
+ diagnostics will be disabled.
384
+ MESSAGE
411
385
 
412
- send_message(
413
- Notification.new(
414
- method: "window/logMessage",
415
- params: Interface::LogMessageParams.new(
416
- type: Constant::MessageType::WARNING,
417
- message: log_message,
418
- ),
386
+ send_message(
387
+ Notification.new(
388
+ method: "window/logMessage",
389
+ params: Interface::LogMessageParams.new(
390
+ type: Constant::MessageType::WARNING,
391
+ message: log_message,
419
392
  ),
420
- )
421
- end
393
+ ),
394
+ )
422
395
  end
423
396
  end
424
397
 
425
398
  #: (Hash[Symbol, untyped] message) -> void
426
399
  def text_document_did_close(message)
427
- @global_state.synchronize do
428
- uri = message.dig(:params, :textDocument, :uri)
429
- @store.delete(uri)
400
+ uri = message.dig(:params, :textDocument, :uri)
401
+ @store.delete(uri)
430
402
 
431
- # Clear diagnostics for the closed file, so that they no longer appear in the problems tab
432
- send_message(Notification.publish_diagnostics(uri.to_s, []))
433
- end
403
+ # Clear diagnostics for the closed file, so that they no longer appear in the problems tab
404
+ send_message(Notification.publish_diagnostics(uri.to_s, []))
434
405
  end
435
406
 
436
407
  #: (Hash[Symbol, untyped] message) -> void
@@ -438,9 +409,7 @@ module RubyLsp
438
409
  params = message[:params]
439
410
  text_document = params[:textDocument]
440
411
 
441
- @global_state.synchronize do
442
- @store.push_edits(uri: text_document[:uri], edits: params[:contentChanges], version: text_document[:version])
443
- end
412
+ @store.push_edits(uri: text_document[:uri], edits: params[:contentChanges], version: text_document[:version])
444
413
  end
445
414
 
446
415
  #: (Hash[Symbol, untyped] message) -> void
@@ -494,8 +463,8 @@ module RubyLsp
494
463
  document_symbol = Requests::DocumentSymbol.new(uri, dispatcher)
495
464
  document_link = Requests::DocumentLink.new(uri, parse_result.comments, dispatcher)
496
465
  inlay_hint = Requests::InlayHints.new(
466
+ @global_state,
497
467
  document,
498
- @store.features_configuration.dig(:inlayHint), #: as !nil
499
468
  dispatcher,
500
469
  )
501
470
 
@@ -513,12 +482,12 @@ module RubyLsp
513
482
  index.delete(uri, skip_require_paths_tree: true)
514
483
  RubyIndexer::DeclarationListener.new(index, dispatcher, parse_result, uri, collect_comments: true)
515
484
  code_lens = Requests::CodeLens.new(@global_state, document, dispatcher)
516
- dispatcher.dispatch(parse_result.value)
485
+ dispatcher.dispatch(document.ast)
517
486
  end
518
487
  end
519
488
  else
520
489
  code_lens = Requests::CodeLens.new(@global_state, document, dispatcher)
521
- dispatcher.dispatch(parse_result.value)
490
+ dispatcher.dispatch(document.ast)
522
491
  end
523
492
 
524
493
  # Store all responses retrieve in this round of visits in the cache and then return the response for the request
@@ -557,7 +526,7 @@ module RubyLsp
557
526
 
558
527
  dispatcher = Prism::Dispatcher.new
559
528
  semantic_highlighting = Requests::SemanticHighlighting.new(@global_state, dispatcher, document, nil)
560
- dispatcher.visit(document.parse_result.value)
529
+ dispatcher.visit(document.ast)
561
530
 
562
531
  send_message(Result.new(id: message[:id], response: semantic_highlighting.perform))
563
532
  end
@@ -583,7 +552,7 @@ module RubyLsp
583
552
  document,
584
553
  message.dig(:params, :previousResultId),
585
554
  )
586
- dispatcher.visit(document.parse_result.value)
555
+ dispatcher.visit(document.ast)
587
556
  send_message(Result.new(id: message[:id], response: request.perform))
588
557
  end
589
558
 
@@ -612,7 +581,7 @@ module RubyLsp
612
581
  nil,
613
582
  range: range.dig(:start, :line)..range.dig(:end, :line),
614
583
  )
615
- dispatcher.visit(document.parse_result.value)
584
+ dispatcher.visit(document.ast)
616
585
  send_message(Result.new(id: message[:id], response: request.perform))
617
586
  end
618
587
 
@@ -704,7 +673,7 @@ module RubyLsp
704
673
  end
705
674
 
706
675
  request = Requests::DocumentHighlight.new(@global_state, document, params[:position], dispatcher)
707
- dispatcher.dispatch(document.parse_result.value)
676
+ dispatcher.dispatch(document.ast)
708
677
  send_message(Result.new(id: message[:id], response: request.perform))
709
678
  end
710
679
 
@@ -842,7 +811,6 @@ module RubyLsp
842
811
  return
843
812
  end
844
813
 
845
- hints_configurations = @store.features_configuration.dig(:inlayHint) #: as !nil
846
814
  dispatcher = Prism::Dispatcher.new
847
815
 
848
816
  unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument)
@@ -850,8 +818,8 @@ module RubyLsp
850
818
  return
851
819
  end
852
820
 
853
- request = Requests::InlayHints.new(document, hints_configurations, dispatcher)
854
- dispatcher.visit(document.parse_result.value)
821
+ request = Requests::InlayHints.new(@global_state, document, dispatcher)
822
+ dispatcher.visit(document.ast)
855
823
  result = request.perform
856
824
  document.cache_set("textDocument/inlayHint", result)
857
825
 
@@ -1124,11 +1092,7 @@ module RubyLsp
1124
1092
  @global_state.register_formatter("rubocop_internal", Requests::Support::RuboCopFormatter.new)
1125
1093
 
1126
1094
  # Clear all document caches for pull diagnostics
1127
- @global_state.synchronize do
1128
- @store.each do |_uri, document|
1129
- document.clear_cache("textDocument/diagnostic")
1130
- end
1131
- end
1095
+ @store.each { |_uri, document| document.clear_cache("textDocument/diagnostic") }
1132
1096
 
1133
1097
  # Request a pull diagnostic refresh from the editor
1134
1098
  if @global_state.client_capabilities.supports_diagnostic_refresh
@@ -1243,7 +1207,7 @@ module RubyLsp
1243
1207
  }
1244
1208
  end
1245
1209
  end
1246
- rescue Bundler::GemNotFound
1210
+ rescue Bundler::GemNotFound, Bundler::GemfileNotFound
1247
1211
  []
1248
1212
  end
1249
1213
 
@@ -1426,8 +1390,38 @@ module RubyLsp
1426
1390
 
1427
1391
  # We compose the bundle in a thread so that the LSP continues to work while we're checking for its validity. Once
1428
1392
  # we return the response back to the editor, then the restart is triggered
1393
+ launch_bundle_compose("Recomposing the bundle ahead of restart") do |stderr, status|
1394
+ if status&.exitstatus == 0
1395
+ # Create a signal for the restart that it can skip composing the bundle and launch directly
1396
+ FileUtils.touch(already_composed_path)
1397
+ send_message(Result.new(id: id, response: { success: true }))
1398
+ else
1399
+ # This special error code makes the extension avoid restarting in case we already know that the composed
1400
+ # bundle is not valid
1401
+ send_message(
1402
+ Error.new(id: id, code: BUNDLE_COMPOSE_FAILED_CODE, message: "Failed to compose bundle\n#{stderr}"),
1403
+ )
1404
+ end
1405
+ end
1406
+ end
1407
+
1408
+ #: -> void
1409
+ def update_server
1410
+ return unless File.exist?(File.join(@global_state.workspace_path, ".ruby-lsp", "needs_update"))
1411
+
1412
+ launch_bundle_compose("Trying to update server") do |stderr, status|
1413
+ if status&.exitstatus == 0
1414
+ send_log_message("Successfully updated the server")
1415
+ else
1416
+ send_log_message("Failed to update server\n#{stderr}", type: Constant::MessageType::ERROR)
1417
+ end
1418
+ end
1419
+ end
1420
+
1421
+ #: (String) { (IO, Process::Status?) -> void } -> Thread
1422
+ def launch_bundle_compose(log, &block)
1429
1423
  Thread.new do
1430
- send_log_message("Recomposing the bundle ahead of restart")
1424
+ send_log_message(log)
1431
1425
 
1432
1426
  _stdout, stderr, status = Bundler.with_unbundled_env do
1433
1427
  Open3.capture3(
@@ -1442,17 +1436,7 @@ module RubyLsp
1442
1436
  )
1443
1437
  end
1444
1438
 
1445
- if status&.exitstatus == 0
1446
- # Create a signal for the restart that it can skip composing the bundle and launch directly
1447
- FileUtils.touch(already_composed_path)
1448
- send_message(Result.new(id: id, response: { success: true }))
1449
- else
1450
- # This special error code makes the extension avoid restarting in case we already know that the composed
1451
- # bundle is not valid
1452
- send_message(
1453
- Error.new(id: id, code: BUNDLE_COMPOSE_FAILED_CODE, message: "Failed to compose bundle\n#{stderr}"),
1454
- )
1455
- end
1439
+ block.call(stderr, status)
1456
1440
  end
1457
1441
  end
1458
1442
 
@@ -1509,10 +1493,7 @@ module RubyLsp
1509
1493
 
1510
1494
  send_message(Result.new(
1511
1495
  id: message[:id],
1512
- response: {
1513
- commands: commands,
1514
- reporterPaths: [Listeners::TestStyle::MINITEST_REPORTER_PATH, Listeners::TestStyle::TEST_UNIT_REPORTER_PATH],
1515
- },
1496
+ response: { commands: commands },
1516
1497
  ))
1517
1498
  end
1518
1499
 
@@ -1,7 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "sorbet-runtime"
5
4
  require "bundler"
6
5
  require "bundler/cli"
7
6
  require "bundler/cli/install"
@@ -12,19 +11,24 @@ require "digest"
12
11
  require "time"
13
12
  require "uri"
14
13
 
15
- # This file is a script that will configure a composed bundle for the Ruby LSP. The composed bundle allows developers to use
16
- # the Ruby LSP without including the gem in their application's Gemfile while at the same time giving us access to the
17
- # exact locked versions of dependencies.
14
+ # This file is a script that will configure a composed bundle for the Ruby LSP. The composed bundle allows developers to
15
+ # use the Ruby LSP without including the gem in their application's Gemfile while at the same time giving us access to
16
+ # the exact locked versions of dependencies.
18
17
 
19
18
  Bundler.ui.level = :silent
20
19
 
21
20
  module RubyLsp
22
21
  class SetupBundler
23
- extend T::Sig
24
-
25
22
  class BundleNotLocked < StandardError; end
26
23
  class BundleInstallFailure < StandardError; end
27
24
 
25
+ module ThorPatch
26
+ #: -> IO
27
+ def stdout
28
+ $stderr
29
+ end
30
+ end
31
+
28
32
  FOUR_HOURS = 4 * 60 * 60 #: Integer
29
33
 
30
34
  #: (String project_path, **untyped options) -> void
@@ -61,6 +65,7 @@ module RubyLsp
61
65
  @bundler_version = bundler_version #: Gem::Version?
62
66
  @rails_app = rails_app? #: bool
63
67
  @retry = false #: bool
68
+ @needs_update_path = @custom_dir + "needs_update" #: Pathname
64
69
  end
65
70
 
66
71
  # Sets up the composed bundle and returns the `BUNDLE_GEMFILE`, `BUNDLE_PATH` and `BUNDLE_APP_CONFIG` that should be
@@ -229,6 +234,14 @@ module RubyLsp
229
234
  # If no error occurred, then clear previous errors
230
235
  @error_path.delete if @error_path.exist?
231
236
  $stderr.puts("Ruby LSP> Composed bundle installation complete")
237
+ rescue Errno::EPIPE
238
+ # If the $stderr pipe was closed by the client, for example when closing the editor during running bundle
239
+ # install, we don't want to write the error to a file or else we will report to telemetry on the next launch and
240
+ # it does not represent an actual error.
241
+ #
242
+ # This situation may happen because while running bundle install, the server is not yet ready to receive
243
+ # shutdown requests and we may continue doing work until the process is killed.
244
+ @error_path.delete if @error_path.exist?
232
245
  rescue => e
233
246
  # Write the error object to a file so that we can read it from the parent process
234
247
  @error_path.write(Marshal.dump(e))
@@ -256,19 +269,50 @@ module RubyLsp
256
269
  #: (Hash[String, String] env, ?force_install: bool) -> Hash[String, String]
257
270
  def run_bundle_install_directly(env, force_install: false)
258
271
  RubyVM::YJIT.enable if defined?(RubyVM::YJIT.enable)
272
+ return update(env) if @needs_update_path.exist?
259
273
 
260
274
  # The ENV can only be merged after checking if an update is required because we depend on the original value of
261
275
  # ENV["BUNDLE_GEMFILE"], which gets overridden after the merge
262
- should_update = should_bundle_update?
263
- ENV #: as untyped
264
- .merge!(env)
276
+ FileUtils.touch(@needs_update_path) if should_bundle_update?
277
+ ENV.merge!(env)
265
278
 
266
- unless should_update && !force_install
267
- Bundler::CLI::Install.new({ "no-cache" => true }).run
268
- correct_relative_remote_paths if @custom_lockfile.exist?
269
- return env
279
+ $stderr.puts("Ruby LSP> Checking if the composed bundle is satisfied...")
280
+ missing_gems = bundle_check
281
+
282
+ unless missing_gems.empty?
283
+ $stderr.puts(<<~MESSAGE)
284
+ Ruby LSP> Running bundle install because the following gems are not installed:
285
+ #{missing_gems.map { |g| "#{g.name}: #{g.version}" }.join("\n")}
286
+ MESSAGE
287
+
288
+ bundle_install
270
289
  end
271
290
 
291
+ $stderr.puts("Ruby LSP> Bundle already satisfied")
292
+ env
293
+ rescue => e
294
+ $stderr.puts("Ruby LSP> Running bundle install because #{e.message}")
295
+ bundle_install
296
+ env
297
+ end
298
+
299
+ # Essentially the same as bundle check, but simplified
300
+ #: -> Array[Gem::Specification]
301
+ def bundle_check
302
+ definition = Bundler.definition
303
+ definition.validate_runtime!
304
+ definition.check!
305
+ definition.missing_specs
306
+ end
307
+
308
+ #: -> void
309
+ def bundle_install
310
+ Bundler::CLI::Install.new({ "no-cache" => true }).run
311
+ correct_relative_remote_paths if @custom_lockfile.exist?
312
+ end
313
+
314
+ #: (Hash[String, String]) -> Hash[String, String]
315
+ def update(env)
272
316
  # Try to auto upgrade the gems we depend on, unless they are in the Gemfile as that would result in undesired
273
317
  # source control changes
274
318
  gems = ["ruby-lsp", "debug", "prism"].reject { |dep| @dependencies[dep] }
@@ -276,11 +320,9 @@ module RubyLsp
276
320
 
277
321
  Bundler::CLI::Update.new({ conservative: true }, gems).run
278
322
  correct_relative_remote_paths if @custom_lockfile.exist?
323
+ @needs_update_path.delete
279
324
  @last_updated_path.write(Time.now.iso8601)
280
325
  env
281
- rescue Bundler::GemNotFound, Bundler::GitError
282
- # If a gem is not installed, skip the upgrade and try to install it with a single retry
283
- @retry ? env : run_bundle_install_directly(env, force_install: true)
284
326
  end
285
327
 
286
328
  #: (Hash[String, String] env) -> Hash[String, String]
@@ -440,15 +482,7 @@ module RubyLsp
440
482
  def patch_thor_to_print_progress_to_stderr!
441
483
  return unless defined?(Bundler::Thor::Shell::Basic)
442
484
 
443
- Bundler::Thor::Shell::Basic.prepend(Module.new do
444
- extend T::Sig
445
-
446
- sig { returns(IO) }
447
- def stdout
448
- $stderr
449
- end
450
- end)
451
-
485
+ Bundler::Thor::Shell::Basic.prepend(ThorPatch)
452
486
  Bundler.ui.level = :info
453
487
  end
454
488
  end
@@ -14,6 +14,7 @@ module RubyLsp
14
14
 
15
15
  # A map of keyword => short documentation to be displayed on hover or completion
16
16
  KEYWORD_DOCS = {
17
+ "break" => "Terminates the execution of a block or loop",
17
18
  "yield" => "Invokes the passed block with the given arguments",
18
19
  }.freeze #: Hash[String, String]
19
20
  end
@@ -5,9 +5,6 @@ module RubyLsp
5
5
  class Store
6
6
  class NonExistingDocumentError < StandardError; end
7
7
 
8
- #: Hash[Symbol, RequestConfig]
9
- attr_accessor :features_configuration
10
-
11
8
  #: String
12
9
  attr_accessor :client_name
13
10
 
@@ -15,13 +12,6 @@ module RubyLsp
15
12
  def initialize(global_state)
16
13
  @global_state = global_state
17
14
  @state = {} #: Hash[String, Document[untyped]]
18
- @features_configuration = {
19
- inlayHint: RequestConfig.new({
20
- enableAll: false,
21
- implicitRescue: false,
22
- implicitHashValue: false,
23
- }),
24
- } #: Hash[Symbol, RequestConfig]
25
15
  @client_name = "Unknown" #: String
26
16
  end
27
17
 
@@ -4,13 +4,10 @@
4
4
  # NOTE: This module is intended to be used by addons for writing their own tests, so keep that in mind if changing.
5
5
 
6
6
  module RubyLsp
7
+ # @requires_ancestor: Kernel
7
8
  module TestHelper
8
9
  class TestError < StandardError; end
9
10
 
10
- extend T::Helpers
11
-
12
- requires_ancestor { Kernel }
13
-
14
11
  #: [T] (?String? source, ?URI::Generic uri, ?stub_no_typechecker: bool, ?load_addons: bool) { (RubyLsp::Server server, URI::Generic uri) -> T } -> T
15
12
  def with_server(source = nil, uri = Kernel.URI("file:///fake.rb"), stub_no_typechecker: false, load_addons: true,
16
13
  &block)
@@ -12,16 +12,23 @@ module RubyLsp
12
12
  class LspReporter
13
13
  include Singleton
14
14
 
15
- #: bool
16
- attr_reader :invoked_shutdown
15
+ # https://code.visualstudio.com/api/references/vscode-api#Position
16
+ #: type position = { line: Integer, character: Integer }
17
+
18
+ # https://code.visualstudio.com/api/references/vscode-api#Range
19
+ #: type range = { start: position, end: position }
20
+
21
+ # https://code.visualstudio.com/api/references/vscode-api#BranchCoverage
22
+ #: type branch_coverage = { executed: Integer, label: String, location: range }
23
+
24
+ # https://code.visualstudio.com/api/references/vscode-api#StatementCoverage
25
+ #: type statement_coverage = { executed: Integer, location: position, branches: Array[branch_coverage] }
17
26
 
18
27
  #: -> void
19
28
  def initialize
20
29
  dir_path = File.join(Dir.tmpdir, "ruby-lsp")
21
30
  FileUtils.mkdir_p(dir_path)
22
31
 
23
- # Remove in 1 month once updates have rolled out
24
- legacy_port_path = File.join(dir_path, "test_reporter_port")
25
32
  port_db_path = File.join(dir_path, "test_reporter_port_db.json")
26
33
  port = ENV["RUBY_LSP_REPORTER_PORT"]
27
34
 
@@ -32,8 +39,6 @@ module RubyLsp
32
39
  elsif File.exist?(port_db_path)
33
40
  db = JSON.load_file(port_db_path)
34
41
  TCPSocket.new("localhost", db[Dir.pwd])
35
- elsif File.exist?(legacy_port_path)
36
- TCPSocket.new("localhost", File.read(legacy_port_path))
37
42
  else
38
43
  # For tests that don't spawn the TCP server
39
44
  require "stringio"
@@ -129,7 +134,7 @@ module RubyLsp
129
134
  # ["Foo", :bar, 6, 21, 6, 65] => 0
130
135
  # }
131
136
  # }
132
- #: -> Hash[String, StatementCoverage]
137
+ #: -> Hash[String, statement_coverage]
133
138
  def gather_coverage_results
134
139
  # Ignore coverage results inside dependencies
135
140
  bundle_path = Bundler.bundle_path.to_s
@@ -187,7 +192,7 @@ module RubyLsp
187
192
 
188
193
  #: -> void
189
194
  def at_exit
190
- internal_shutdown unless invoked_shutdown
195
+ internal_shutdown unless @invoked_shutdown
191
196
  end
192
197
 
193
198
  class << self