ruby-lsp 0.23.10 → 0.23.13
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/README.md +2 -2
- data/VERSION +1 -1
- data/exe/ruby-lsp-launcher +12 -11
- data/lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb +1 -1
- data/lib/rubocop/cop/ruby_lsp/use_register_with_handler_method.rb +3 -5
- data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +81 -115
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +117 -166
- data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +9 -7
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +89 -201
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +63 -192
- data/lib/ruby_indexer/lib/ruby_indexer/location.rb +4 -27
- data/lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb +14 -16
- data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +22 -45
- data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +42 -60
- data/lib/ruby_indexer/lib/ruby_indexer/uri.rb +9 -16
- data/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb +5 -9
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +75 -0
- data/lib/ruby_indexer/test/configuration_test.rb +42 -3
- data/lib/ruby_indexer/test/index_test.rb +21 -0
- data/lib/ruby_indexer/test/method_test.rb +28 -2
- data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
- data/lib/ruby_lsp/addon.rb +44 -71
- data/lib/ruby_lsp/base_server.rb +31 -33
- data/lib/ruby_lsp/client_capabilities.rb +10 -12
- data/lib/ruby_lsp/document.rb +34 -45
- data/lib/ruby_lsp/erb_document.rb +24 -36
- data/lib/ruby_lsp/global_state.rb +51 -56
- data/lib/ruby_lsp/internal.rb +6 -0
- data/lib/ruby_lsp/listeners/code_lens.rb +81 -88
- data/lib/ruby_lsp/listeners/completion.rb +36 -55
- data/lib/ruby_lsp/listeners/definition.rb +37 -51
- data/lib/ruby_lsp/listeners/document_highlight.rb +123 -150
- data/lib/ruby_lsp/listeners/document_link.rb +43 -62
- data/lib/ruby_lsp/listeners/document_symbol.rb +35 -49
- data/lib/ruby_lsp/listeners/folding_ranges.rb +32 -39
- data/lib/ruby_lsp/listeners/hover.rb +81 -100
- data/lib/ruby_lsp/listeners/inlay_hints.rb +4 -11
- data/lib/ruby_lsp/listeners/semantic_highlighting.rb +42 -51
- data/lib/ruby_lsp/listeners/signature_help.rb +6 -25
- data/lib/ruby_lsp/listeners/spec_style.rb +155 -0
- data/lib/ruby_lsp/listeners/test_discovery.rb +89 -0
- data/lib/ruby_lsp/listeners/test_style.rb +236 -0
- data/lib/ruby_lsp/node_context.rb +12 -39
- data/lib/ruby_lsp/rbs_document.rb +8 -6
- data/lib/ruby_lsp/requests/code_action_resolve.rb +10 -10
- data/lib/ruby_lsp/requests/code_actions.rb +14 -26
- data/lib/ruby_lsp/requests/code_lens.rb +6 -17
- data/lib/ruby_lsp/requests/completion.rb +7 -20
- data/lib/ruby_lsp/requests/completion_resolve.rb +5 -5
- data/lib/ruby_lsp/requests/definition.rb +8 -17
- data/lib/ruby_lsp/requests/diagnostics.rb +8 -11
- data/lib/ruby_lsp/requests/discover_tests.rb +75 -0
- data/lib/ruby_lsp/requests/document_highlight.rb +5 -15
- data/lib/ruby_lsp/requests/document_link.rb +6 -17
- data/lib/ruby_lsp/requests/document_symbol.rb +5 -8
- data/lib/ruby_lsp/requests/folding_ranges.rb +7 -15
- data/lib/ruby_lsp/requests/formatting.rb +6 -9
- data/lib/ruby_lsp/requests/go_to_relevant_file.rb +87 -0
- data/lib/ruby_lsp/requests/hover.rb +8 -18
- data/lib/ruby_lsp/requests/inlay_hints.rb +6 -17
- data/lib/ruby_lsp/requests/on_type_formatting.rb +28 -38
- data/lib/ruby_lsp/requests/prepare_rename.rb +4 -9
- data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +4 -13
- data/lib/ruby_lsp/requests/range_formatting.rb +5 -6
- data/lib/ruby_lsp/requests/references.rb +6 -36
- data/lib/ruby_lsp/requests/rename.rb +11 -37
- data/lib/ruby_lsp/requests/request.rb +7 -19
- data/lib/ruby_lsp/requests/selection_ranges.rb +5 -5
- data/lib/ruby_lsp/requests/semantic_highlighting.rb +12 -31
- data/lib/ruby_lsp/requests/show_syntax_tree.rb +5 -6
- data/lib/ruby_lsp/requests/signature_help.rb +8 -26
- data/lib/ruby_lsp/requests/support/annotation.rb +4 -10
- data/lib/ruby_lsp/requests/support/common.rb +13 -48
- data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +27 -35
- data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +9 -12
- data/lib/ruby_lsp/requests/support/rubocop_runner.rb +22 -34
- data/lib/ruby_lsp/requests/support/selection_range.rb +1 -3
- data/lib/ruby_lsp/requests/support/sorbet.rb +29 -38
- data/lib/ruby_lsp/requests/support/source_uri.rb +16 -30
- data/lib/ruby_lsp/requests/support/syntax_tree_formatter.rb +12 -19
- data/lib/ruby_lsp/requests/support/test_item.rb +55 -0
- data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +5 -6
- data/lib/ruby_lsp/requests/workspace_symbol.rb +4 -4
- data/lib/ruby_lsp/response_builders/collection_response_builder.rb +5 -5
- data/lib/ruby_lsp/response_builders/document_symbol.rb +10 -16
- data/lib/ruby_lsp/response_builders/hover.rb +10 -13
- data/lib/ruby_lsp/response_builders/response_builder.rb +1 -1
- data/lib/ruby_lsp/response_builders/semantic_highlighting.rb +59 -87
- data/lib/ruby_lsp/response_builders/signature_help.rb +5 -6
- data/lib/ruby_lsp/response_builders/test_collection.rb +34 -0
- data/lib/ruby_lsp/ruby_document.rb +22 -60
- data/lib/ruby_lsp/ruby_lsp_reporter_plugin.rb +109 -0
- data/lib/ruby_lsp/scope.rb +7 -11
- data/lib/ruby_lsp/server.rb +177 -72
- data/lib/ruby_lsp/setup_bundler.rb +61 -59
- data/lib/ruby_lsp/static_docs.rb +4 -7
- data/lib/ruby_lsp/store.rb +21 -40
- data/lib/ruby_lsp/test_helper.rb +3 -12
- data/lib/ruby_lsp/test_reporter.rb +207 -0
- data/lib/ruby_lsp/test_unit_test_runner.rb +98 -0
- data/lib/ruby_lsp/type_inferrer.rb +9 -13
- data/lib/ruby_lsp/utils.rb +37 -81
- metadata +13 -3
@@ -25,50 +25,47 @@ module RubyLsp
|
|
25
25
|
class BundleNotLocked < StandardError; end
|
26
26
|
class BundleInstallFailure < StandardError; end
|
27
27
|
|
28
|
-
FOUR_HOURS =
|
28
|
+
FOUR_HOURS = 4 * 60 * 60 #: Integer
|
29
29
|
|
30
|
-
|
30
|
+
#: (String project_path, **untyped options) -> void
|
31
31
|
def initialize(project_path, **options)
|
32
32
|
@project_path = project_path
|
33
|
-
@branch =
|
34
|
-
@launcher =
|
33
|
+
@branch = options[:branch] #: String?
|
34
|
+
@launcher = options[:launcher] #: bool?
|
35
35
|
patch_thor_to_print_progress_to_stderr! if @launcher
|
36
36
|
|
37
37
|
# Regular bundle paths
|
38
|
-
@gemfile =
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
T.nilable(Pathname),
|
45
|
-
)
|
46
|
-
@lockfile = T.let(@gemfile ? Bundler.default_lockfile : nil, T.nilable(Pathname))
|
38
|
+
@gemfile = begin
|
39
|
+
Bundler.default_gemfile
|
40
|
+
rescue Bundler::GemfileNotFound
|
41
|
+
nil
|
42
|
+
end #: Pathname?
|
43
|
+
@lockfile = @gemfile ? Bundler.default_lockfile : nil #: Pathname?
|
47
44
|
|
48
|
-
@gemfile_hash =
|
49
|
-
@lockfile_hash =
|
45
|
+
@gemfile_hash = @gemfile ? Digest::SHA256.hexdigest(@gemfile.read) : nil #: String?
|
46
|
+
@lockfile_hash = @lockfile&.exist? ? Digest::SHA256.hexdigest(@lockfile.read) : nil #: String?
|
50
47
|
|
51
|
-
@gemfile_name =
|
48
|
+
@gemfile_name = @gemfile&.basename&.to_s || "Gemfile" #: String
|
52
49
|
|
53
50
|
# Custom bundle paths
|
54
|
-
@custom_dir =
|
55
|
-
@custom_gemfile =
|
56
|
-
@custom_lockfile =
|
57
|
-
@lockfile_hash_path =
|
58
|
-
@last_updated_path =
|
59
|
-
@error_path =
|
60
|
-
@already_composed_path =
|
51
|
+
@custom_dir = Pathname.new(".ruby-lsp").expand_path(@project_path) #: Pathname
|
52
|
+
@custom_gemfile = @custom_dir + @gemfile_name #: Pathname
|
53
|
+
@custom_lockfile = @custom_dir + (@lockfile&.basename || "Gemfile.lock") #: Pathname
|
54
|
+
@lockfile_hash_path = @custom_dir + "main_lockfile_hash" #: Pathname
|
55
|
+
@last_updated_path = @custom_dir + "last_updated" #: Pathname
|
56
|
+
@error_path = @custom_dir + "install_error" #: Pathname
|
57
|
+
@already_composed_path = @custom_dir + "bundle_is_composed" #: Pathname
|
61
58
|
|
62
59
|
dependencies, bundler_version = load_dependencies
|
63
|
-
@dependencies =
|
64
|
-
@bundler_version =
|
65
|
-
@rails_app =
|
66
|
-
@retry =
|
60
|
+
@dependencies = dependencies #: Hash[String, untyped]
|
61
|
+
@bundler_version = bundler_version #: Gem::Version?
|
62
|
+
@rails_app = rails_app? #: bool
|
63
|
+
@retry = false #: bool
|
67
64
|
end
|
68
65
|
|
69
66
|
# Sets up the composed bundle and returns the `BUNDLE_GEMFILE`, `BUNDLE_PATH` and `BUNDLE_APP_CONFIG` that should be
|
70
67
|
# used for running the server
|
71
|
-
|
68
|
+
#: -> Hash[String, String]
|
72
69
|
def setup!
|
73
70
|
raise BundleNotLocked if !@launcher && @gemfile&.exist? && !@lockfile&.exist?
|
74
71
|
|
@@ -128,26 +125,23 @@ module RubyLsp
|
|
128
125
|
|
129
126
|
private
|
130
127
|
|
131
|
-
|
128
|
+
#: -> Hash[String, untyped]
|
132
129
|
def composed_bundle_dependencies
|
133
|
-
@composed_bundle_dependencies ||=
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
end,
|
146
|
-
T.nilable(T::Hash[String, T.untyped]),
|
147
|
-
)
|
130
|
+
@composed_bundle_dependencies ||= begin
|
131
|
+
original_bundle_gemfile = ENV["BUNDLE_GEMFILE"]
|
132
|
+
|
133
|
+
if @custom_lockfile.exist?
|
134
|
+
ENV["BUNDLE_GEMFILE"] = @custom_gemfile.to_s
|
135
|
+
Bundler::LockfileParser.new(@custom_lockfile.read).dependencies
|
136
|
+
else
|
137
|
+
{}
|
138
|
+
end
|
139
|
+
ensure
|
140
|
+
ENV["BUNDLE_GEMFILE"] = original_bundle_gemfile
|
141
|
+
end #: Hash[String, untyped]?
|
148
142
|
end
|
149
143
|
|
150
|
-
|
144
|
+
#: -> void
|
151
145
|
def write_custom_gemfile
|
152
146
|
parts = [
|
153
147
|
"# This custom gemfile is automatically generated by the Ruby LSP.",
|
@@ -158,7 +152,8 @@ module RubyLsp
|
|
158
152
|
# If there's a top level Gemfile, we want to evaluate from the composed bundle. We get the source from the top
|
159
153
|
# level Gemfile, so if there isn't one we need to add a default source
|
160
154
|
if @gemfile&.exist? && @lockfile&.exist?
|
161
|
-
|
155
|
+
gemfile_path = @gemfile.relative_path_from(@custom_dir.realpath)
|
156
|
+
parts << "eval_gemfile(File.expand_path(\"#{gemfile_path}\", __dir__))"
|
162
157
|
else
|
163
158
|
parts.unshift('source "https://rubygems.org"')
|
164
159
|
end
|
@@ -187,7 +182,7 @@ module RubyLsp
|
|
187
182
|
@custom_gemfile.write(content) unless @custom_gemfile.exist? && @custom_gemfile.read == content
|
188
183
|
end
|
189
184
|
|
190
|
-
|
185
|
+
#: -> [Hash[String, untyped], Gem::Version?]
|
191
186
|
def load_dependencies
|
192
187
|
return [{}, nil] unless @lockfile&.exist?
|
193
188
|
|
@@ -207,7 +202,7 @@ module RubyLsp
|
|
207
202
|
[dependencies, lockfile_parser.bundler_version]
|
208
203
|
end
|
209
204
|
|
210
|
-
|
205
|
+
#: (?Pathname? bundle_gemfile) -> Hash[String, String]
|
211
206
|
def run_bundle_install(bundle_gemfile = @gemfile)
|
212
207
|
env = bundler_settings_as_env
|
213
208
|
env["BUNDLE_GEMFILE"] = bundle_gemfile.to_s
|
@@ -258,7 +253,7 @@ module RubyLsp
|
|
258
253
|
env
|
259
254
|
end
|
260
255
|
|
261
|
-
|
256
|
+
#: (Hash[String, String] env, ?force_install: bool) -> Hash[String, String]
|
262
257
|
def run_bundle_install_directly(env, force_install: false)
|
263
258
|
RubyVM::YJIT.enable if defined?(RubyVM::YJIT.enable)
|
264
259
|
|
@@ -268,7 +263,7 @@ module RubyLsp
|
|
268
263
|
T.unsafe(ENV).merge!(env)
|
269
264
|
|
270
265
|
unless should_update && !force_install
|
271
|
-
Bundler::CLI::Install.new({}).run
|
266
|
+
Bundler::CLI::Install.new({ "no-cache" => true }).run
|
272
267
|
correct_relative_remote_paths if @custom_lockfile.exist?
|
273
268
|
return env
|
274
269
|
end
|
@@ -287,7 +282,7 @@ module RubyLsp
|
|
287
282
|
@retry ? env : run_bundle_install_directly(env, force_install: true)
|
288
283
|
end
|
289
284
|
|
290
|
-
|
285
|
+
#: (Hash[String, String] env) -> Hash[String, String]
|
291
286
|
def run_bundle_install_through_command(env)
|
292
287
|
# If `ruby-lsp` and `debug` (and potentially `ruby-lsp-rails`) are already in the Gemfile, then we shouldn't try
|
293
288
|
# to upgrade them or else we'll produce undesired source control changes. If the composed bundle was just created
|
@@ -344,7 +339,7 @@ module RubyLsp
|
|
344
339
|
end
|
345
340
|
|
346
341
|
# Gather all Bundler settings (global and local) and return them as a hash that can be used as the environment
|
347
|
-
|
342
|
+
#: -> Hash[String, String]
|
348
343
|
def bundler_settings_as_env
|
349
344
|
local_config_path = File.join(@project_path, ".bundle")
|
350
345
|
|
@@ -356,18 +351,25 @@ module RubyLsp
|
|
356
351
|
Bundler::Settings.new
|
357
352
|
end
|
358
353
|
|
354
|
+
# List of Bundler settings that don't make sense for the composed bundle and are better controlled manually by the
|
355
|
+
# user
|
356
|
+
ignored_settings = ["bin", "cache_all", "cache_all_platforms"]
|
357
|
+
|
359
358
|
# Map all settings to their environment variable names with `key_for` and their values. For example, the if the
|
360
359
|
# setting name `e` is `path` with a value of `vendor/bundle`, then it will return `"BUNDLE_PATH" =>
|
361
360
|
# "vendor/bundle"`
|
362
|
-
settings
|
363
|
-
|
361
|
+
settings
|
362
|
+
.all
|
363
|
+
.reject { |setting| ignored_settings.include?(setting) }
|
364
|
+
.to_h do |e|
|
365
|
+
key = settings.key_for(e)
|
364
366
|
value = Array(settings[e]).join(":").tr(" ", ":")
|
365
367
|
|
366
368
|
[key, value]
|
367
369
|
end
|
368
370
|
end
|
369
371
|
|
370
|
-
|
372
|
+
#: -> void
|
371
373
|
def install_bundler_if_needed
|
372
374
|
# Try to find the bundler version specified in the lockfile in installed gems. If not found, install it
|
373
375
|
requirement = Gem::Requirement.new(@bundler_version.to_s)
|
@@ -376,7 +378,7 @@ module RubyLsp
|
|
376
378
|
Gem.install("bundler", @bundler_version.to_s)
|
377
379
|
end
|
378
380
|
|
379
|
-
|
381
|
+
#: -> bool
|
380
382
|
def should_bundle_update?
|
381
383
|
# If `ruby-lsp`, `ruby-lsp-rails` and `debug` are in the Gemfile, then we shouldn't try to upgrade them or else it
|
382
384
|
# will produce version control changes
|
@@ -399,7 +401,7 @@ module RubyLsp
|
|
399
401
|
|
400
402
|
# When a lockfile has remote references based on relative file paths, we need to ensure that they are pointing to
|
401
403
|
# the correct place since after copying the relative path is no longer valid
|
402
|
-
|
404
|
+
#: -> void
|
403
405
|
def correct_relative_remote_paths
|
404
406
|
content = @custom_lockfile.read
|
405
407
|
content.gsub!(/remote: (.*)/) do |match|
|
@@ -421,7 +423,7 @@ module RubyLsp
|
|
421
423
|
end
|
422
424
|
|
423
425
|
# Detects if the project is a Rails app by looking if the superclass of the main class is `Rails::Application`
|
424
|
-
|
426
|
+
#: -> bool
|
425
427
|
def rails_app?
|
426
428
|
config = Pathname.new("config/application.rb").expand_path
|
427
429
|
application_contents = config.read(external_encoding: Encoding::UTF_8) if config.exist?
|
@@ -430,7 +432,7 @@ module RubyLsp
|
|
430
432
|
/class .* < (::)?Rails::Application/.match?(application_contents)
|
431
433
|
end
|
432
434
|
|
433
|
-
|
435
|
+
#: -> void
|
434
436
|
def patch_thor_to_print_progress_to_stderr!
|
435
437
|
return unless defined?(Bundler::Thor::Shell::Basic)
|
436
438
|
|
data/lib/ruby_lsp/static_docs.rb
CHANGED
@@ -3,13 +3,10 @@
|
|
3
3
|
|
4
4
|
module RubyLsp
|
5
5
|
# The path to the `static_docs` directory, where we keep long-form static documentation
|
6
|
-
STATIC_DOCS_PATH =
|
6
|
+
STATIC_DOCS_PATH = File.join(File.dirname(File.dirname(T.must(__dir__))), "static_docs") #: String
|
7
7
|
|
8
8
|
# A map of keyword => short documentation to be displayed on hover or completion
|
9
|
-
KEYWORD_DOCS =
|
10
|
-
|
11
|
-
|
12
|
-
}.freeze,
|
13
|
-
T::Hash[String, String],
|
14
|
-
)
|
9
|
+
KEYWORD_DOCS = {
|
10
|
+
"yield" => "Invokes the passed block with the given arguments",
|
11
|
+
}.freeze #: Hash[String, String]
|
15
12
|
end
|
data/lib/ruby_lsp/store.rb
CHANGED
@@ -3,34 +3,29 @@
|
|
3
3
|
|
4
4
|
module RubyLsp
|
5
5
|
class Store
|
6
|
-
extend T::Sig
|
7
|
-
|
8
6
|
class NonExistingDocumentError < StandardError; end
|
9
7
|
|
10
|
-
|
8
|
+
#: Hash[Symbol, RequestConfig]
|
11
9
|
attr_accessor :features_configuration
|
12
10
|
|
13
|
-
|
11
|
+
#: String
|
14
12
|
attr_accessor :client_name
|
15
13
|
|
16
|
-
|
14
|
+
#: (GlobalState global_state) -> void
|
17
15
|
def initialize(global_state)
|
18
16
|
@global_state = global_state
|
19
|
-
@state =
|
20
|
-
@features_configuration =
|
21
|
-
{
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
T::Hash[Symbol, RequestConfig],
|
29
|
-
)
|
30
|
-
@client_name = T.let("Unknown", String)
|
17
|
+
@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
|
+
@client_name = "Unknown" #: String
|
31
26
|
end
|
32
27
|
|
33
|
-
|
28
|
+
#: (URI::Generic uri) -> Document[untyped]
|
34
29
|
def get(uri)
|
35
30
|
document = @state[uri.to_s]
|
36
31
|
return document unless document.nil?
|
@@ -56,14 +51,7 @@ module RubyLsp
|
|
56
51
|
raise NonExistingDocumentError, uri.to_s
|
57
52
|
end
|
58
53
|
|
59
|
-
|
60
|
-
params(
|
61
|
-
uri: URI::Generic,
|
62
|
-
source: String,
|
63
|
-
version: Integer,
|
64
|
-
language_id: Document::LanguageId,
|
65
|
-
).returns(Document[T.untyped])
|
66
|
-
end
|
54
|
+
#: (uri: URI::Generic, source: String, version: Integer, language_id: Document::LanguageId) -> Document[untyped]
|
67
55
|
def set(uri:, source:, version:, language_id:)
|
68
56
|
@state[uri.to_s] = case language_id
|
69
57
|
when Document::LanguageId::ERB
|
@@ -75,46 +63,39 @@ module RubyLsp
|
|
75
63
|
end
|
76
64
|
end
|
77
65
|
|
78
|
-
|
66
|
+
#: (uri: URI::Generic, edits: Array[Hash[Symbol, untyped]], version: Integer) -> void
|
79
67
|
def push_edits(uri:, edits:, version:)
|
80
68
|
T.must(@state[uri.to_s]).push_edits(edits, version: version)
|
81
69
|
end
|
82
70
|
|
83
|
-
|
71
|
+
#: -> void
|
84
72
|
def clear
|
85
73
|
@state.clear
|
86
74
|
end
|
87
75
|
|
88
|
-
|
76
|
+
#: -> bool
|
89
77
|
def empty?
|
90
78
|
@state.empty?
|
91
79
|
end
|
92
80
|
|
93
|
-
|
81
|
+
#: (URI::Generic uri) -> void
|
94
82
|
def delete(uri)
|
95
83
|
@state.delete(uri.to_s)
|
96
84
|
end
|
97
85
|
|
98
|
-
|
86
|
+
#: (URI::Generic uri) -> bool
|
99
87
|
def key?(uri)
|
100
88
|
@state.key?(uri.to_s)
|
101
89
|
end
|
102
90
|
|
103
|
-
|
91
|
+
#: { (String uri, Document[untyped] document) -> void } -> void
|
104
92
|
def each(&block)
|
105
93
|
@state.each do |uri, document|
|
106
94
|
block.call(uri, document)
|
107
95
|
end
|
108
96
|
end
|
109
97
|
|
110
|
-
|
111
|
-
type_parameters(:T)
|
112
|
-
.params(
|
113
|
-
uri: URI::Generic,
|
114
|
-
request_name: String,
|
115
|
-
block: T.proc.params(document: Document[T.untyped]).returns(T.type_parameter(:T)),
|
116
|
-
).returns(T.type_parameter(:T))
|
117
|
-
end
|
98
|
+
#: [T] (URI::Generic uri, String request_name) { (Document[untyped] document) -> T } -> T
|
118
99
|
def cache_fetch(uri, request_name, &block)
|
119
100
|
get(uri).cache_fetch(request_name, &block)
|
120
101
|
end
|
data/lib/ruby_lsp/test_helper.rb
CHANGED
@@ -7,21 +7,11 @@ module RubyLsp
|
|
7
7
|
module TestHelper
|
8
8
|
class TestError < StandardError; end
|
9
9
|
|
10
|
-
extend T::Sig
|
11
10
|
extend T::Helpers
|
12
11
|
|
13
12
|
requires_ancestor { Kernel }
|
14
13
|
|
15
|
-
|
16
|
-
type_parameters(:T)
|
17
|
-
.params(
|
18
|
-
source: T.nilable(String),
|
19
|
-
uri: URI::Generic,
|
20
|
-
stub_no_typechecker: T::Boolean,
|
21
|
-
load_addons: T::Boolean,
|
22
|
-
block: T.proc.params(server: RubyLsp::Server, uri: URI::Generic).returns(T.type_parameter(:T)),
|
23
|
-
).returns(T.type_parameter(:T))
|
24
|
-
end
|
14
|
+
#: [T] (?String? source, ?URI::Generic uri, ?stub_no_typechecker: bool, ?load_addons: bool) { (RubyLsp::Server server, URI::Generic uri) -> T } -> T
|
25
15
|
def with_server(source = nil, uri = Kernel.URI("file:///fake.rb"), stub_no_typechecker: false, load_addons: true,
|
26
16
|
&block)
|
27
17
|
server = RubyLsp::Server.new(test_mode: true)
|
@@ -52,13 +42,14 @@ module RubyLsp
|
|
52
42
|
ensure
|
53
43
|
if load_addons
|
54
44
|
RubyLsp::Addon.addons.each(&:deactivate)
|
45
|
+
RubyLsp::Addon.addon_classes.clear
|
55
46
|
RubyLsp::Addon.addons.clear
|
56
47
|
end
|
57
48
|
server.run_shutdown
|
58
49
|
end
|
59
50
|
end
|
60
51
|
|
61
|
-
|
52
|
+
#: (RubyLsp::Server server) -> RubyLsp::Result
|
62
53
|
def pop_result(server)
|
63
54
|
result = server.pop_response
|
64
55
|
result = server.pop_response until result.is_a?(RubyLsp::Result) || result.is_a?(RubyLsp::Error)
|
@@ -0,0 +1,207 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "json"
|
5
|
+
require "delegate"
|
6
|
+
|
7
|
+
$stdout.binmode
|
8
|
+
$stdout.sync = true
|
9
|
+
$stderr.binmode
|
10
|
+
$stderr.sync = true
|
11
|
+
|
12
|
+
module RubyLsp
|
13
|
+
module TestReporter
|
14
|
+
class << self
|
15
|
+
#: (id: String, uri: URI::Generic) -> void
|
16
|
+
def start_test(id:, uri:)
|
17
|
+
params = {
|
18
|
+
id: id,
|
19
|
+
uri: uri.to_s,
|
20
|
+
}
|
21
|
+
send_message("start", params)
|
22
|
+
end
|
23
|
+
|
24
|
+
#: (id: String, uri: URI::Generic) -> void
|
25
|
+
def record_pass(id:, uri:)
|
26
|
+
params = {
|
27
|
+
id: id,
|
28
|
+
uri: uri.to_s,
|
29
|
+
}
|
30
|
+
send_message("pass", params)
|
31
|
+
end
|
32
|
+
|
33
|
+
#: (id: String, message: String, uri: URI::Generic) -> void
|
34
|
+
def record_fail(id:, message:, uri:)
|
35
|
+
params = {
|
36
|
+
id: id,
|
37
|
+
message: message,
|
38
|
+
uri: uri.to_s,
|
39
|
+
}
|
40
|
+
send_message("fail", params)
|
41
|
+
end
|
42
|
+
|
43
|
+
#: (id: String, uri: URI::Generic) -> void
|
44
|
+
def record_skip(id:, uri:)
|
45
|
+
params = {
|
46
|
+
id: id,
|
47
|
+
uri: uri.to_s,
|
48
|
+
}
|
49
|
+
send_message("skip", params)
|
50
|
+
end
|
51
|
+
|
52
|
+
#: (id: String, message: String?, uri: URI::Generic) -> void
|
53
|
+
def record_error(id:, message:, uri:)
|
54
|
+
params = {
|
55
|
+
id: id,
|
56
|
+
message: message,
|
57
|
+
uri: uri.to_s,
|
58
|
+
}
|
59
|
+
send_message("error", params)
|
60
|
+
end
|
61
|
+
|
62
|
+
#: (message: String) -> void
|
63
|
+
def append_output(message:)
|
64
|
+
params = {
|
65
|
+
message: message,
|
66
|
+
}
|
67
|
+
send_message("append_output", params)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Gather the results returned by Coverage.result and format like the VS Code test explorer expects
|
71
|
+
#
|
72
|
+
# Coverage result format:
|
73
|
+
#
|
74
|
+
# Lines are reported in order as an array where each number is the number of times it was executed. For example,
|
75
|
+
# the following says that line 0 was executed 1 time and line 1 executed 3 times: [1, 3].
|
76
|
+
# Nil values represent lines for which coverage is not available, like empty lines, comments or keywords like
|
77
|
+
# `else`
|
78
|
+
#
|
79
|
+
# Branches are a hash containing the name of the branch and the location where it is found in tuples with the
|
80
|
+
# following elements: [NAME, ID, START_LINE, START_COLUMN, END_LINE, END_COLUMN] as the keys and the value is the
|
81
|
+
# number of times it was executed
|
82
|
+
#
|
83
|
+
# Methods are a similar hash [ClassName, :method_name, START_LINE, START_COLUMN, END_LINE, END_COLUMN] => NUMBER
|
84
|
+
# OF EXECUTIONS
|
85
|
+
#
|
86
|
+
# Example:
|
87
|
+
# {
|
88
|
+
# "file_path" => {
|
89
|
+
# "lines" => [1, 2, 3, nil],
|
90
|
+
# "branches" => {
|
91
|
+
# ["&.", 0, 6, 21, 6, 65] => { [:then, 1, 6, 21, 6, 65] => 0, [:else, 5, 7, 0, 7, 87] => 1 }
|
92
|
+
# },
|
93
|
+
# "methods" => {
|
94
|
+
# ["Foo", :bar, 6, 21, 6, 65] => 0
|
95
|
+
# }
|
96
|
+
# }
|
97
|
+
#: () -> Hash[String, StatementCoverage]
|
98
|
+
def gather_coverage_results
|
99
|
+
# Ignore coverage results inside dependencies
|
100
|
+
bundle_path = Bundler.bundle_path.to_s
|
101
|
+
default_gems_path = File.dirname(RbConfig::CONFIG["rubylibdir"])
|
102
|
+
|
103
|
+
result = Coverage.result.reject do |file_path, _coverage_info|
|
104
|
+
file_path.start_with?(bundle_path) ||
|
105
|
+
file_path.start_with?(default_gems_path) ||
|
106
|
+
file_path.start_with?("eval")
|
107
|
+
end
|
108
|
+
|
109
|
+
result.to_h do |file_path, coverage_info|
|
110
|
+
# Format the branch coverage information as VS Code expects it and then group it based on the start line of
|
111
|
+
# the conditional that causes the branching. We need to match each line coverage data with the branches that
|
112
|
+
# spawn from that line
|
113
|
+
branch_by_line = coverage_info[:branches]
|
114
|
+
.flat_map do |branch, data|
|
115
|
+
branch_name, _branch_id, branch_start_line, _branch_start_col, _branch_end_line, _branch_end_col = branch
|
116
|
+
|
117
|
+
data.map do |then_or_else, execution_count|
|
118
|
+
name, _id, start_line, start_column, end_line, end_column = then_or_else
|
119
|
+
|
120
|
+
{
|
121
|
+
groupingLine: branch_start_line,
|
122
|
+
executed: execution_count,
|
123
|
+
location: {
|
124
|
+
start: { line: start_line, character: start_column },
|
125
|
+
end: { line: end_line, character: end_column },
|
126
|
+
},
|
127
|
+
label: "#{branch_name} #{name}",
|
128
|
+
}
|
129
|
+
end
|
130
|
+
end
|
131
|
+
.group_by { |branch| branch[:groupingLine] }
|
132
|
+
|
133
|
+
# Format the line coverage information, gathering any branch coverage data associated with that line
|
134
|
+
data = coverage_info[:lines].filter_map.with_index do |execution_count, line_index|
|
135
|
+
next if execution_count.nil?
|
136
|
+
|
137
|
+
{
|
138
|
+
executed: execution_count,
|
139
|
+
location: { line: line_index, character: 0 },
|
140
|
+
branches: branch_by_line[line_index] || [],
|
141
|
+
}
|
142
|
+
end
|
143
|
+
|
144
|
+
# The expected format is URI => { executed: number_of_times_executed, location: { ... }, branches: [ ... ] }
|
145
|
+
[URI::Generic.from_path(path: File.expand_path(file_path)).to_s, data]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
#: (method_name: String?, params: Hash[String, untyped]) -> void
|
152
|
+
def send_message(method_name, params)
|
153
|
+
json_message = { method: method_name, params: params }.to_json
|
154
|
+
ORIGINAL_STDOUT.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
ORIGINAL_STDOUT = $stdout #: IO
|
159
|
+
|
160
|
+
class IOWrapper < SimpleDelegator
|
161
|
+
#: (Object) -> void
|
162
|
+
def puts(*args)
|
163
|
+
args.each { |arg| log(convert_line_breaks(arg) + "\r\n") }
|
164
|
+
end
|
165
|
+
|
166
|
+
#: (Object) -> void
|
167
|
+
def print(*args)
|
168
|
+
args.each { |arg| log(convert_line_breaks(arg)) }
|
169
|
+
end
|
170
|
+
|
171
|
+
#: (Object) -> void
|
172
|
+
def write(*args)
|
173
|
+
args.each { |arg| log(convert_line_breaks(arg)) }
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
#: (Object) -> String
|
179
|
+
def convert_line_breaks(message)
|
180
|
+
message.to_s.gsub("\n", "\r\n")
|
181
|
+
end
|
182
|
+
|
183
|
+
#: (String) -> void
|
184
|
+
def log(message)
|
185
|
+
TestReporter.append_output(message: message)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
if ENV["RUBY_LSP_TEST_RUNNER"]
|
192
|
+
# We wrap the default output stream so that we can capture anything written to stdout and emit it as part of the JSON
|
193
|
+
# event stream.
|
194
|
+
$> = RubyLsp::TestReporter::IOWrapper.new($stdout)
|
195
|
+
|
196
|
+
if ENV["RUBY_LSP_TEST_RUNNER"] == "coverage"
|
197
|
+
# Auto start coverage when running tests under that profile. This avoids the user from having to configure coverage
|
198
|
+
# manually for their project or adding extra dependencies
|
199
|
+
require "coverage"
|
200
|
+
Coverage.start(:all)
|
201
|
+
|
202
|
+
at_exit do
|
203
|
+
coverage_results = RubyLsp::TestReporter.gather_coverage_results
|
204
|
+
File.write(File.join(".ruby-lsp", "coverage_result.json"), coverage_results.to_json)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|