ruby-lsp 0.5.1 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +4 -0
- data/VERSION +1 -1
- data/exe/ruby-lsp +33 -1
- data/lib/ruby_lsp/check_docs.rb +23 -5
- data/lib/ruby_lsp/event_emitter.rb +61 -0
- data/lib/ruby_lsp/executor.rb +31 -46
- data/lib/ruby_lsp/requests/base_request.rb +2 -6
- data/lib/ruby_lsp/requests/code_lens.rb +125 -33
- data/lib/ruby_lsp/requests/diagnostics.rb +2 -0
- data/lib/ruby_lsp/requests/folding_ranges.rb +2 -1
- data/lib/ruby_lsp/requests/formatting.rb +30 -11
- data/lib/ruby_lsp/requests/inlay_hints.rb +18 -16
- data/lib/ruby_lsp/requests/semantic_highlighting.rb +85 -113
- data/lib/ruby_lsp/requests/support/common.rb +6 -17
- data/lib/ruby_lsp/requests/support/dependency_detector.rb +41 -0
- data/lib/ruby_lsp/requests/support/formatter_runner.rb +18 -0
- data/lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb +2 -1
- data/lib/ruby_lsp/requests/support/source_uri.rb +1 -1
- data/lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb +2 -1
- data/lib/ruby_lsp/requests.rb +1 -0
- data/lib/ruby_lsp/server.rb +5 -2
- data/lib/ruby_lsp/utils.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 11dd3401d408ed456da9b4dd0aca906b991a45a0103ca8786eafbf34d4659c85
|
4
|
+
data.tar.gz: 8469dd62a3660965daee589835e1481fe09e17b55cbd9d61c461d4ba94db833e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6d2e0c3d9c5bf11068b1d5e3092d4b5f3b233066a33b7e74ea218f115fbb8b154798a8b8b0634569781245f1dedcb6da3e5ca3a23db04f78fc105c496d2a3280
|
7
|
+
data.tar.gz: 529ce841833605ac40c67536bbca1eb1a9568b742de854f4698ced147813ec63569fd926d4035d518f7bb0ab54d2bbd4665e7ad2564923b49036573a0d4625e7
|
data/README.md
CHANGED
@@ -39,10 +39,14 @@ end
|
|
39
39
|
See the [documentation](https://shopify.github.io/ruby-lsp) for more in-depth details about the
|
40
40
|
[supported features](https://shopify.github.io/ruby-lsp/RubyLsp/Requests.html).
|
41
41
|
|
42
|
+
For creating rich themes for Ruby using the semantic highlighting information, see the [semantic highlighting
|
43
|
+
documentation](SEMANTIC_HIGHLIGHTING.md).
|
44
|
+
|
42
45
|
## Learn More
|
43
46
|
|
44
47
|
* [RubyConf 2022: Improving the development experience with language servers](https://www.youtube.com/watch?v=kEfXPTm1aCI) ([Vinicius Stock](https://github.com/vinistock))
|
45
48
|
* [Remote Ruby: Ruby Language Server with Vinicius Stock](https://remoteruby.com/221)
|
49
|
+
* [RubyKaigi 2023: Code indexing - How language servers understand our code](https://www.youtube.com/watch?v=ks3tQojSJLU) ([Vinicius Stock](https://github.com/vinistock))
|
46
50
|
|
47
51
|
## Contributing
|
48
52
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.6.1
|
data/exe/ruby-lsp
CHANGED
@@ -20,7 +20,39 @@ end
|
|
20
20
|
|
21
21
|
require_relative "../lib/ruby_lsp/internal"
|
22
22
|
|
23
|
-
|
23
|
+
require "optparse"
|
24
|
+
|
25
|
+
options = {}
|
26
|
+
parser = OptionParser.new do |opts|
|
27
|
+
opts.banner = "Usage: ruby-lsp [options]"
|
28
|
+
|
29
|
+
opts.on("--version", "Print ruby-lsp version") do
|
30
|
+
puts RubyLsp::VERSION
|
31
|
+
exit(0)
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on("--debug", "Launch the Ruby LSP with a debugger attached") do
|
35
|
+
options[:debug] = true
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.on("-h", "--help", "Print this help") do
|
39
|
+
puts opts.help
|
40
|
+
puts
|
41
|
+
puts "See https://shopify.github.io/ruby-lsp/ for more information"
|
42
|
+
exit(0)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
begin
|
47
|
+
parser.parse!
|
48
|
+
rescue OptionParser::InvalidOption => e
|
49
|
+
warn(e)
|
50
|
+
warn("")
|
51
|
+
warn(parser.help)
|
52
|
+
exit(1)
|
53
|
+
end
|
54
|
+
|
55
|
+
if options[:debug]
|
24
56
|
if ["x64-mingw-ucrt", "x64-mingw32"].include?(RUBY_PLATFORM)
|
25
57
|
puts "Debugging is not supported on Windows"
|
26
58
|
exit 1
|
data/lib/ruby_lsp/check_docs.rb
CHANGED
@@ -6,23 +6,26 @@ require "objspace"
|
|
6
6
|
|
7
7
|
module RubyLsp
|
8
8
|
# This rake task checks that all requests or extensions are fully documented. Add the rake task to your Rakefile and
|
9
|
-
# specify the absolute path for all files that must be required in order to discover all listeners
|
9
|
+
# specify the absolute path for all files that must be required in order to discover all listeners and their
|
10
|
+
# related GIFs
|
10
11
|
#
|
11
12
|
# # Rakefile
|
12
13
|
# request_files = FileList.new("#{__dir__}/lib/ruby_lsp/requests/*.rb") do |fl|
|
13
14
|
# fl.exclude(/base_request\.rb/)
|
14
15
|
# end
|
15
|
-
#
|
16
|
+
# gif_files = FileList.new("#{__dir__}/**/*.gif")
|
17
|
+
# RubyLsp::CheckDocs.new(request_files, gif_files)
|
16
18
|
# # Run with bundle exec rake ruby_lsp:check_docs
|
17
19
|
class CheckDocs < Rake::TaskLib
|
18
20
|
extend T::Sig
|
19
21
|
|
20
|
-
sig { params(require_files: Rake::FileList).void }
|
21
|
-
def initialize(require_files)
|
22
|
+
sig { params(require_files: Rake::FileList, gif_files: Rake::FileList).void }
|
23
|
+
def initialize(require_files, gif_files)
|
22
24
|
super()
|
23
25
|
|
24
26
|
@name = T.let("ruby_lsp:check_docs", String)
|
25
27
|
@file_list = require_files
|
28
|
+
@gif_list = gif_files
|
26
29
|
define_task
|
27
30
|
end
|
28
31
|
|
@@ -34,6 +37,13 @@ module RubyLsp
|
|
34
37
|
task(@name) { run_task }
|
35
38
|
end
|
36
39
|
|
40
|
+
sig { params(request_path: String).returns(T::Boolean) }
|
41
|
+
def gif_exists?(request_path)
|
42
|
+
request_gif = request_path.gsub(".rb", ".gif").split("/").last
|
43
|
+
|
44
|
+
@gif_list.any? { |gif_path| gif_path.end_with?(request_gif) }
|
45
|
+
end
|
46
|
+
|
37
47
|
sig { void }
|
38
48
|
def run_task
|
39
49
|
# Require all files configured to make sure all listeners are loaded
|
@@ -42,7 +52,7 @@ module RubyLsp
|
|
42
52
|
# Find all classes that inherit from BaseRequest or Listener, which are the ones we want to make sure are
|
43
53
|
# documented
|
44
54
|
features = ObjectSpace.each_object(Class).filter_map do |k|
|
45
|
-
klass = T.cast(k, Class)
|
55
|
+
klass = T.cast(k, T::Class[T.anything])
|
46
56
|
klass if klass < RubyLsp::Requests::BaseRequest || klass < RubyLsp::Listener
|
47
57
|
end
|
48
58
|
|
@@ -93,6 +103,14 @@ module RubyLsp
|
|
93
103
|
|
94
104
|
# [Inlay hint demo](../../inlay_hint.gif)
|
95
105
|
DOCS
|
106
|
+
elsif !gif_exists?(file_path)
|
107
|
+
T.must(missing_docs[class_name]) << <<~DOCS
|
108
|
+
The GIF for the request documentation does not exist. Make sure to add it,
|
109
|
+
with the same naming as the request. For example:
|
110
|
+
|
111
|
+
# lib/ruby_lsp/requests/code_lens.rb
|
112
|
+
# foo/bar/code_lens.gif
|
113
|
+
DOCS
|
96
114
|
end
|
97
115
|
end
|
98
116
|
|
@@ -20,6 +20,7 @@ module RubyLsp
|
|
20
20
|
# ```
|
21
21
|
class EventEmitter < SyntaxTree::Visitor
|
22
22
|
extend T::Sig
|
23
|
+
include SyntaxTree::WithScope
|
23
24
|
|
24
25
|
sig { void }
|
25
26
|
def initialize
|
@@ -73,6 +74,12 @@ module RubyLsp
|
|
73
74
|
@listeners[:after_command]&.each { |l| T.unsafe(l).after_command(node) }
|
74
75
|
end
|
75
76
|
|
77
|
+
sig { override.params(node: SyntaxTree::CommandCall).void }
|
78
|
+
def visit_command_call(node)
|
79
|
+
@listeners[:on_command_call]&.each { |l| T.unsafe(l).on_command_call(node) }
|
80
|
+
super
|
81
|
+
end
|
82
|
+
|
76
83
|
sig { override.params(node: SyntaxTree::CallNode).void }
|
77
84
|
def visit_call(node)
|
78
85
|
@listeners[:on_call]&.each { |l| T.unsafe(l).on_call(node) }
|
@@ -116,5 +123,59 @@ module RubyLsp
|
|
116
123
|
@listeners[:on_comment]&.each { |l| T.unsafe(l).on_comment(node) }
|
117
124
|
super
|
118
125
|
end
|
126
|
+
|
127
|
+
sig { override.params(node: SyntaxTree::Rescue).void }
|
128
|
+
def visit_rescue(node)
|
129
|
+
@listeners[:on_rescue]&.each { |l| T.unsafe(l).on_rescue(node) }
|
130
|
+
super
|
131
|
+
end
|
132
|
+
|
133
|
+
sig { override.params(node: SyntaxTree::Kw).void }
|
134
|
+
def visit_kw(node)
|
135
|
+
@listeners[:on_kw]&.each { |l| T.unsafe(l).on_kw(node) }
|
136
|
+
super
|
137
|
+
end
|
138
|
+
|
139
|
+
sig { override.params(node: SyntaxTree::Params).void }
|
140
|
+
def visit_params(node)
|
141
|
+
@listeners[:on_params]&.each { |l| T.unsafe(l).on_params(node) }
|
142
|
+
super
|
143
|
+
end
|
144
|
+
|
145
|
+
sig { override.params(node: SyntaxTree::Field).void }
|
146
|
+
def visit_field(node)
|
147
|
+
@listeners[:on_field]&.each { |l| T.unsafe(l).on_field(node) }
|
148
|
+
super
|
149
|
+
end
|
150
|
+
|
151
|
+
sig { override.params(node: SyntaxTree::VarRef).void }
|
152
|
+
def visit_var_ref(node)
|
153
|
+
@listeners[:on_var_ref]&.each { |l| T.unsafe(l).on_var_ref(node) }
|
154
|
+
super
|
155
|
+
end
|
156
|
+
|
157
|
+
sig { override.params(node: SyntaxTree::BlockVar).void }
|
158
|
+
def visit_block_var(node)
|
159
|
+
@listeners[:on_block_var]&.each { |l| T.unsafe(l).on_block_var(node) }
|
160
|
+
super
|
161
|
+
end
|
162
|
+
|
163
|
+
sig { override.params(node: SyntaxTree::LambdaVar).void }
|
164
|
+
def visit_lambda_var(node)
|
165
|
+
@listeners[:on_lambda_var]&.each { |l| T.unsafe(l).on_lambda_var(node) }
|
166
|
+
super
|
167
|
+
end
|
168
|
+
|
169
|
+
sig { override.params(node: SyntaxTree::Binary).void }
|
170
|
+
def visit_binary(node)
|
171
|
+
super
|
172
|
+
@listeners[:after_binary]&.each { |l| T.unsafe(l).after_binary(node) }
|
173
|
+
end
|
174
|
+
|
175
|
+
sig { override.params(node: SyntaxTree::Const).void }
|
176
|
+
def visit_const(node)
|
177
|
+
@listeners[:on_const]&.each { |l| T.unsafe(l).on_const(node) }
|
178
|
+
super
|
179
|
+
end
|
119
180
|
end
|
120
181
|
end
|
data/lib/ruby_lsp/executor.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require "ruby_lsp/requests/support/dependency_detector"
|
5
|
+
|
4
6
|
module RubyLsp
|
5
7
|
# This class dispatches a request execution to the right request class. No IO should happen anywhere here!
|
6
8
|
class Executor
|
@@ -11,6 +13,7 @@ module RubyLsp
|
|
11
13
|
# Requests that mutate the store must be run sequentially! Parallel requests only receive a temporary copy of the
|
12
14
|
# store
|
13
15
|
@store = store
|
16
|
+
@test_library = T.let(DependencyDetector.detected_test_library, String)
|
14
17
|
@message_queue = message_queue
|
15
18
|
end
|
16
19
|
|
@@ -81,7 +84,8 @@ module RubyLsp
|
|
81
84
|
folding_range(uri)
|
82
85
|
when "textDocument/selectionRange"
|
83
86
|
selection_range(uri, request.dig(:params, :positions))
|
84
|
-
when "textDocument/documentSymbol", "textDocument/documentLink", "textDocument/codeLens"
|
87
|
+
when "textDocument/documentSymbol", "textDocument/documentLink", "textDocument/codeLens",
|
88
|
+
"textDocument/semanticTokens/full"
|
85
89
|
document = @store.get(uri)
|
86
90
|
|
87
91
|
# If the response has already been cached by another request, return it
|
@@ -92,10 +96,11 @@ module RubyLsp
|
|
92
96
|
emitter = EventEmitter.new
|
93
97
|
document_symbol = Requests::DocumentSymbol.new(emitter, @message_queue)
|
94
98
|
document_link = Requests::DocumentLink.new(uri, emitter, @message_queue)
|
95
|
-
code_lens = Requests::CodeLens.new(uri, emitter, @message_queue)
|
99
|
+
code_lens = Requests::CodeLens.new(uri, emitter, @message_queue, @test_library)
|
96
100
|
code_lens_extensions_listeners = Requests::CodeLens.listeners.map do |l|
|
97
101
|
T.unsafe(l).new(document.uri, emitter, @message_queue)
|
98
102
|
end
|
103
|
+
semantic_highlighting = Requests::SemanticHighlighting.new(emitter, @message_queue)
|
99
104
|
emitter.visit(document.tree) if document.parsed?
|
100
105
|
|
101
106
|
code_lens_extensions_listeners.each { |ext| code_lens.merge_response!(ext) }
|
@@ -105,9 +110,11 @@ module RubyLsp
|
|
105
110
|
document.cache_set("textDocument/documentSymbol", document_symbol.response)
|
106
111
|
document.cache_set("textDocument/documentLink", document_link.response)
|
107
112
|
document.cache_set("textDocument/codeLens", code_lens.response)
|
113
|
+
document.cache_set(
|
114
|
+
"textDocument/semanticTokens/full",
|
115
|
+
Requests::Support::SemanticTokenEncoder.new.encode(semantic_highlighting.response),
|
116
|
+
)
|
108
117
|
document.cache_get(request[:method])
|
109
|
-
when "textDocument/semanticTokens/full"
|
110
|
-
semantic_tokens_full(uri)
|
111
118
|
when "textDocument/semanticTokens/range"
|
112
119
|
semantic_tokens_range(uri, request.dig(:params, :range))
|
113
120
|
when "textDocument/formatting"
|
@@ -245,19 +252,6 @@ module RubyLsp
|
|
245
252
|
end
|
246
253
|
end
|
247
254
|
|
248
|
-
sig { params(uri: String).returns(Interface::SemanticTokens) }
|
249
|
-
def semantic_tokens_full(uri)
|
250
|
-
@store.cache_fetch(uri, "textDocument/semanticTokens/full") do |document|
|
251
|
-
T.cast(
|
252
|
-
Requests::SemanticHighlighting.new(
|
253
|
-
document,
|
254
|
-
encoder: Requests::Support::SemanticTokenEncoder.new,
|
255
|
-
).run,
|
256
|
-
Interface::SemanticTokens,
|
257
|
-
)
|
258
|
-
end
|
259
|
-
end
|
260
|
-
|
261
255
|
sig { params(uri: String).returns(T.nilable(T::Array[Interface::TextEdit])) }
|
262
256
|
def formatting(uri)
|
263
257
|
# If formatter is set to `auto` but no supported formatting gem is found, don't attempt to format
|
@@ -287,13 +281,18 @@ module RubyLsp
|
|
287
281
|
Requests::DocumentHighlight.new(@store.get(uri), position).run
|
288
282
|
end
|
289
283
|
|
290
|
-
sig { params(uri: String, range: Document::RangeShape).returns(T::Array[Interface::InlayHint]) }
|
284
|
+
sig { params(uri: String, range: Document::RangeShape).returns(T.nilable(T::Array[Interface::InlayHint])) }
|
291
285
|
def inlay_hint(uri, range)
|
292
286
|
document = @store.get(uri)
|
287
|
+
return if document.syntax_error?
|
288
|
+
|
293
289
|
start_line = range.dig(:start, :line)
|
294
290
|
end_line = range.dig(:end, :line)
|
295
291
|
|
296
|
-
|
292
|
+
emitter = EventEmitter.new
|
293
|
+
listener = Requests::InlayHints.new(start_line..end_line, emitter, @message_queue)
|
294
|
+
emitter.visit(document.tree)
|
295
|
+
listener.response
|
297
296
|
end
|
298
297
|
|
299
298
|
sig do
|
@@ -354,14 +353,15 @@ module RubyLsp
|
|
354
353
|
start_line = range.dig(:start, :line)
|
355
354
|
end_line = range.dig(:end, :line)
|
356
355
|
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
).run,
|
363
|
-
Interface::SemanticTokens,
|
356
|
+
emitter = EventEmitter.new
|
357
|
+
listener = Requests::SemanticHighlighting.new(
|
358
|
+
emitter,
|
359
|
+
@message_queue,
|
360
|
+
range: start_line..end_line,
|
364
361
|
)
|
362
|
+
emitter.visit(document.tree) if document.parsed?
|
363
|
+
|
364
|
+
Requests::Support::SemanticTokenEncoder.new.encode(listener.response)
|
365
365
|
end
|
366
366
|
|
367
367
|
sig do
|
@@ -421,9 +421,9 @@ module RubyLsp
|
|
421
421
|
encodings.first
|
422
422
|
end
|
423
423
|
|
424
|
-
formatter = options.dig(:initializationOptions, :formatter)
|
424
|
+
formatter = options.dig(:initializationOptions, :formatter) || "auto"
|
425
425
|
@store.formatter = if formatter == "auto"
|
426
|
-
detected_formatter
|
426
|
+
DependencyDetector.detected_formatter
|
427
427
|
else
|
428
428
|
formatter
|
429
429
|
end
|
@@ -536,23 +536,6 @@ module RubyLsp
|
|
536
536
|
)
|
537
537
|
end
|
538
538
|
|
539
|
-
sig { returns(String) }
|
540
|
-
def detected_formatter
|
541
|
-
# NOTE: Intentionally no $ at end, since we want to match rubocop-shopify, etc.
|
542
|
-
if direct_dependency?(/^rubocop/)
|
543
|
-
"rubocop"
|
544
|
-
elsif direct_dependency?(/^syntax_tree$/)
|
545
|
-
"syntax_tree"
|
546
|
-
else
|
547
|
-
"none"
|
548
|
-
end
|
549
|
-
end
|
550
|
-
|
551
|
-
sig { params(gem_pattern: Regexp).returns(T::Boolean) }
|
552
|
-
def direct_dependency?(gem_pattern)
|
553
|
-
Bundler.locked_gems.dependencies.keys.grep(gem_pattern).any?
|
554
|
-
end
|
555
|
-
|
556
539
|
sig { void }
|
557
540
|
def check_formatter_is_available
|
558
541
|
# Warn of an unavailable `formatter` setting, e.g. `rubocop` on a project which doesn't have RuboCop.
|
@@ -560,11 +543,13 @@ module RubyLsp
|
|
560
543
|
return unless @store.formatter == "rubocop"
|
561
544
|
|
562
545
|
unless defined?(RubyLsp::Requests::Support::RuboCopRunner)
|
546
|
+
@store.formatter = "none"
|
547
|
+
|
563
548
|
@message_queue << Notification.new(
|
564
549
|
message: "window/showMessage",
|
565
550
|
params: Interface::ShowMessageParams.new(
|
566
551
|
type: Constant::MessageType::ERROR,
|
567
|
-
message: "Ruby LSP formatter is set to `rubocop` but RuboCop was not found in the
|
552
|
+
message: "Ruby LSP formatter is set to `rubocop` but RuboCop was not found in the Gemfile or gemspec.",
|
568
553
|
),
|
569
554
|
)
|
570
555
|
end
|
@@ -11,12 +11,8 @@ module RubyLsp
|
|
11
11
|
|
12
12
|
abstract!
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
# anything. We can remove these arguments once we drop support for Ruby 2.7
|
17
|
-
# https://github.com/ruby-syntax-tree/syntax_tree/blob/4dac90b53df388f726dce50ce638a1ba71cc59f8/lib/syntax_tree/with_scope.rb#L122
|
18
|
-
sig { params(document: Document, _kwargs: T.untyped).void }
|
19
|
-
def initialize(document, **_kwargs)
|
14
|
+
sig { params(document: Document).void }
|
15
|
+
def initialize(document)
|
20
16
|
@document = document
|
21
17
|
super()
|
22
18
|
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require "shellwords"
|
5
|
+
|
4
6
|
module RubyLsp
|
5
7
|
module Requests
|
6
8
|
# ![Code lens demo](../../code_lens.gif)
|
@@ -31,35 +33,65 @@ module RubyLsp
|
|
31
33
|
sig { override.returns(ResponseType) }
|
32
34
|
attr_reader :response
|
33
35
|
|
34
|
-
sig { params(uri: String, emitter: EventEmitter, message_queue: Thread::Queue).void }
|
35
|
-
def initialize(uri, emitter, message_queue)
|
36
|
+
sig { params(uri: String, emitter: EventEmitter, message_queue: Thread::Queue, test_library: String).void }
|
37
|
+
def initialize(uri, emitter, message_queue, test_library)
|
36
38
|
super(emitter, message_queue)
|
37
39
|
|
40
|
+
@test_library = T.let(test_library, String)
|
38
41
|
@response = T.let([], ResponseType)
|
39
42
|
@path = T.let(T.must(URI(uri).path), String)
|
40
|
-
|
41
|
-
@
|
42
|
-
|
43
|
-
|
43
|
+
# visibility_stack is a stack of [current_visibility, previous_visibility]
|
44
|
+
@visibility_stack = T.let([["public", "public"]], T::Array[T::Array[T.nilable(String)]])
|
45
|
+
@class_stack = T.let([], T::Array[String])
|
46
|
+
|
47
|
+
emitter.register(
|
48
|
+
self,
|
49
|
+
:on_class,
|
50
|
+
:after_class,
|
51
|
+
:on_def,
|
52
|
+
:on_command,
|
53
|
+
:after_command,
|
54
|
+
:on_call,
|
55
|
+
:after_call,
|
56
|
+
:on_vcall,
|
57
|
+
)
|
44
58
|
end
|
45
59
|
|
46
60
|
sig { params(node: SyntaxTree::ClassDeclaration).void }
|
47
61
|
def on_class(node)
|
62
|
+
@visibility_stack.push(["public", "public"])
|
48
63
|
class_name = node.constant.constant.value
|
49
64
|
if class_name.end_with?("Test")
|
50
|
-
|
65
|
+
@class_stack.push(class_name)
|
66
|
+
add_test_code_lens(
|
67
|
+
node,
|
68
|
+
name: class_name,
|
69
|
+
command: generate_test_command(class_name: class_name),
|
70
|
+
kind: :group,
|
71
|
+
)
|
51
72
|
end
|
52
73
|
end
|
53
74
|
|
75
|
+
sig { params(node: SyntaxTree::ClassDeclaration).void }
|
76
|
+
def after_class(node)
|
77
|
+
@visibility_stack.pop
|
78
|
+
@class_stack.pop
|
79
|
+
end
|
80
|
+
|
54
81
|
sig { params(node: SyntaxTree::DefNode).void }
|
55
82
|
def on_def(node)
|
56
|
-
|
83
|
+
class_name = @class_stack.last
|
84
|
+
return unless class_name
|
85
|
+
|
86
|
+
visibility, _ = @visibility_stack.last
|
87
|
+
if visibility == "public"
|
57
88
|
method_name = node.name.value
|
58
89
|
if method_name.start_with?("test_")
|
59
|
-
|
90
|
+
add_test_code_lens(
|
60
91
|
node,
|
61
92
|
name: method_name,
|
62
|
-
command:
|
93
|
+
command: generate_test_command(method_name: method_name, class_name: class_name),
|
94
|
+
kind: :example,
|
63
95
|
)
|
64
96
|
end
|
65
97
|
end
|
@@ -67,15 +99,22 @@ module RubyLsp
|
|
67
99
|
|
68
100
|
sig { params(node: SyntaxTree::Command).void }
|
69
101
|
def on_command(node)
|
70
|
-
|
71
|
-
|
72
|
-
|
102
|
+
node_message = node.message.value
|
103
|
+
if ACCESS_MODIFIERS.include?(node_message) && node.arguments.parts.any?
|
104
|
+
visibility, _ = @visibility_stack.pop
|
105
|
+
@visibility_stack.push([node_message, visibility])
|
106
|
+
elsif @path.include?("Gemfile") && node_message.include?("gem") && node.arguments.parts.any?
|
107
|
+
remote = resolve_gem_remote(node)
|
108
|
+
return unless remote
|
109
|
+
|
110
|
+
add_open_gem_remote_code_lens(node, remote)
|
73
111
|
end
|
74
112
|
end
|
75
113
|
|
76
114
|
sig { params(node: SyntaxTree::Command).void }
|
77
115
|
def after_command(node)
|
78
|
-
|
116
|
+
_, prev_visibility = @visibility_stack.pop
|
117
|
+
@visibility_stack.push([prev_visibility, prev_visibility])
|
79
118
|
end
|
80
119
|
|
81
120
|
sig { params(node: SyntaxTree::CallNode).void }
|
@@ -85,15 +124,16 @@ module RubyLsp
|
|
85
124
|
if ident
|
86
125
|
ident_value = T.cast(ident, SyntaxTree::Ident).value
|
87
126
|
if ACCESS_MODIFIERS.include?(ident_value)
|
88
|
-
|
89
|
-
@visibility
|
127
|
+
visibility, _ = @visibility_stack.pop
|
128
|
+
@visibility_stack.push([ident_value, visibility])
|
90
129
|
end
|
91
130
|
end
|
92
131
|
end
|
93
132
|
|
94
133
|
sig { params(node: SyntaxTree::CallNode).void }
|
95
134
|
def after_call(node)
|
96
|
-
|
135
|
+
_, prev_visibility = @visibility_stack.pop
|
136
|
+
@visibility_stack.push([prev_visibility, prev_visibility])
|
97
137
|
end
|
98
138
|
|
99
139
|
sig { params(node: SyntaxTree::VCall).void }
|
@@ -101,8 +141,8 @@ module RubyLsp
|
|
101
141
|
vcall_value = node.value.value
|
102
142
|
|
103
143
|
if ACCESS_MODIFIERS.include?(vcall_value)
|
104
|
-
@
|
105
|
-
@
|
144
|
+
@visibility_stack.pop
|
145
|
+
@visibility_stack.push([vcall_value, vcall_value])
|
106
146
|
end
|
107
147
|
end
|
108
148
|
|
@@ -114,36 +154,88 @@ module RubyLsp
|
|
114
154
|
|
115
155
|
private
|
116
156
|
|
117
|
-
sig { params(node: SyntaxTree::Node, name: String, command: String).void }
|
118
|
-
def
|
157
|
+
sig { params(node: SyntaxTree::Node, name: String, command: String, kind: Symbol).void }
|
158
|
+
def add_test_code_lens(node, name:, command:, kind:)
|
159
|
+
arguments = [
|
160
|
+
@path,
|
161
|
+
name,
|
162
|
+
command,
|
163
|
+
{
|
164
|
+
start_line: node.location.start_line - 1,
|
165
|
+
start_column: node.location.start_column,
|
166
|
+
end_line: node.location.end_line - 1,
|
167
|
+
end_column: node.location.end_column,
|
168
|
+
},
|
169
|
+
]
|
170
|
+
|
119
171
|
@response << create_code_lens(
|
120
172
|
node,
|
121
173
|
title: "Run",
|
122
174
|
command_name: "rubyLsp.runTest",
|
123
|
-
|
124
|
-
|
125
|
-
test_command: command,
|
126
|
-
type: "test",
|
175
|
+
arguments: arguments,
|
176
|
+
data: { type: "test", kind: kind },
|
127
177
|
)
|
128
178
|
|
129
179
|
@response << create_code_lens(
|
130
180
|
node,
|
131
181
|
title: "Run In Terminal",
|
132
182
|
command_name: "rubyLsp.runTestInTerminal",
|
133
|
-
|
134
|
-
|
135
|
-
test_command: command,
|
136
|
-
type: "test_in_terminal",
|
183
|
+
arguments: arguments,
|
184
|
+
data: { type: "test_in_terminal", kind: kind },
|
137
185
|
)
|
138
186
|
|
139
187
|
@response << create_code_lens(
|
140
188
|
node,
|
141
189
|
title: "Debug",
|
142
190
|
command_name: "rubyLsp.debugTest",
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
191
|
+
arguments: arguments,
|
192
|
+
data: { type: "debug", kind: kind },
|
193
|
+
)
|
194
|
+
end
|
195
|
+
|
196
|
+
sig { params(node: SyntaxTree::Command).returns(T.nilable(String)) }
|
197
|
+
def resolve_gem_remote(node)
|
198
|
+
gem_statement = node.arguments.parts.flat_map(&:child_nodes).first
|
199
|
+
return unless gem_statement
|
200
|
+
|
201
|
+
spec = Gem::Specification.stubs.find { |gem| gem.name == gem_statement.value }&.to_spec
|
202
|
+
return if spec.nil?
|
203
|
+
|
204
|
+
[spec.homepage, spec.metadata["source_code_uri"]].compact.find do |page|
|
205
|
+
page.start_with?("https://github.com", "https://gitlab.com")
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
sig { params(class_name: String, method_name: T.nilable(String)).returns(String) }
|
210
|
+
def generate_test_command(class_name:, method_name: nil)
|
211
|
+
command = BASE_COMMAND + @path
|
212
|
+
|
213
|
+
case @test_library
|
214
|
+
when "minitest"
|
215
|
+
command += if method_name
|
216
|
+
" --name " + "/#{Shellwords.escape(class_name + "#" + method_name)}/"
|
217
|
+
else
|
218
|
+
" --name " + "/#{Shellwords.escape(class_name)}/"
|
219
|
+
end
|
220
|
+
when "test-unit"
|
221
|
+
command += " --testcase " + "/#{Shellwords.escape(class_name)}/"
|
222
|
+
|
223
|
+
if method_name
|
224
|
+
command += " --name " + Shellwords.escape(method_name)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
command
|
229
|
+
end
|
230
|
+
|
231
|
+
sig { params(node: SyntaxTree::Command, remote: String).void }
|
232
|
+
def add_open_gem_remote_code_lens(node, remote)
|
233
|
+
@response << create_code_lens(
|
234
|
+
node,
|
235
|
+
title: "Open remote",
|
236
|
+
command_name: "rubyLsp.openLink",
|
237
|
+
arguments: [remote],
|
238
|
+
data: { type: "link" },
|
147
239
|
)
|
148
240
|
end
|
149
241
|
end
|
@@ -30,7 +30,9 @@ module RubyLsp
|
|
30
30
|
|
31
31
|
sig { override.returns(T.nilable(T.all(T::Array[Support::RuboCopDiagnostic], Object))) }
|
32
32
|
def run
|
33
|
+
# Running RuboCop is slow, so to avoid excessive runs we only do so if the file is syntactically valid
|
33
34
|
return if @document.syntax_error?
|
35
|
+
|
34
36
|
return unless defined?(Support::RuboCopDiagnosticsRunner)
|
35
37
|
|
36
38
|
# Don't try to run RuboCop diagnostics for files outside the current working directory
|
@@ -27,7 +27,6 @@ module RubyLsp
|
|
27
27
|
SyntaxTree::For,
|
28
28
|
SyntaxTree::HashLiteral,
|
29
29
|
SyntaxTree::Heredoc,
|
30
|
-
SyntaxTree::IfNode,
|
31
30
|
SyntaxTree::ModuleDeclaration,
|
32
31
|
SyntaxTree::SClass,
|
33
32
|
SyntaxTree::UnlessNode,
|
@@ -42,6 +41,7 @@ module RubyLsp
|
|
42
41
|
|
43
42
|
NODES_WITH_STATEMENTS = T.let(
|
44
43
|
[
|
44
|
+
SyntaxTree::IfNode,
|
45
45
|
SyntaxTree::Elsif,
|
46
46
|
SyntaxTree::In,
|
47
47
|
SyntaxTree::Rescue,
|
@@ -52,6 +52,7 @@ module RubyLsp
|
|
52
52
|
|
53
53
|
StatementNode = T.type_alias do
|
54
54
|
T.any(
|
55
|
+
SyntaxTree::IfNode,
|
55
56
|
SyntaxTree::Elsif,
|
56
57
|
SyntaxTree::In,
|
57
58
|
SyntaxTree::Rescue,
|