ruby-lsp 0.24.2 → 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 (35) 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 +18 -9
  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/entry.rb +4 -1
  10. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +9 -0
  11. data/lib/ruby_indexer/test/configuration_test.rb +1 -2
  12. data/lib/ruby_indexer/test/index_test.rb +12 -0
  13. data/lib/ruby_lsp/addon.rb +32 -9
  14. data/lib/ruby_lsp/base_server.rb +31 -21
  15. data/lib/ruby_lsp/document.rb +17 -14
  16. data/lib/ruby_lsp/global_state.rb +21 -0
  17. data/lib/ruby_lsp/internal.rb +0 -2
  18. data/lib/ruby_lsp/listeners/completion.rb +5 -2
  19. data/lib/ruby_lsp/listeners/inlay_hints.rb +5 -3
  20. data/lib/ruby_lsp/listeners/test_style.rb +7 -5
  21. data/lib/ruby_lsp/requests/code_lens.rb +9 -3
  22. data/lib/ruby_lsp/requests/inlay_hints.rb +3 -3
  23. data/lib/ruby_lsp/requests/on_type_formatting.rb +1 -1
  24. data/lib/ruby_lsp/requests/request.rb +3 -1
  25. data/lib/ruby_lsp/requests/support/formatter.rb +9 -3
  26. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +2 -2
  27. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +8 -2
  28. data/lib/ruby_lsp/response_builders/response_builder.rb +3 -3
  29. data/lib/ruby_lsp/server.rb +83 -81
  30. data/lib/ruby_lsp/setup_bundler.rb +56 -22
  31. data/lib/ruby_lsp/store.rb +0 -10
  32. data/lib/ruby_lsp/test_reporters/lsp_reporter.rb +13 -4
  33. data/lib/ruby_lsp/utils.rb +44 -5
  34. metadata +1 -16
  35. data/lib/ruby_lsp/load_sorbet.rb +0 -62
@@ -162,7 +162,7 @@ module RubyLsp
162
162
 
163
163
  #: (Integer line, Integer character) -> void
164
164
  def move_cursor_to(line, character)
165
- return unless /Visual Studio Code|Cursor|VSCodium/.match?(@client_name)
165
+ return unless /Visual Studio Code|Cursor|VSCodium|Windsurf/.match?(@client_name)
166
166
 
167
167
  position = Interface::Position.new(
168
168
  line: line,
@@ -9,7 +9,9 @@ module RubyLsp
9
9
 
10
10
  # @abstract
11
11
  #: -> untyped
12
- def perform; end
12
+ def perform
13
+ raise AbstractMethodInvokedError
14
+ end
13
15
 
14
16
  private
15
17
 
@@ -9,15 +9,21 @@ module RubyLsp
9
9
  module Formatter
10
10
  # @abstract
11
11
  #: (URI::Generic, RubyLsp::RubyDocument) -> String?
12
- def run_formatting(uri, document); end
12
+ def run_formatting(uri, document)
13
+ raise AbstractMethodInvokedError
14
+ end
13
15
 
14
16
  # @abstract
15
17
  #: (URI::Generic, String, Integer) -> String?
16
- def run_range_formatting(uri, source, base_indentation); end
18
+ def run_range_formatting(uri, source, base_indentation)
19
+ raise AbstractMethodInvokedError
20
+ end
17
21
 
18
22
  # @abstract
19
23
  #: (URI::Generic, RubyLsp::RubyDocument) -> Array[Interface::Diagnostic]?
20
- def run_diagnostic(uri, document); end
24
+ def run_diagnostic(uri, document)
25
+ raise AbstractMethodInvokedError
26
+ end
21
27
  end
22
28
  end
23
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,8 +93,8 @@ 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 = []
@@ -101,6 +102,11 @@ module RubyLsp
101
102
  @offenses = []
102
103
  @options[:stdin] = contents
103
104
 
105
+ # Setting the Prism result before running the RuboCop runner makes it reuse the existing AST and avoids
106
+ # double-parsing. Unfortunately, this leads to a bunch of cops failing to execute properly under LSP mode.
107
+ # Uncomment this once reusing the Prism result is more stable
108
+ # @prism_result = prism_result
109
+
104
110
  super([path])
105
111
 
106
112
  # RuboCop rescues interrupts and then sets the `@aborting` variable to true. We don't want them to be rescued,
@@ -5,11 +5,11 @@ module RubyLsp
5
5
  module ResponseBuilders
6
6
  # @abstract
7
7
  class ResponseBuilder
8
- extend T::Generic
9
-
10
8
  # @abstract
11
9
  #: -> top
12
- def response; end
10
+ def response
11
+ raise AbstractMethodInvokedError
12
+ end
13
13
  end
14
14
  end
15
15
  end
@@ -94,7 +94,13 @@ module RubyLsp
94
94
  id: message[:id],
95
95
  response:
96
96
  Addon.addons.map do |addon|
97
- { name: addon.name, version: addon.version, errored: addon.error? }
97
+ version = begin
98
+ addon.version
99
+ rescue AbstractMethodInvokedError
100
+ nil
101
+ end
102
+
103
+ { name: addon.name, version: version, errored: addon.error? }
98
104
  end,
99
105
  ),
100
106
  )
@@ -161,6 +167,7 @@ module RubyLsp
161
167
  return if @setup_error
162
168
 
163
169
  errors = Addon.load_addons(@global_state, @outgoing_queue, include_project_addons: include_project_addons)
170
+ return if test_mode?
164
171
 
165
172
  if errors.any?
166
173
  send_log_message(
@@ -173,21 +180,13 @@ module RubyLsp
173
180
 
174
181
  if errored_addons.any?
175
182
  send_message(
176
- Notification.new(
177
- method: "window/showMessage",
178
- params: Interface::ShowMessageParams.new(
179
- type: Constant::MessageType::WARNING,
180
- message: "Error loading add-ons:\n\n#{errored_addons.map(&:formatted_errors).join("\n\n")}",
181
- ),
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,
182
186
  ),
183
187
  )
184
188
 
185
- unless @test_mode
186
- send_log_message(
187
- errored_addons.map(&:errors_details).join("\n\n"),
188
- type: Constant::MessageType::WARNING,
189
- )
190
- end
189
+ send_log_message(errored_addons.map(&:errors_details).join("\n\n"), type: Constant::MessageType::WARNING)
191
190
  end
192
191
  end
193
192
 
@@ -203,10 +202,6 @@ module RubyLsp
203
202
 
204
203
  configured_features = options.dig(:initializationOptions, :enabledFeatures)
205
204
 
206
- configured_hints = options.dig(:initializationOptions, :featuresConfiguration, :inlayHint)
207
- @store.features_configuration.dig(:inlayHint) #: as !nil
208
- .configuration.merge!(configured_hints) if configured_hints
209
-
210
205
  enabled_features = case configured_features
211
206
  when Array
212
207
  # If the configuration is using an array, then absent features are disabled and present ones are enabled. That's
@@ -360,56 +355,53 @@ module RubyLsp
360
355
 
361
356
  perform_initial_indexing
362
357
  check_formatter_is_available
358
+ update_server if @global_state.enabled_feature?(:launcher)
363
359
  end
364
360
 
365
361
  #: (Hash[Symbol, untyped] message) -> void
366
362
  def text_document_did_open(message)
367
- @global_state.synchronize do
368
- text_document = message.dig(:params, :textDocument)
369
- language_id = case text_document[:languageId]
370
- when "erb", "eruby"
371
- :erb
372
- when "rbs"
373
- :rbs
374
- else
375
- :ruby
376
- 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
377
372
 
378
- document = @store.set(
379
- uri: text_document[:uri],
380
- source: text_document[:text],
381
- version: text_document[:version],
382
- language_id: language_id,
383
- )
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
+ )
384
379
 
385
- if document.past_expensive_limit? && text_document[:uri].scheme == "file"
386
- log_message = <<~MESSAGE
387
- The file #{text_document[:uri].path} is too long. For performance reasons, semantic highlighting and
388
- diagnostics will be disabled.
389
- 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
390
385
 
391
- send_message(
392
- Notification.new(
393
- method: "window/logMessage",
394
- params: Interface::LogMessageParams.new(
395
- type: Constant::MessageType::WARNING,
396
- message: log_message,
397
- ),
386
+ send_message(
387
+ Notification.new(
388
+ method: "window/logMessage",
389
+ params: Interface::LogMessageParams.new(
390
+ type: Constant::MessageType::WARNING,
391
+ message: log_message,
398
392
  ),
399
- )
400
- end
393
+ ),
394
+ )
401
395
  end
402
396
  end
403
397
 
404
398
  #: (Hash[Symbol, untyped] message) -> void
405
399
  def text_document_did_close(message)
406
- @global_state.synchronize do
407
- uri = message.dig(:params, :textDocument, :uri)
408
- @store.delete(uri)
400
+ uri = message.dig(:params, :textDocument, :uri)
401
+ @store.delete(uri)
409
402
 
410
- # Clear diagnostics for the closed file, so that they no longer appear in the problems tab
411
- send_message(Notification.publish_diagnostics(uri.to_s, []))
412
- 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, []))
413
405
  end
414
406
 
415
407
  #: (Hash[Symbol, untyped] message) -> void
@@ -417,9 +409,7 @@ module RubyLsp
417
409
  params = message[:params]
418
410
  text_document = params[:textDocument]
419
411
 
420
- @global_state.synchronize do
421
- @store.push_edits(uri: text_document[:uri], edits: params[:contentChanges], version: text_document[:version])
422
- end
412
+ @store.push_edits(uri: text_document[:uri], edits: params[:contentChanges], version: text_document[:version])
423
413
  end
424
414
 
425
415
  #: (Hash[Symbol, untyped] message) -> void
@@ -473,8 +463,8 @@ module RubyLsp
473
463
  document_symbol = Requests::DocumentSymbol.new(uri, dispatcher)
474
464
  document_link = Requests::DocumentLink.new(uri, parse_result.comments, dispatcher)
475
465
  inlay_hint = Requests::InlayHints.new(
466
+ @global_state,
476
467
  document,
477
- @store.features_configuration.dig(:inlayHint), #: as !nil
478
468
  dispatcher,
479
469
  )
480
470
 
@@ -821,7 +811,6 @@ module RubyLsp
821
811
  return
822
812
  end
823
813
 
824
- hints_configurations = @store.features_configuration.dig(:inlayHint) #: as !nil
825
814
  dispatcher = Prism::Dispatcher.new
826
815
 
827
816
  unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument)
@@ -829,7 +818,7 @@ module RubyLsp
829
818
  return
830
819
  end
831
820
 
832
- request = Requests::InlayHints.new(document, hints_configurations, dispatcher)
821
+ request = Requests::InlayHints.new(@global_state, document, dispatcher)
833
822
  dispatcher.visit(document.ast)
834
823
  result = request.perform
835
824
  document.cache_set("textDocument/inlayHint", result)
@@ -1103,11 +1092,7 @@ module RubyLsp
1103
1092
  @global_state.register_formatter("rubocop_internal", Requests::Support::RuboCopFormatter.new)
1104
1093
 
1105
1094
  # Clear all document caches for pull diagnostics
1106
- @global_state.synchronize do
1107
- @store.each do |_uri, document|
1108
- document.clear_cache("textDocument/diagnostic")
1109
- end
1110
- end
1095
+ @store.each { |_uri, document| document.clear_cache("textDocument/diagnostic") }
1111
1096
 
1112
1097
  # Request a pull diagnostic refresh from the editor
1113
1098
  if @global_state.client_capabilities.supports_diagnostic_refresh
@@ -1222,7 +1207,7 @@ module RubyLsp
1222
1207
  }
1223
1208
  end
1224
1209
  end
1225
- rescue Bundler::GemNotFound
1210
+ rescue Bundler::GemNotFound, Bundler::GemfileNotFound
1226
1211
  []
1227
1212
  end
1228
1213
 
@@ -1405,8 +1390,38 @@ module RubyLsp
1405
1390
 
1406
1391
  # We compose the bundle in a thread so that the LSP continues to work while we're checking for its validity. Once
1407
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)
1408
1423
  Thread.new do
1409
- send_log_message("Recomposing the bundle ahead of restart")
1424
+ send_log_message(log)
1410
1425
 
1411
1426
  _stdout, stderr, status = Bundler.with_unbundled_env do
1412
1427
  Open3.capture3(
@@ -1421,17 +1436,7 @@ module RubyLsp
1421
1436
  )
1422
1437
  end
1423
1438
 
1424
- if status&.exitstatus == 0
1425
- # Create a signal for the restart that it can skip composing the bundle and launch directly
1426
- FileUtils.touch(already_composed_path)
1427
- send_message(Result.new(id: id, response: { success: true }))
1428
- else
1429
- # This special error code makes the extension avoid restarting in case we already know that the composed
1430
- # bundle is not valid
1431
- send_message(
1432
- Error.new(id: id, code: BUNDLE_COMPOSE_FAILED_CODE, message: "Failed to compose bundle\n#{stderr}"),
1433
- )
1434
- end
1439
+ block.call(stderr, status)
1435
1440
  end
1436
1441
  end
1437
1442
 
@@ -1488,10 +1493,7 @@ module RubyLsp
1488
1493
 
1489
1494
  send_message(Result.new(
1490
1495
  id: message[:id],
1491
- response: {
1492
- commands: commands,
1493
- reporterPaths: [Listeners::TestStyle::MINITEST_REPORTER_PATH, Listeners::TestStyle::TEST_UNIT_REPORTER_PATH],
1494
- },
1496
+ response: { commands: commands },
1495
1497
  ))
1496
1498
  end
1497
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"
@@ -20,11 +19,16 @@ 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
@@ -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
 
@@ -12,8 +12,17 @@ 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
@@ -125,7 +134,7 @@ module RubyLsp
125
134
  # ["Foo", :bar, 6, 21, 6, 65] => 0
126
135
  # }
127
136
  # }
128
- #: -> Hash[String, StatementCoverage]
137
+ #: -> Hash[String, statement_coverage]
129
138
  def gather_coverage_results
130
139
  # Ignore coverage results inside dependencies
131
140
  bundle_path = Bundler.bundle_path.to_s
@@ -183,7 +192,7 @@ module RubyLsp
183
192
 
184
193
  #: -> void
185
194
  def at_exit
186
- internal_shutdown unless invoked_shutdown
195
+ internal_shutdown unless @invoked_shutdown
187
196
  end
188
197
 
189
198
  class << self
@@ -5,7 +5,6 @@ module RubyLsp
5
5
  # rubocop:disable RubyLsp/UseLanguageServerAliases
6
6
  Interface = LanguageServer::Protocol::Interface
7
7
  Constant = LanguageServer::Protocol::Constant
8
- Transport = LanguageServer::Protocol::Transport
9
8
  # rubocop:enable RubyLsp/UseLanguageServerAliases
10
9
 
11
10
  # Used to indicate that a request shouldn't return a response
@@ -20,6 +19,7 @@ module RubyLsp
20
19
  "Gemfile"
21
20
  end #: String
22
21
  GUESSED_TYPES_URL = "https://shopify.github.io/ruby-lsp/#guessed-types"
22
+ TEST_PATH_PATTERN = "**/{test,spec,features}/**/{*_test.rb,test_*.rb,*_spec.rb,*.feature}"
23
23
 
24
24
  # Request delegation for embedded languages is not yet standardized into the language server specification. Here we
25
25
  # use this custom error class as a way to return a signal to the client that the request should be delegated to the
@@ -31,6 +31,8 @@ module RubyLsp
31
31
  CODE = -32900
32
32
  end
33
33
 
34
+ class AbstractMethodInvokedError < StandardError; end
35
+
34
36
  BUNDLE_COMPOSE_FAILED_CODE = -33000
35
37
 
36
38
  # A notification to be sent to the client
@@ -50,7 +52,9 @@ module RubyLsp
50
52
 
51
53
  # @abstract
52
54
  #: -> Hash[Symbol, untyped]
53
- def to_hash; end
55
+ def to_hash
56
+ raise AbstractMethodInvokedError
57
+ end
54
58
  end
55
59
 
56
60
  class Notification < Message
@@ -243,9 +247,6 @@ module RubyLsp
243
247
 
244
248
  # A request configuration, to turn on/off features
245
249
  class RequestConfig
246
- #: Hash[Symbol, bool]
247
- attr_accessor :configuration
248
-
249
250
  #: (Hash[Symbol, bool] configuration) -> void
250
251
  def initialize(configuration)
251
252
  @configuration = configuration
@@ -255,6 +256,11 @@ module RubyLsp
255
256
  def enabled?(feature)
256
257
  @configuration[:enableAll] || @configuration[feature]
257
258
  end
259
+
260
+ #: (Hash[Symbol, bool]) -> void
261
+ def merge!(hash)
262
+ @configuration.merge!(hash)
263
+ end
258
264
  end
259
265
 
260
266
  class SorbetLevel
@@ -299,4 +305,37 @@ module RubyLsp
299
305
  #: -> bool
300
306
  def true_or_higher? = @level == :true || @level == :strict
301
307
  end
308
+
309
+ # Reads JSON RPC messages from the given IO in a loop
310
+ class MessageReader
311
+ #: (IO) -> void
312
+ def initialize(io)
313
+ @io = io
314
+ end
315
+
316
+ #: () { (Hash[Symbol, untyped]) -> void } -> void
317
+ def each_message(&block)
318
+ while (headers = @io.gets("\r\n\r\n"))
319
+ raw_message = @io.read(headers[/Content-Length: (\d+)/i, 1].to_i) #: as !nil
320
+ block.call(JSON.parse(raw_message, symbolize_names: true))
321
+ end
322
+ end
323
+ end
324
+
325
+ # Writes JSON RPC messages to the given IO
326
+ class MessageWriter
327
+ #: (IO) -> void
328
+ def initialize(io)
329
+ @io = io
330
+ end
331
+
332
+ #: (Hash[Symbol, untyped]) -> void
333
+ def write(message)
334
+ message[:jsonrpc] = "2.0"
335
+ json_message = message.to_json
336
+
337
+ @io.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
338
+ @io.flush
339
+ end
340
+ end
302
341
  end