ruby-lsp 0.23.15 → 0.26.9
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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/exe/ruby-lsp +17 -14
- data/exe/ruby-lsp-check +0 -4
- data/exe/ruby-lsp-launcher +41 -14
- data/exe/ruby-lsp-test-exec +6 -0
- data/lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb +0 -1
- data/lib/rubocop/cop/ruby_lsp/use_register_with_handler_method.rb +0 -1
- data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +4 -3
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +42 -20
- data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +1 -7
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +49 -62
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +84 -74
- data/lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb +6 -9
- data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +9 -14
- data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +12 -8
- data/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb +4 -4
- data/lib/ruby_lsp/addon.rb +44 -15
- data/lib/ruby_lsp/base_server.rb +56 -37
- data/lib/ruby_lsp/client_capabilities.rb +6 -1
- data/lib/ruby_lsp/document.rb +174 -62
- data/lib/ruby_lsp/erb_document.rb +10 -8
- data/lib/ruby_lsp/global_state.rb +86 -33
- data/lib/ruby_lsp/internal.rb +6 -3
- data/lib/ruby_lsp/listeners/completion.rb +22 -11
- data/lib/ruby_lsp/listeners/definition.rb +41 -21
- data/lib/ruby_lsp/listeners/document_highlight.rb +26 -1
- data/lib/ruby_lsp/listeners/document_link.rb +64 -28
- data/lib/ruby_lsp/listeners/hover.rb +27 -16
- data/lib/ruby_lsp/listeners/inlay_hints.rb +5 -3
- data/lib/ruby_lsp/listeners/semantic_highlighting.rb +2 -2
- data/lib/ruby_lsp/listeners/signature_help.rb +2 -2
- data/lib/ruby_lsp/listeners/spec_style.rb +155 -79
- data/lib/ruby_lsp/listeners/test_discovery.rb +39 -21
- data/lib/ruby_lsp/listeners/test_style.rb +75 -35
- data/lib/ruby_lsp/rbs_document.rb +3 -6
- data/lib/ruby_lsp/requests/code_action_resolve.rb +83 -58
- data/lib/ruby_lsp/requests/code_actions.rb +20 -5
- data/lib/ruby_lsp/requests/code_lens.rb +27 -6
- data/lib/ruby_lsp/requests/completion.rb +3 -3
- data/lib/ruby_lsp/requests/completion_resolve.rb +8 -6
- data/lib/ruby_lsp/requests/definition.rb +4 -7
- data/lib/ruby_lsp/requests/discover_tests.rb +2 -2
- data/lib/ruby_lsp/requests/document_highlight.rb +2 -2
- data/lib/ruby_lsp/requests/document_link.rb +1 -1
- data/lib/ruby_lsp/requests/folding_ranges.rb +1 -1
- data/lib/ruby_lsp/requests/go_to_relevant_file.rb +64 -12
- data/lib/ruby_lsp/requests/hover.rb +3 -6
- data/lib/ruby_lsp/requests/inlay_hints.rb +4 -4
- data/lib/ruby_lsp/requests/on_type_formatting.rb +1 -1
- data/lib/ruby_lsp/requests/prepare_rename.rb +1 -1
- data/lib/ruby_lsp/requests/references.rb +10 -21
- data/lib/ruby_lsp/requests/rename.rb +9 -10
- data/lib/ruby_lsp/requests/request.rb +8 -8
- data/lib/ruby_lsp/requests/selection_ranges.rb +2 -2
- data/lib/ruby_lsp/requests/semantic_highlighting.rb +1 -1
- data/lib/ruby_lsp/requests/show_syntax_tree.rb +2 -2
- data/lib/ruby_lsp/requests/signature_help.rb +2 -2
- data/lib/ruby_lsp/requests/support/annotation.rb +1 -1
- data/lib/ruby_lsp/requests/support/common.rb +9 -12
- data/lib/ruby_lsp/requests/support/formatter.rb +16 -15
- data/lib/ruby_lsp/requests/support/package_url.rb +414 -0
- data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +7 -1
- data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +2 -2
- data/lib/ruby_lsp/requests/support/rubocop_runner.rb +13 -3
- data/lib/ruby_lsp/requests/support/source_uri.rb +7 -4
- data/lib/ruby_lsp/requests/support/test_item.rb +7 -1
- data/lib/ruby_lsp/requests/workspace_symbol.rb +20 -12
- data/lib/ruby_lsp/response_builders/collection_response_builder.rb +1 -4
- data/lib/ruby_lsp/response_builders/document_symbol.rb +2 -3
- data/lib/ruby_lsp/response_builders/hover.rb +1 -4
- data/lib/ruby_lsp/response_builders/response_builder.rb +6 -7
- data/lib/ruby_lsp/response_builders/semantic_highlighting.rb +4 -5
- data/lib/ruby_lsp/response_builders/signature_help.rb +1 -2
- data/lib/ruby_lsp/response_builders/test_collection.rb +29 -3
- data/lib/ruby_lsp/ruby_document.rb +14 -42
- data/lib/ruby_lsp/scripts/compose_bundle.rb +3 -3
- data/lib/ruby_lsp/scripts/compose_bundle_windows.rb +3 -1
- data/lib/ruby_lsp/server.rb +173 -130
- data/lib/ruby_lsp/setup_bundler.rb +114 -47
- data/lib/ruby_lsp/static_docs.rb +1 -0
- data/lib/ruby_lsp/store.rb +6 -16
- data/lib/ruby_lsp/test_helper.rb +1 -4
- data/lib/ruby_lsp/test_reporters/lsp_reporter.rb +121 -17
- data/lib/ruby_lsp/test_reporters/minitest_reporter.rb +65 -25
- data/lib/ruby_lsp/test_reporters/test_unit_reporter.rb +16 -18
- data/lib/ruby_lsp/utils.rb +102 -13
- data/static_docs/break.md +103 -0
- metadata +8 -33
- data/lib/ruby_indexer/test/class_variables_test.rb +0 -140
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +0 -770
- data/lib/ruby_indexer/test/configuration_test.rb +0 -280
- data/lib/ruby_indexer/test/constant_test.rb +0 -402
- data/lib/ruby_indexer/test/enhancements_test.rb +0 -325
- data/lib/ruby_indexer/test/global_variable_test.rb +0 -49
- data/lib/ruby_indexer/test/index_test.rb +0 -2190
- data/lib/ruby_indexer/test/instance_variables_test.rb +0 -240
- data/lib/ruby_indexer/test/method_test.rb +0 -973
- data/lib/ruby_indexer/test/prefix_tree_test.rb +0 -150
- data/lib/ruby_indexer/test/rbs_indexer_test.rb +0 -380
- data/lib/ruby_indexer/test/reference_finder_test.rb +0 -330
- data/lib/ruby_indexer/test/test_case.rb +0 -51
- data/lib/ruby_indexer/test/uri_test.rb +0 -85
- data/lib/ruby_lsp/load_sorbet.rb +0 -62
|
@@ -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,27 +11,37 @@ 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
|
|
16
|
-
# the Ruby LSP without including the gem in their application's Gemfile while at the same time giving us access to
|
|
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
|
|
|
34
|
+
# Gems that should be kept up to date in the composed bundle. When updating, any of these gems that are not
|
|
35
|
+
# already in the user's Gemfile will be updated together.
|
|
36
|
+
GEMS_TO_UPDATE = ["ruby-lsp", "debug", "prism", "rbs"].freeze #: Array[String]
|
|
37
|
+
RUBY_LSP_MIN_VERSION = "0.18.0" #: String
|
|
38
|
+
|
|
30
39
|
#: (String project_path, **untyped options) -> void
|
|
31
40
|
def initialize(project_path, **options)
|
|
32
41
|
@project_path = project_path
|
|
33
|
-
@branch = options[:branch] #: String?
|
|
34
42
|
@launcher = options[:launcher] #: bool?
|
|
35
|
-
|
|
43
|
+
@beta = options[:beta] #: bool?
|
|
44
|
+
force_output_to_stderr! if @launcher
|
|
36
45
|
|
|
37
46
|
# Regular bundle paths
|
|
38
47
|
@gemfile = begin
|
|
@@ -51,7 +60,7 @@ module RubyLsp
|
|
|
51
60
|
@custom_dir = Pathname.new(".ruby-lsp").expand_path(@project_path) #: Pathname
|
|
52
61
|
@custom_gemfile = @custom_dir + @gemfile_name #: Pathname
|
|
53
62
|
@custom_lockfile = @custom_dir + (@lockfile&.basename || "Gemfile.lock") #: Pathname
|
|
54
|
-
@
|
|
63
|
+
@freshness_hash_path = @custom_dir + "freshness_hash" #: Pathname
|
|
55
64
|
@last_updated_path = @custom_dir + "last_updated" #: Pathname
|
|
56
65
|
@error_path = @custom_dir + "install_error" #: Pathname
|
|
57
66
|
@already_composed_path = @custom_dir + "bundle_is_composed" #: Pathname
|
|
@@ -61,6 +70,7 @@ module RubyLsp
|
|
|
61
70
|
@bundler_version = bundler_version #: Gem::Version?
|
|
62
71
|
@rails_app = rails_app? #: bool
|
|
63
72
|
@retry = false #: bool
|
|
73
|
+
@needs_update_path = @custom_dir + "needs_update" #: Pathname
|
|
64
74
|
end
|
|
65
75
|
|
|
66
76
|
# Sets up the composed bundle and returns the `BUNDLE_GEMFILE`, `BUNDLE_PATH` and `BUNDLE_APP_CONFIG` that should be
|
|
@@ -109,17 +119,24 @@ module RubyLsp
|
|
|
109
119
|
return run_bundle_install(@custom_gemfile)
|
|
110
120
|
end
|
|
111
121
|
|
|
112
|
-
if
|
|
113
|
-
|
|
122
|
+
# Our freshness hash determines if we need to copy the lockfile from the main app again and run bundle install
|
|
123
|
+
# from scratch. We use a combination of the main app's lockfile and the composed Gemfile. The goal is to
|
|
124
|
+
# automatically account for CLI arguments which can change the Gemfile we compose. If the CLI arguments or the
|
|
125
|
+
# main lockfile change, we need to make sure we're re-composing.
|
|
126
|
+
freshness_digest = Digest::SHA256.hexdigest("#{@lockfile_hash}#{@custom_gemfile.read}")
|
|
127
|
+
|
|
128
|
+
if @lockfile_hash && @custom_lockfile.exist? && @freshness_hash_path.exist? &&
|
|
129
|
+
@freshness_hash_path.read == freshness_digest
|
|
114
130
|
$stderr.puts(
|
|
115
131
|
"Ruby LSP> Skipping composed bundle setup since #{@custom_lockfile} already exists and is up to date",
|
|
116
132
|
)
|
|
117
133
|
return run_bundle_install(@custom_gemfile)
|
|
118
134
|
end
|
|
119
135
|
|
|
136
|
+
@needs_update_path.delete if @needs_update_path.exist?
|
|
120
137
|
FileUtils.cp(@lockfile.to_s, @custom_lockfile.to_s)
|
|
121
138
|
correct_relative_remote_paths
|
|
122
|
-
@
|
|
139
|
+
@freshness_hash_path.write(freshness_digest)
|
|
123
140
|
run_bundle_install(@custom_gemfile)
|
|
124
141
|
end
|
|
125
142
|
|
|
@@ -159,9 +176,8 @@ module RubyLsp
|
|
|
159
176
|
end
|
|
160
177
|
|
|
161
178
|
unless @dependencies["ruby-lsp"]
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
parts << ruby_lsp_entry
|
|
179
|
+
version = @beta ? "0.a" : RUBY_LSP_MIN_VERSION
|
|
180
|
+
parts << "gem \"ruby-lsp\", \">= #{version}\", require: false, group: :development"
|
|
165
181
|
end
|
|
166
182
|
|
|
167
183
|
unless @dependencies["debug"]
|
|
@@ -229,6 +245,16 @@ module RubyLsp
|
|
|
229
245
|
# If no error occurred, then clear previous errors
|
|
230
246
|
@error_path.delete if @error_path.exist?
|
|
231
247
|
$stderr.puts("Ruby LSP> Composed bundle installation complete")
|
|
248
|
+
rescue Errno::EPIPE, Bundler::HTTPError, Bundler::InstallError
|
|
249
|
+
# There are cases where we expect certain errors to happen occasionally, and we don't want to write them to
|
|
250
|
+
# a file, which would report to telemetry on the next launch.
|
|
251
|
+
#
|
|
252
|
+
# - The $stderr pipe might be closed by the client, for example when closing the editor during running bundle
|
|
253
|
+
# install. This situation may happen because, while running bundle install, the server is not yet ready to
|
|
254
|
+
# receive shutdown requests and we may continue doing work until the process is killed.
|
|
255
|
+
# - Bundler might also encounter a network error.
|
|
256
|
+
# - Native extension build failures (InstallError) are user environment issues that Ruby LSP cannot resolve.
|
|
257
|
+
@error_path.delete if @error_path.exist?
|
|
232
258
|
rescue => e
|
|
233
259
|
# Write the error object to a file so that we can read it from the parent process
|
|
234
260
|
@error_path.write(Marshal.dump(e))
|
|
@@ -257,41 +283,86 @@ module RubyLsp
|
|
|
257
283
|
def run_bundle_install_directly(env, force_install: false)
|
|
258
284
|
RubyVM::YJIT.enable if defined?(RubyVM::YJIT.enable)
|
|
259
285
|
|
|
260
|
-
# The
|
|
261
|
-
#
|
|
262
|
-
|
|
263
|
-
|
|
286
|
+
# The should_bundle_update? check needs to run on the original Bundler environment, but everything else (like
|
|
287
|
+
# updating or running install) requires the modified environment. Here we compute the check ahead of time and
|
|
288
|
+
# merge the environment to ensure correct results.
|
|
289
|
+
#
|
|
290
|
+
# The symptoms of having these operations in the wrong order is seeing unwanted modifications in the application's
|
|
291
|
+
# main lockfile because we accidentally run update on the main bundle instead of the composed one.
|
|
292
|
+
needs_update = should_bundle_update?
|
|
293
|
+
ENV.merge!(env)
|
|
264
294
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
295
|
+
return update(env) if @needs_update_path.exist?
|
|
296
|
+
|
|
297
|
+
FileUtils.touch(@needs_update_path) if needs_update
|
|
298
|
+
|
|
299
|
+
$stderr.puts("Ruby LSP> Checking if the composed bundle is satisfied...")
|
|
300
|
+
|
|
301
|
+
begin
|
|
302
|
+
missing_gems = bundle_check
|
|
303
|
+
rescue Errno::EPIPE, Bundler::HTTPError
|
|
304
|
+
# These are errors cases where we cannot recover
|
|
305
|
+
raise
|
|
306
|
+
rescue => e
|
|
307
|
+
# If anything fails with bundle check, try to bundle install
|
|
308
|
+
$stderr.puts("Ruby LSP> Running bundle install because #{e.message}")
|
|
309
|
+
bundle_install
|
|
268
310
|
return env
|
|
269
311
|
end
|
|
270
312
|
|
|
313
|
+
if missing_gems.empty?
|
|
314
|
+
$stderr.puts("Ruby LSP> Bundle already satisfied")
|
|
315
|
+
else
|
|
316
|
+
$stderr.puts(<<~MESSAGE)
|
|
317
|
+
Ruby LSP> Running bundle install because the following gems are not installed:
|
|
318
|
+
#{missing_gems.map { |g| "#{g.name}: #{g.version}" }.join("\n")}
|
|
319
|
+
MESSAGE
|
|
320
|
+
|
|
321
|
+
bundle_install
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
env
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Essentially the same as bundle check, but simplified
|
|
328
|
+
#: -> Array[Gem::Specification]
|
|
329
|
+
def bundle_check
|
|
330
|
+
definition = Bundler.definition
|
|
331
|
+
definition.validate_runtime!
|
|
332
|
+
definition.check!
|
|
333
|
+
definition.missing_specs
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
#: -> void
|
|
337
|
+
def bundle_install
|
|
338
|
+
Bundler::CLI::Install.new({ "no-cache" => true }).run
|
|
339
|
+
correct_relative_remote_paths if @custom_lockfile.exist?
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
#: (Hash[String, String]) -> Hash[String, String]
|
|
343
|
+
def update(env)
|
|
271
344
|
# Try to auto upgrade the gems we depend on, unless they are in the Gemfile as that would result in undesired
|
|
272
345
|
# source control changes
|
|
273
|
-
gems =
|
|
346
|
+
gems = GEMS_TO_UPDATE.reject { |dep| @dependencies[dep] }
|
|
274
347
|
gems << "ruby-lsp-rails" if @rails_app && !@dependencies["ruby-lsp-rails"]
|
|
275
348
|
|
|
276
349
|
Bundler::CLI::Update.new({ conservative: true }, gems).run
|
|
277
350
|
correct_relative_remote_paths if @custom_lockfile.exist?
|
|
351
|
+
@needs_update_path.delete if @needs_update_path.exist?
|
|
278
352
|
@last_updated_path.write(Time.now.iso8601)
|
|
279
353
|
env
|
|
280
|
-
rescue Bundler::GemNotFound, Bundler::GitError
|
|
281
|
-
# If a gem is not installed, skip the upgrade and try to install it with a single retry
|
|
282
|
-
@retry ? env : run_bundle_install_directly(env, force_install: true)
|
|
283
354
|
end
|
|
284
355
|
|
|
285
356
|
#: (Hash[String, String] env) -> Hash[String, String]
|
|
286
357
|
def run_bundle_install_through_command(env)
|
|
287
|
-
# If
|
|
288
|
-
# to upgrade them or else we'll produce undesired source control changes. If the composed bundle was just
|
|
289
|
-
# and any of
|
|
290
|
-
#
|
|
291
|
-
#
|
|
358
|
+
# If the gems in GEMS_TO_UPDATE (and potentially `ruby-lsp-rails`) are already in the Gemfile, then we shouldn't
|
|
359
|
+
# try to upgrade them or else we'll produce undesired source control changes. If the composed bundle was just
|
|
360
|
+
# created and any of those gems weren't a part of the Gemfile, then we need to run `bundle install` for the first
|
|
361
|
+
# time to generate the Gemfile.lock with them included or else Bundler will complain that they're missing. We can
|
|
362
|
+
# only update if the custom `.ruby-lsp/Gemfile.lock` already exists and includes all gems
|
|
292
363
|
|
|
293
364
|
# When not updating, we run `(bundle check || bundle install)`
|
|
294
|
-
# When updating, we run `((bundle check && bundle update
|
|
365
|
+
# When updating, we run `((bundle check && bundle update <GEMS_TO_UPDATE>) || bundle install)`
|
|
295
366
|
bundler_path = File.join(Gem.default_bindir, "bundle")
|
|
296
367
|
base_command = (!Gem.win_platform? && File.exist?(bundler_path) ? "#{Gem.ruby} #{bundler_path}" : "bundle").dup
|
|
297
368
|
|
|
@@ -302,12 +373,11 @@ module RubyLsp
|
|
|
302
373
|
command = +"(#{base_command} check"
|
|
303
374
|
|
|
304
375
|
if should_bundle_update?
|
|
305
|
-
# If any of
|
|
306
|
-
# version
|
|
376
|
+
# If any of the gems in GEMS_TO_UPDATE (or `ruby-lsp-rails` for Rails apps) are not in the Gemfile, try to
|
|
377
|
+
# update them to the latest version
|
|
307
378
|
command.prepend("(")
|
|
308
379
|
command << " && #{base_command} update "
|
|
309
|
-
command << "
|
|
310
|
-
command << "debug " unless @dependencies["debug"]
|
|
380
|
+
GEMS_TO_UPDATE.each { |gem| command << "#{gem} " unless @dependencies[gem] }
|
|
311
381
|
command << "ruby-lsp-rails " if @rails_app && !@dependencies["ruby-lsp-rails"]
|
|
312
382
|
command.delete_suffix!(" ")
|
|
313
383
|
command << ")"
|
|
@@ -436,19 +506,16 @@ module RubyLsp
|
|
|
436
506
|
end
|
|
437
507
|
|
|
438
508
|
#: -> void
|
|
439
|
-
def
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
extend T::Sig
|
|
509
|
+
def force_output_to_stderr!
|
|
510
|
+
# Bundler and RubyGems have different UI objects used for printing. We need to ensure that both are configured to
|
|
511
|
+
# print only to stderr or else they'll break the connection with the editor
|
|
512
|
+
Gem::DefaultUserInteraction.ui = Gem::StreamUI.new($stdin, $stderr, $stderr, false)
|
|
444
513
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
end
|
|
449
|
-
end)
|
|
514
|
+
ui = Bundler.ui
|
|
515
|
+
ui.output_stream = :stderr if ui.respond_to?(:output_stream=)
|
|
516
|
+
ui.level = :info
|
|
450
517
|
|
|
451
|
-
Bundler.
|
|
518
|
+
Bundler::Thor::Shell::Basic.prepend(ThorPatch) if defined?(Bundler::Thor::Shell::Basic)
|
|
452
519
|
end
|
|
453
520
|
end
|
|
454
521
|
end
|
data/lib/ruby_lsp/static_docs.rb
CHANGED
|
@@ -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
|
data/lib/ruby_lsp/store.rb
CHANGED
|
@@ -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
|
|
|
@@ -38,11 +28,11 @@ module RubyLsp
|
|
|
38
28
|
ext = File.extname(path)
|
|
39
29
|
language_id = case ext
|
|
40
30
|
when ".erb", ".rhtml"
|
|
41
|
-
|
|
31
|
+
:erb
|
|
42
32
|
when ".rbs"
|
|
43
|
-
|
|
33
|
+
:rbs
|
|
44
34
|
else
|
|
45
|
-
|
|
35
|
+
:ruby
|
|
46
36
|
end
|
|
47
37
|
|
|
48
38
|
set(uri: uri, source: File.binread(path), version: 0, language_id: language_id)
|
|
@@ -51,12 +41,12 @@ module RubyLsp
|
|
|
51
41
|
raise NonExistingDocumentError, uri.to_s
|
|
52
42
|
end
|
|
53
43
|
|
|
54
|
-
#: (uri: URI::Generic, source: String, version: Integer, language_id:
|
|
44
|
+
#: (uri: URI::Generic, source: String, version: Integer, language_id: Symbol) -> Document[untyped]
|
|
55
45
|
def set(uri:, source:, version:, language_id:)
|
|
56
46
|
@state[uri.to_s] = case language_id
|
|
57
|
-
when
|
|
47
|
+
when :erb
|
|
58
48
|
ERBDocument.new(source: source, version: version, uri: uri, global_state: @global_state)
|
|
59
|
-
when
|
|
49
|
+
when :rbs
|
|
60
50
|
RBSDocument.new(source: source, version: version, uri: uri, global_state: @global_state)
|
|
61
51
|
else
|
|
62
52
|
RubyDocument.new(source: source, version: version, uri: uri, global_state: @global_state)
|
data/lib/ruby_lsp/test_helper.rb
CHANGED
|
@@ -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)
|
|
@@ -1,35 +1,110 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require "English"
|
|
4
5
|
require "json"
|
|
5
6
|
require "socket"
|
|
6
|
-
require "
|
|
7
|
+
require "tmpdir"
|
|
8
|
+
require_relative "../../ruby_indexer/lib/ruby_indexer/uri"
|
|
7
9
|
|
|
8
10
|
module RubyLsp
|
|
9
11
|
class LspReporter
|
|
10
|
-
|
|
12
|
+
@instance = nil #: LspReporter?
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
#: -> LspReporter
|
|
16
|
+
def instance
|
|
17
|
+
@instance ||= new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#: -> bool
|
|
21
|
+
def start_coverage?
|
|
22
|
+
ENV["RUBY_LSP_TEST_RUNNER"] == "coverage"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
#: -> bool
|
|
26
|
+
def executed_under_test_runner?
|
|
27
|
+
!!(ENV["RUBY_LSP_TEST_RUNNER"] && ENV["RUBY_LSP_ENV"] != "test")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
#: (Method | UnboundMethod) -> [URI::Generic, Integer?]?
|
|
31
|
+
def uri_and_line_for(method_object)
|
|
32
|
+
file_path, line = method_object.source_location
|
|
33
|
+
return unless file_path
|
|
34
|
+
return if file_path.start_with?("(eval at ")
|
|
35
|
+
|
|
36
|
+
uri = URI::Generic.from_path(path: File.expand_path(file_path))
|
|
37
|
+
zero_based_line = line ? line - 1 : nil
|
|
38
|
+
[uri, zero_based_line]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# https://code.visualstudio.com/api/references/vscode-api#Position
|
|
43
|
+
#: type position = { line: Integer, character: Integer }
|
|
44
|
+
|
|
45
|
+
# https://code.visualstudio.com/api/references/vscode-api#Range
|
|
46
|
+
#: type range = { start: position, end: position }
|
|
47
|
+
|
|
48
|
+
# https://code.visualstudio.com/api/references/vscode-api#BranchCoverage
|
|
49
|
+
#: type branch_coverage = { executed: Integer, label: String, location: range }
|
|
50
|
+
|
|
51
|
+
# https://code.visualstudio.com/api/references/vscode-api#StatementCoverage
|
|
52
|
+
#: type statement_coverage = { executed: Integer, location: position, branches: Array[branch_coverage] }
|
|
11
53
|
|
|
12
54
|
#: -> void
|
|
13
55
|
def initialize
|
|
56
|
+
dir_path = File.join(Dir.tmpdir, "ruby-lsp")
|
|
57
|
+
FileUtils.mkdir_p(dir_path)
|
|
58
|
+
|
|
59
|
+
port_db_path = File.join(dir_path, "test_reporter_port_db.json")
|
|
14
60
|
port = ENV["RUBY_LSP_REPORTER_PORT"]
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
61
|
+
|
|
62
|
+
@io = begin
|
|
63
|
+
# The environment variable is only used for tests. The extension always writes to the temporary file
|
|
64
|
+
if port
|
|
65
|
+
socket(port)
|
|
66
|
+
elsif File.exist?(port_db_path)
|
|
67
|
+
db = JSON.load_file(port_db_path)
|
|
68
|
+
socket(db[Dir.pwd])
|
|
69
|
+
else
|
|
70
|
+
# For tests that don't spawn the TCP server
|
|
71
|
+
require "stringio"
|
|
72
|
+
StringIO.new
|
|
73
|
+
end
|
|
74
|
+
rescue
|
|
19
75
|
require "stringio"
|
|
20
76
|
StringIO.new
|
|
21
77
|
end #: IO | StringIO
|
|
78
|
+
|
|
79
|
+
@invoked_shutdown = false #: bool
|
|
80
|
+
@message_queue = Thread::Queue.new #: Thread::Queue
|
|
81
|
+
@writer = Thread.new { write_loop } #: Thread
|
|
22
82
|
end
|
|
23
83
|
|
|
24
84
|
#: -> void
|
|
25
85
|
def shutdown
|
|
86
|
+
# When running in coverage mode, we don't want to inform the extension that we finished immediately after running
|
|
87
|
+
# tests. We only do it after we finish processing coverage results, by invoking `internal_shutdown`
|
|
88
|
+
return if ENV["RUBY_LSP_TEST_RUNNER"] == "coverage"
|
|
89
|
+
|
|
90
|
+
internal_shutdown
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# This method is intended to be used by the RubyLsp::LspReporter class itself only. If you're writing a custom test
|
|
94
|
+
# reporter, use `shutdown` instead
|
|
95
|
+
#: -> void
|
|
96
|
+
def internal_shutdown
|
|
97
|
+
@invoked_shutdown = true
|
|
98
|
+
|
|
26
99
|
send_message("finish")
|
|
100
|
+
@message_queue.close
|
|
101
|
+
@writer.join
|
|
27
102
|
@io.close
|
|
28
103
|
end
|
|
29
104
|
|
|
30
|
-
#: (id: String, uri: URI::Generic) -> void
|
|
31
|
-
def start_test(id:, uri:)
|
|
32
|
-
send_message("start", id: id, uri: uri.to_s)
|
|
105
|
+
#: (id: String, uri: URI::Generic, ?line: Integer?) -> void
|
|
106
|
+
def start_test(id:, uri:, line: nil)
|
|
107
|
+
send_message("start", id: id, uri: uri.to_s, line: line)
|
|
33
108
|
end
|
|
34
109
|
|
|
35
110
|
#: (id: String, uri: URI::Generic) -> void
|
|
@@ -79,14 +154,13 @@ module RubyLsp
|
|
|
79
154
|
# ["Foo", :bar, 6, 21, 6, 65] => 0
|
|
80
155
|
# }
|
|
81
156
|
# }
|
|
82
|
-
#: -> Hash[String,
|
|
157
|
+
#: -> Hash[String, statement_coverage]
|
|
83
158
|
def gather_coverage_results
|
|
84
159
|
# Ignore coverage results inside dependencies
|
|
85
160
|
bundle_path = Bundler.bundle_path.to_s
|
|
86
|
-
default_gems_path = File.dirname(RbConfig::CONFIG["rubylibdir"])
|
|
87
161
|
|
|
88
162
|
result = Coverage.result.reject do |file_path, _coverage_info|
|
|
89
|
-
file_path.start_with?(bundle_path
|
|
163
|
+
file_path.start_with?(bundle_path) || !file_path.start_with?(Dir.pwd)
|
|
90
164
|
end
|
|
91
165
|
|
|
92
166
|
result.to_h do |file_path, coverage_info|
|
|
@@ -129,24 +203,54 @@ module RubyLsp
|
|
|
129
203
|
end
|
|
130
204
|
end
|
|
131
205
|
|
|
206
|
+
#: -> void
|
|
207
|
+
def at_coverage_exit
|
|
208
|
+
coverage_results = gather_coverage_results
|
|
209
|
+
File.write(File.join(".ruby-lsp", "coverage_result.json"), coverage_results.to_json)
|
|
210
|
+
internal_shutdown
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
#: -> void
|
|
214
|
+
def at_exit
|
|
215
|
+
internal_shutdown unless @invoked_shutdown
|
|
216
|
+
end
|
|
217
|
+
|
|
132
218
|
private
|
|
133
219
|
|
|
134
|
-
#: (
|
|
220
|
+
#: (String) -> Socket
|
|
221
|
+
def socket(port)
|
|
222
|
+
socket = Socket.tcp("localhost", port)
|
|
223
|
+
socket.binmode
|
|
224
|
+
socket.sync = true
|
|
225
|
+
socket
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
#: (String?, **untyped) -> void
|
|
135
229
|
def send_message(method_name, **params)
|
|
136
230
|
json_message = { method: method_name, params: params }.to_json
|
|
137
|
-
@
|
|
231
|
+
@message_queue << "Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
#: -> void
|
|
235
|
+
def write_loop
|
|
236
|
+
while (message = @message_queue.pop)
|
|
237
|
+
@io.write(message)
|
|
238
|
+
end
|
|
138
239
|
end
|
|
139
240
|
end
|
|
140
241
|
end
|
|
141
242
|
|
|
142
|
-
if
|
|
243
|
+
if RubyLsp::LspReporter.start_coverage?
|
|
143
244
|
# Auto start coverage when running tests under that profile. This avoids the user from having to configure coverage
|
|
144
245
|
# manually for their project or adding extra dependencies
|
|
145
246
|
require "coverage"
|
|
146
247
|
Coverage.start(:all)
|
|
248
|
+
end
|
|
147
249
|
|
|
250
|
+
if RubyLsp::LspReporter.executed_under_test_runner?
|
|
148
251
|
at_exit do
|
|
149
|
-
|
|
150
|
-
|
|
252
|
+
# Regular finish events are registered per test reporter. However, if the test crashes during loading the files
|
|
253
|
+
# (e.g.: a bad require), we need to ensure that the execution is finalized so that the extension is not left hanging
|
|
254
|
+
RubyLsp::LspReporter.instance.at_exit if $ERROR_INFO
|
|
151
255
|
end
|
|
152
256
|
end
|
|
@@ -8,7 +8,6 @@ rescue LoadError
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
require_relative "lsp_reporter"
|
|
11
|
-
require "ruby_indexer/lib/ruby_indexer/uri"
|
|
12
11
|
|
|
13
12
|
module RubyLsp
|
|
14
13
|
# An override of the default progress reporter in Minitest to add color to the output
|
|
@@ -31,6 +30,30 @@ module RubyLsp
|
|
|
31
30
|
end
|
|
32
31
|
end
|
|
33
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
|
+
|
|
34
57
|
class MinitestReporter < Minitest::AbstractReporter
|
|
35
58
|
class << self
|
|
36
59
|
#: (Hash[untyped, untyped]) -> void
|
|
@@ -46,21 +69,43 @@ module RubyLsp
|
|
|
46
69
|
|
|
47
70
|
# Add the JSON RPC reporter
|
|
48
71
|
reporters << MinitestReporter.new
|
|
72
|
+
PreventReporterOverridePatch.lsp_reporters = reporters
|
|
73
|
+
Minitest.reporter.class.prepend(PreventReporterOverridePatch)
|
|
49
74
|
end
|
|
50
75
|
end
|
|
51
76
|
|
|
52
|
-
#: (
|
|
53
|
-
def prerecord(
|
|
54
|
-
|
|
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.uri_and_line_for(klass.instance_method(method_name))
|
|
55
93
|
return unless uri
|
|
56
94
|
|
|
57
|
-
|
|
95
|
+
id = "#{name}##{handle_spec_test_id(method_name, line)}"
|
|
96
|
+
LspReporter.instance.start_test(id: id, uri: uri, line: line)
|
|
58
97
|
end
|
|
59
98
|
|
|
60
99
|
#: (Minitest::Result result) -> void
|
|
61
100
|
def record(result)
|
|
62
|
-
|
|
63
|
-
|
|
101
|
+
file_path, line = result.source_location
|
|
102
|
+
return unless file_path
|
|
103
|
+
|
|
104
|
+
zero_based_line = line ? line - 1 : nil
|
|
105
|
+
name = handle_spec_test_id(result.name, zero_based_line)
|
|
106
|
+
id = "#{result.klass}##{name}"
|
|
107
|
+
|
|
108
|
+
uri = URI::Generic.from_path(path: File.expand_path(file_path))
|
|
64
109
|
|
|
65
110
|
if result.error?
|
|
66
111
|
message = result.failures.first.message
|
|
@@ -80,26 +125,21 @@ module RubyLsp
|
|
|
80
125
|
LspReporter.instance.shutdown
|
|
81
126
|
end
|
|
82
127
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def uri_from_result(result)
|
|
87
|
-
file = result.source_location[0]
|
|
88
|
-
absolute_path = File.expand_path(file, Dir.pwd)
|
|
89
|
-
URI::Generic.from_path(path: absolute_path)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
#: (singleton(Minitest::Test) test_class, String method_name) -> URI::Generic?
|
|
93
|
-
def uri_from_test_class(test_class, method_name)
|
|
94
|
-
file, _line = test_class.instance_method(method_name).source_location
|
|
95
|
-
return unless file
|
|
96
|
-
|
|
97
|
-
return if file.start_with?("(eval at ") # test is dynamically defined
|
|
98
|
-
|
|
99
|
-
absolute_path = File.expand_path(file, Dir.pwd)
|
|
100
|
-
URI::Generic.from_path(path: absolute_path)
|
|
128
|
+
#: (String, Integer?) -> String
|
|
129
|
+
def handle_spec_test_id(method_name, line)
|
|
130
|
+
method_name.gsub(/(?<=test_)\d{4}(?=_)/, format("%04d", line.to_s))
|
|
101
131
|
end
|
|
102
132
|
end
|
|
103
133
|
end
|
|
104
134
|
|
|
105
135
|
Minitest.extensions << RubyLsp::MinitestReporter
|
|
136
|
+
|
|
137
|
+
if RubyLsp::LspReporter.start_coverage?
|
|
138
|
+
Minitest.after_run do
|
|
139
|
+
RubyLsp::LspReporter.instance.at_coverage_exit
|
|
140
|
+
end
|
|
141
|
+
elsif RubyLsp::LspReporter.executed_under_test_runner?
|
|
142
|
+
Minitest.after_run do
|
|
143
|
+
RubyLsp::LspReporter.instance.at_exit
|
|
144
|
+
end
|
|
145
|
+
end
|