ruby-lsp 0.23.21 → 0.25.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/lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb +0 -1
  7. data/lib/rubocop/cop/ruby_lsp/use_register_with_handler_method.rb +0 -1
  8. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +7 -1
  9. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +1 -4
  10. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +9 -19
  11. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +18 -7
  12. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +2 -2
  13. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +12 -8
  14. data/lib/ruby_indexer/test/configuration_test.rb +1 -1
  15. data/lib/ruby_indexer/test/index_test.rb +24 -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 +29 -15
  21. data/lib/ruby_lsp/base_server.rb +14 -12
  22. data/lib/ruby_lsp/document.rb +158 -46
  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 +9 -1
  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 +63 -20
  30. data/lib/ruby_lsp/listeners/test_discovery.rb +18 -15
  31. data/lib/ruby_lsp/listeners/test_style.rb +21 -10
  32. data/lib/ruby_lsp/requests/code_action_resolve.rb +3 -3
  33. data/lib/ruby_lsp/requests/code_lens.rb +14 -5
  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 +9 -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 +50 -49
  55. data/lib/ruby_lsp/setup_bundler.rb +51 -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 -5
  60. data/lib/ruby_lsp/test_reporters/minitest_reporter.rb +43 -4
  61. data/lib/ruby_lsp/utils.rb +47 -11
  62. data/static_docs/break.md +103 -0
  63. metadata +4 -18
  64. data/lib/ruby_lsp/load_sorbet.rb +0 -62
@@ -4,25 +4,26 @@
4
4
  module RubyLsp
5
5
  module Requests
6
6
  module Support
7
+ # Empty module to avoid the runtime component. This is an interface defined in sorbet/rbi/shims/ruby_lsp.rbi
8
+ # @interface
7
9
  module Formatter
8
- extend T::Sig
9
- extend T::Helpers
10
-
11
- interface!
12
-
13
- sig { abstract.params(uri: URI::Generic, document: RubyDocument).returns(T.nilable(String)) }
14
- def run_formatting(uri, document); end
10
+ # @abstract
11
+ #: (URI::Generic, RubyLsp::RubyDocument) -> String?
12
+ def run_formatting(uri, document)
13
+ raise AbstractMethodInvokedError
14
+ end
15
15
 
16
- sig { abstract.params(uri: URI::Generic, source: String, base_indentation: Integer).returns(T.nilable(String)) }
17
- def run_range_formatting(uri, source, base_indentation); end
16
+ # @abstract
17
+ #: (URI::Generic, String, Integer) -> String?
18
+ def run_range_formatting(uri, source, base_indentation)
19
+ raise AbstractMethodInvokedError
20
+ end
18
21
 
19
- sig do
20
- abstract.params(
21
- uri: URI::Generic,
22
- document: RubyDocument,
23
- ).returns(T.nilable(T::Array[Interface::Diagnostic]))
22
+ # @abstract
23
+ #: (URI::Generic, RubyLsp::RubyDocument) -> Array[Interface::Diagnostic]?
24
+ def run_diagnostic(uri, document)
25
+ raise AbstractMethodInvokedError
24
26
  end
25
- def run_diagnostic(uri, document); end
26
27
  end
27
28
  end
28
29
  end
@@ -24,7 +24,7 @@ module RubyLsp
24
24
  filename = uri.to_standardized_path || uri.opaque #: as !nil
25
25
 
26
26
  # Invoke RuboCop with just this file in `paths`
27
- @format_runner.run(filename, document.source)
27
+ @format_runner.run(filename, document.source, document.parse_result)
28
28
  @format_runner.formatted_source
29
29
  end
30
30
 
@@ -40,7 +40,7 @@ module RubyLsp
40
40
  def run_diagnostic(uri, document)
41
41
  filename = uri.to_standardized_path || uri.opaque #: as !nil
42
42
  # Invoke RuboCop with just this file in `paths`
43
- @diagnostic_runner.run(filename, document.source)
43
+ @diagnostic_runner.run(filename, document.source, document.parse_result)
44
44
 
45
45
  @diagnostic_runner.offenses.map do |offense|
46
46
  Support::RuboCopDiagnostic.new(
@@ -81,6 +81,7 @@ module RubyLsp
81
81
  @offenses = [] #: Array[::RuboCop::Cop::Offense]
82
82
  @errors = [] #: Array[String]
83
83
  @warnings = [] #: Array[String]
84
+ @prism_result = nil #: Prism::ParseLexResult?
84
85
 
85
86
  args += DEFAULT_ARGS
86
87
  rubocop_options = ::RuboCop::Options.new.parse(args).first
@@ -92,14 +93,15 @@ module RubyLsp
92
93
  super(rubocop_options, config_store)
93
94
  end
94
95
 
95
- #: (String path, String contents) -> void
96
- def run(path, contents)
96
+ #: (String, String, Prism::ParseLexResult) -> void
97
+ def run(path, contents, prism_result)
97
98
  # Clear Runner state between runs since we get a single instance of this class
98
99
  # on every use site.
99
100
  @errors = []
100
101
  @warnings = []
101
102
  @offenses = []
102
103
  @options[:stdin] = contents
104
+ @prism_result = prism_result
103
105
 
104
106
  super([path])
105
107
 
@@ -111,7 +113,11 @@ module RubyLsp
111
113
  rescue ::RuboCop::ValidationError => error
112
114
  raise ConfigurationError, error.message
113
115
  rescue StandardError => error
114
- raise InternalRuboCopError, error
116
+ # Maintain the original backtrace so that debugging cops that are breaking is easier, but re-raise as a
117
+ # different error class
118
+ internal_error = InternalRuboCopError.new(error)
119
+ internal_error.set_backtrace(error.backtrace)
120
+ raise internal_error
115
121
  end
116
122
 
117
123
  #: -> String
@@ -3,15 +3,13 @@
3
3
 
4
4
  module RubyLsp
5
5
  module ResponseBuilders
6
+ # @abstract
6
7
  class ResponseBuilder
7
- extend T::Sig
8
- extend T::Helpers
9
- extend T::Generic
10
-
11
- abstract!
12
-
13
- sig { abstract.returns(T.anything) }
14
- def response; end
8
+ # @abstract
9
+ #: -> top
10
+ def response
11
+ raise AbstractMethodInvokedError
12
+ end
15
13
  end
16
14
  end
17
15
  end
@@ -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],
@@ -224,10 +209,6 @@ module RubyLsp
224
209
 
225
210
  configured_features = options.dig(:initializationOptions, :enabledFeatures)
226
211
 
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
212
  enabled_features = case configured_features
232
213
  when Array
233
214
  # If the configuration is using an array, then absent features are disabled and present ones are enabled. That's
@@ -381,6 +362,7 @@ module RubyLsp
381
362
 
382
363
  perform_initial_indexing
383
364
  check_formatter_is_available
365
+ update_server if @global_state.enabled_feature?(:launcher)
384
366
  end
385
367
 
386
368
  #: (Hash[Symbol, untyped] message) -> void
@@ -494,8 +476,8 @@ module RubyLsp
494
476
  document_symbol = Requests::DocumentSymbol.new(uri, dispatcher)
495
477
  document_link = Requests::DocumentLink.new(uri, parse_result.comments, dispatcher)
496
478
  inlay_hint = Requests::InlayHints.new(
479
+ @global_state,
497
480
  document,
498
- @store.features_configuration.dig(:inlayHint), #: as !nil
499
481
  dispatcher,
500
482
  )
501
483
 
@@ -512,13 +494,13 @@ module RubyLsp
512
494
  @global_state.index.handle_change(uri) do |index|
513
495
  index.delete(uri, skip_require_paths_tree: true)
514
496
  RubyIndexer::DeclarationListener.new(index, dispatcher, parse_result, uri, collect_comments: true)
515
- code_lens = Requests::CodeLens.new(@global_state, uri, dispatcher)
516
- dispatcher.dispatch(parse_result.value)
497
+ code_lens = Requests::CodeLens.new(@global_state, document, dispatcher)
498
+ dispatcher.dispatch(document.ast)
517
499
  end
518
500
  end
519
501
  else
520
- code_lens = Requests::CodeLens.new(@global_state, uri, dispatcher)
521
- dispatcher.dispatch(parse_result.value)
502
+ code_lens = Requests::CodeLens.new(@global_state, document, dispatcher)
503
+ dispatcher.dispatch(document.ast)
522
504
  end
523
505
 
524
506
  # Store all responses retrieve in this round of visits in the cache and then return the response for the request
@@ -557,7 +539,7 @@ module RubyLsp
557
539
 
558
540
  dispatcher = Prism::Dispatcher.new
559
541
  semantic_highlighting = Requests::SemanticHighlighting.new(@global_state, dispatcher, document, nil)
560
- dispatcher.visit(document.parse_result.value)
542
+ dispatcher.visit(document.ast)
561
543
 
562
544
  send_message(Result.new(id: message[:id], response: semantic_highlighting.perform))
563
545
  end
@@ -583,7 +565,7 @@ module RubyLsp
583
565
  document,
584
566
  message.dig(:params, :previousResultId),
585
567
  )
586
- dispatcher.visit(document.parse_result.value)
568
+ dispatcher.visit(document.ast)
587
569
  send_message(Result.new(id: message[:id], response: request.perform))
588
570
  end
589
571
 
@@ -612,7 +594,7 @@ module RubyLsp
612
594
  nil,
613
595
  range: range.dig(:start, :line)..range.dig(:end, :line),
614
596
  )
615
- dispatcher.visit(document.parse_result.value)
597
+ dispatcher.visit(document.ast)
616
598
  send_message(Result.new(id: message[:id], response: request.perform))
617
599
  end
618
600
 
@@ -704,7 +686,7 @@ module RubyLsp
704
686
  end
705
687
 
706
688
  request = Requests::DocumentHighlight.new(@global_state, document, params[:position], dispatcher)
707
- dispatcher.dispatch(document.parse_result.value)
689
+ dispatcher.dispatch(document.ast)
708
690
  send_message(Result.new(id: message[:id], response: request.perform))
709
691
  end
710
692
 
@@ -842,7 +824,6 @@ module RubyLsp
842
824
  return
843
825
  end
844
826
 
845
- hints_configurations = @store.features_configuration.dig(:inlayHint) #: as !nil
846
827
  dispatcher = Prism::Dispatcher.new
847
828
 
848
829
  unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument)
@@ -850,8 +831,8 @@ module RubyLsp
850
831
  return
851
832
  end
852
833
 
853
- request = Requests::InlayHints.new(document, hints_configurations, dispatcher)
854
- dispatcher.visit(document.parse_result.value)
834
+ request = Requests::InlayHints.new(@global_state, document, dispatcher)
835
+ dispatcher.visit(document.ast)
855
836
  result = request.perform
856
837
  document.cache_set("textDocument/inlayHint", result)
857
838
 
@@ -1426,8 +1407,38 @@ module RubyLsp
1426
1407
 
1427
1408
  # We compose the bundle in a thread so that the LSP continues to work while we're checking for its validity. Once
1428
1409
  # we return the response back to the editor, then the restart is triggered
1410
+ launch_bundle_compose("Recomposing the bundle ahead of restart") do |stderr, status|
1411
+ if status&.exitstatus == 0
1412
+ # Create a signal for the restart that it can skip composing the bundle and launch directly
1413
+ FileUtils.touch(already_composed_path)
1414
+ send_message(Result.new(id: id, response: { success: true }))
1415
+ else
1416
+ # This special error code makes the extension avoid restarting in case we already know that the composed
1417
+ # bundle is not valid
1418
+ send_message(
1419
+ Error.new(id: id, code: BUNDLE_COMPOSE_FAILED_CODE, message: "Failed to compose bundle\n#{stderr}"),
1420
+ )
1421
+ end
1422
+ end
1423
+ end
1424
+
1425
+ #: -> void
1426
+ def update_server
1427
+ return unless File.exist?(File.join(@global_state.workspace_path, ".ruby-lsp", "needs_update"))
1428
+
1429
+ launch_bundle_compose("Trying to update server") do |stderr, status|
1430
+ if status&.exitstatus == 0
1431
+ send_log_message("Successfully updated the server")
1432
+ else
1433
+ send_log_message("Failed to update server\n#{stderr}", type: Constant::MessageType::ERROR)
1434
+ end
1435
+ end
1436
+ end
1437
+
1438
+ #: (String) { (IO, Process::Status?) -> void } -> Thread
1439
+ def launch_bundle_compose(log, &block)
1429
1440
  Thread.new do
1430
- send_log_message("Recomposing the bundle ahead of restart")
1441
+ send_log_message(log)
1431
1442
 
1432
1443
  _stdout, stderr, status = Bundler.with_unbundled_env do
1433
1444
  Open3.capture3(
@@ -1442,17 +1453,7 @@ module RubyLsp
1442
1453
  )
1443
1454
  end
1444
1455
 
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
1456
+ block.call(stderr, status)
1456
1457
  end
1457
1458
  end
1458
1459
 
@@ -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
@@ -256,19 +261,50 @@ module RubyLsp
256
261
  #: (Hash[String, String] env, ?force_install: bool) -> Hash[String, String]
257
262
  def run_bundle_install_directly(env, force_install: false)
258
263
  RubyVM::YJIT.enable if defined?(RubyVM::YJIT.enable)
264
+ return update(env) if @needs_update_path.exist?
259
265
 
260
266
  # The ENV can only be merged after checking if an update is required because we depend on the original value of
261
267
  # ENV["BUNDLE_GEMFILE"], which gets overridden after the merge
262
- should_update = should_bundle_update?
263
- ENV #: as untyped
264
- .merge!(env)
268
+ FileUtils.touch(@needs_update_path) if should_bundle_update?
269
+ ENV.merge!(env)
265
270
 
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
271
+ $stderr.puts("Ruby LSP> Checking if the composed bundle is satisfied...")
272
+ missing_gems = bundle_check
273
+
274
+ unless missing_gems.empty?
275
+ $stderr.puts(<<~MESSAGE)
276
+ Ruby LSP> Running bundle install because the following gems are not installed:
277
+ #{missing_gems.map { |g| "#{g.name}: #{g.version}" }.join("\n")}
278
+ MESSAGE
279
+
280
+ bundle_install
270
281
  end
271
282
 
283
+ $stderr.puts("Ruby LSP> Bundle already satisfied")
284
+ env
285
+ rescue => e
286
+ $stderr.puts("Ruby LSP> Running bundle install because #{e.message}")
287
+ bundle_install
288
+ env
289
+ end
290
+
291
+ # Essentially the same as bundle check, but simplified
292
+ #: -> Array[Gem::Specification]
293
+ def bundle_check
294
+ definition = Bundler.definition
295
+ definition.validate_runtime!
296
+ definition.check!
297
+ definition.missing_specs
298
+ end
299
+
300
+ #: -> void
301
+ def bundle_install
302
+ Bundler::CLI::Install.new({ "no-cache" => true }).run
303
+ correct_relative_remote_paths if @custom_lockfile.exist?
304
+ end
305
+
306
+ #: (Hash[String, String]) -> Hash[String, String]
307
+ def update(env)
272
308
  # Try to auto upgrade the gems we depend on, unless they are in the Gemfile as that would result in undesired
273
309
  # source control changes
274
310
  gems = ["ruby-lsp", "debug", "prism"].reject { |dep| @dependencies[dep] }
@@ -276,11 +312,9 @@ module RubyLsp
276
312
 
277
313
  Bundler::CLI::Update.new({ conservative: true }, gems).run
278
314
  correct_relative_remote_paths if @custom_lockfile.exist?
315
+ @needs_update_path.delete
279
316
  @last_updated_path.write(Time.now.iso8601)
280
317
  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
318
  end
285
319
 
286
320
  #: (Hash[String, String] env) -> Hash[String, String]
@@ -440,15 +474,7 @@ module RubyLsp
440
474
  def patch_thor_to_print_progress_to_stderr!
441
475
  return unless defined?(Bundler::Thor::Shell::Basic)
442
476
 
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
-
477
+ Bundler::Thor::Shell::Basic.prepend(ThorPatch)
452
478
  Bundler.ui.level = :info
453
479
  end
454
480
  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,6 +12,18 @@ module RubyLsp
12
12
  class LspReporter
13
13
  include Singleton
14
14
 
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] }
26
+
15
27
  #: bool
16
28
  attr_reader :invoked_shutdown
17
29
 
@@ -20,8 +32,6 @@ module RubyLsp
20
32
  dir_path = File.join(Dir.tmpdir, "ruby-lsp")
21
33
  FileUtils.mkdir_p(dir_path)
22
34
 
23
- # Remove in 1 month once updates have rolled out
24
- legacy_port_path = File.join(dir_path, "test_reporter_port")
25
35
  port_db_path = File.join(dir_path, "test_reporter_port_db.json")
26
36
  port = ENV["RUBY_LSP_REPORTER_PORT"]
27
37
 
@@ -32,8 +42,6 @@ module RubyLsp
32
42
  elsif File.exist?(port_db_path)
33
43
  db = JSON.load_file(port_db_path)
34
44
  TCPSocket.new("localhost", db[Dir.pwd])
35
- elsif File.exist?(legacy_port_path)
36
- TCPSocket.new("localhost", File.read(legacy_port_path))
37
45
  else
38
46
  # For tests that don't spawn the TCP server
39
47
  require "stringio"
@@ -129,7 +137,7 @@ module RubyLsp
129
137
  # ["Foo", :bar, 6, 21, 6, 65] => 0
130
138
  # }
131
139
  # }
132
- #: -> Hash[String, StatementCoverage]
140
+ #: -> Hash[String, statement_coverage]
133
141
  def gather_coverage_results
134
142
  # Ignore coverage results inside dependencies
135
143
  bundle_path = Bundler.bundle_path.to_s
@@ -30,6 +30,30 @@ module RubyLsp
30
30
  end
31
31
  end
32
32
 
33
+ # This patch is here to prevent other gems from overriding or adding more Minitest reporters. Otherwise, they may
34
+ # break the integration between the server and extension
35
+ module PreventReporterOverridePatch
36
+ @lsp_reporters = [] #: Array[Minitest::AbstractReporter]
37
+
38
+ class << self
39
+ #: Array[Minitest::AbstractReporter]
40
+ attr_accessor :lsp_reporters
41
+ end
42
+
43
+ # Patch the writer to prevent replacing the entire array
44
+ #: (untyped) -> void
45
+ def reporters=(reporters)
46
+ # Do nothing. We don't want other gems to override our reporter
47
+ end
48
+
49
+ # Patch the reader to prevent appending more reporters. This method always returns a temporary copy of the real
50
+ # reporters so that if any gem mutates it, it continues to return the original reporters
51
+ #: -> Array[untyped]
52
+ def reporters
53
+ PreventReporterOverridePatch.lsp_reporters.dup
54
+ end
55
+ end
56
+
33
57
  class MinitestReporter < Minitest::AbstractReporter
34
58
  class << self
35
59
  #: (Hash[untyped, untyped]) -> void
@@ -45,15 +69,30 @@ module RubyLsp
45
69
 
46
70
  # Add the JSON RPC reporter
47
71
  reporters << MinitestReporter.new
72
+ PreventReporterOverridePatch.lsp_reporters = reporters
73
+ Minitest.reporter.class.prepend(PreventReporterOverridePatch)
48
74
  end
49
75
  end
50
76
 
51
- #: (singleton(Minitest::Test) test_class, String method_name) -> void
52
- def prerecord(test_class, method_name)
53
- uri, line = LspReporter.instance.uri_and_line_for(test_class.instance_method(method_name))
77
+ #: (untyped, String) -> void
78
+ def prerecord(test_class_or_wrapper, method_name)
79
+ # In frameworks like Rails, they can control the Minitest execution by wrapping the test class
80
+ # But they conform to responding to `name`, so we can use that as a guarantee
81
+ # We are interested in the test class, not the wrapper
82
+ name = test_class_or_wrapper.name
83
+
84
+ klass = begin
85
+ Object.const_get(name) # rubocop:disable Sorbet/ConstantsFromStrings
86
+ rescue NameError
87
+ # Handle Minitest specs that create classes with invalid constant names like "MySpec::when something is true"
88
+ # If we can't resolve the constant, it means we were given the actual test class object, not the wrapper
89
+ test_class_or_wrapper
90
+ end
91
+
92
+ uri, line = LspReporter.instance.uri_and_line_for(klass.instance_method(method_name))
54
93
  return unless uri
55
94
 
56
- id = "#{test_class.name}##{handle_spec_test_id(method_name, line)}"
95
+ id = "#{name}##{handle_spec_test_id(method_name, line)}"
57
96
  LspReporter.instance.start_test(id: id, uri: uri, line: line)
58
97
  end
59
98