ruby-lsp 0.4.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +29 -116
- data/VERSION +1 -1
- data/exe/ruby-lsp +10 -1
- data/lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb +62 -0
- data/lib/ruby_lsp/check_docs.rb +112 -0
- data/lib/ruby_lsp/document.rb +87 -8
- data/lib/ruby_lsp/event_emitter.rb +120 -0
- data/lib/ruby_lsp/executor.rb +191 -44
- data/lib/ruby_lsp/extension.rb +104 -0
- data/lib/ruby_lsp/internal.rb +4 -0
- data/lib/ruby_lsp/listener.rb +42 -0
- data/lib/ruby_lsp/requests/base_request.rb +2 -90
- data/lib/ruby_lsp/requests/code_action_resolve.rb +47 -20
- data/lib/ruby_lsp/requests/code_actions.rb +6 -5
- data/lib/ruby_lsp/requests/code_lens.rb +151 -0
- data/lib/ruby_lsp/requests/diagnostics.rb +5 -5
- data/lib/ruby_lsp/requests/document_highlight.rb +8 -10
- data/lib/ruby_lsp/requests/document_link.rb +17 -15
- data/lib/ruby_lsp/requests/document_symbol.rb +63 -40
- data/lib/ruby_lsp/requests/folding_ranges.rb +14 -10
- data/lib/ruby_lsp/requests/formatting.rb +15 -15
- data/lib/ruby_lsp/requests/hover.rb +45 -34
- data/lib/ruby_lsp/requests/inlay_hints.rb +6 -5
- data/lib/ruby_lsp/requests/on_type_formatting.rb +5 -1
- data/lib/ruby_lsp/requests/path_completion.rb +21 -51
- data/lib/ruby_lsp/requests/selection_ranges.rb +4 -4
- data/lib/ruby_lsp/requests/semantic_highlighting.rb +30 -16
- data/lib/ruby_lsp/requests/support/common.rb +91 -0
- data/lib/ruby_lsp/requests/support/highlight_target.rb +5 -4
- data/lib/ruby_lsp/requests/support/rails_document_client.rb +7 -6
- data/lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb +0 -1
- data/lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb +0 -1
- data/lib/ruby_lsp/requests/support/selection_range.rb +1 -1
- data/lib/ruby_lsp/requests/support/semantic_token_encoder.rb +2 -2
- data/lib/ruby_lsp/requests/support/sorbet.rb +5 -15
- data/lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb +42 -0
- data/lib/ruby_lsp/requests.rb +17 -14
- data/lib/ruby_lsp/server.rb +45 -9
- data/lib/ruby_lsp/store.rb +11 -11
- data/lib/ruby_lsp/utils.rb +9 -8
- metadata +13 -5
data/lib/ruby_lsp/executor.rb
CHANGED
@@ -6,12 +6,12 @@ module RubyLsp
|
|
6
6
|
class Executor
|
7
7
|
extend T::Sig
|
8
8
|
|
9
|
-
sig { params(store: Store).void }
|
10
|
-
def initialize(store)
|
9
|
+
sig { params(store: Store, message_queue: Thread::Queue).void }
|
10
|
+
def initialize(store, message_queue)
|
11
11
|
# Requests that mutate the store must be run sequentially! Parallel requests only receive a temporary copy of the
|
12
12
|
# store
|
13
13
|
@store = store
|
14
|
-
@
|
14
|
+
@message_queue = message_queue
|
15
15
|
end
|
16
16
|
|
17
17
|
sig { params(request: T::Hash[Symbol, T.untyped]).returns(Result) }
|
@@ -25,7 +25,7 @@ module RubyLsp
|
|
25
25
|
error = e
|
26
26
|
end
|
27
27
|
|
28
|
-
Result.new(response: response, error: error, request_time: request_time
|
28
|
+
Result.new(response: response, error: error, request_time: request_time)
|
29
29
|
end
|
30
30
|
|
31
31
|
private
|
@@ -38,27 +38,74 @@ module RubyLsp
|
|
38
38
|
when "initialize"
|
39
39
|
initialize_request(request.dig(:params))
|
40
40
|
when "initialized"
|
41
|
+
Extension.load_extensions
|
42
|
+
|
43
|
+
errored_extensions = Extension.extensions.select(&:error?)
|
44
|
+
|
45
|
+
if errored_extensions.any?
|
46
|
+
@message_queue << Notification.new(
|
47
|
+
message: "window/showMessage",
|
48
|
+
params: Interface::ShowMessageParams.new(
|
49
|
+
type: Constant::MessageType::WARNING,
|
50
|
+
message: "Error loading extensions:\n\n#{errored_extensions.map(&:formatted_errors).join("\n\n")}",
|
51
|
+
),
|
52
|
+
)
|
53
|
+
|
54
|
+
warn(errored_extensions.map(&:backtraces).join("\n\n"))
|
55
|
+
end
|
56
|
+
|
57
|
+
check_formatter_is_available
|
58
|
+
|
41
59
|
warn("Ruby LSP is ready")
|
42
60
|
VOID
|
43
61
|
when "textDocument/didOpen"
|
44
|
-
text_document_did_open(
|
62
|
+
text_document_did_open(
|
63
|
+
uri,
|
64
|
+
request.dig(:params, :textDocument, :text),
|
65
|
+
request.dig(:params, :textDocument, :version),
|
66
|
+
)
|
45
67
|
when "textDocument/didClose"
|
46
|
-
@
|
68
|
+
@message_queue << Notification.new(
|
47
69
|
message: "textDocument/publishDiagnostics",
|
48
70
|
params: Interface::PublishDiagnosticsParams.new(uri: uri, diagnostics: []),
|
49
71
|
)
|
50
72
|
|
51
73
|
text_document_did_close(uri)
|
52
74
|
when "textDocument/didChange"
|
53
|
-
text_document_did_change(
|
75
|
+
text_document_did_change(
|
76
|
+
uri,
|
77
|
+
request.dig(:params, :contentChanges),
|
78
|
+
request.dig(:params, :textDocument, :version),
|
79
|
+
)
|
54
80
|
when "textDocument/foldingRange"
|
55
81
|
folding_range(uri)
|
56
|
-
when "textDocument/documentLink"
|
57
|
-
document_link(uri)
|
58
82
|
when "textDocument/selectionRange"
|
59
83
|
selection_range(uri, request.dig(:params, :positions))
|
60
|
-
when "textDocument/documentSymbol"
|
61
|
-
|
84
|
+
when "textDocument/documentSymbol", "textDocument/documentLink", "textDocument/codeLens"
|
85
|
+
document = @store.get(uri)
|
86
|
+
|
87
|
+
# If the response has already been cached by another request, return it
|
88
|
+
cached_response = document.cache_get(request[:method])
|
89
|
+
return cached_response if cached_response
|
90
|
+
|
91
|
+
# Run listeners for the document
|
92
|
+
emitter = EventEmitter.new
|
93
|
+
document_symbol = Requests::DocumentSymbol.new(emitter, @message_queue)
|
94
|
+
document_link = Requests::DocumentLink.new(uri, emitter, @message_queue)
|
95
|
+
code_lens = Requests::CodeLens.new(uri, emitter, @message_queue)
|
96
|
+
code_lens_extensions_listeners = Requests::CodeLens.listeners.map do |l|
|
97
|
+
T.unsafe(l).new(document.uri, emitter, @message_queue)
|
98
|
+
end
|
99
|
+
emitter.visit(document.tree) if document.parsed?
|
100
|
+
|
101
|
+
code_lens_extensions_listeners.each { |ext| code_lens.merge_response!(ext) }
|
102
|
+
|
103
|
+
# Store all responses retrieve in this round of visits in the cache and then return the response for the request
|
104
|
+
# we actually received
|
105
|
+
document.cache_set("textDocument/documentSymbol", document_symbol.response)
|
106
|
+
document.cache_set("textDocument/documentLink", document_link.response)
|
107
|
+
document.cache_set("textDocument/codeLens", code_lens.response)
|
108
|
+
document.cache_get(request[:method])
|
62
109
|
when "textDocument/semanticTokens/full"
|
63
110
|
semantic_tokens_full(uri)
|
64
111
|
when "textDocument/semanticTokens/range"
|
@@ -67,7 +114,7 @@ module RubyLsp
|
|
67
114
|
begin
|
68
115
|
formatting(uri)
|
69
116
|
rescue Requests::Formatting::InvalidFormatter => error
|
70
|
-
@
|
117
|
+
@message_queue << Notification.new(
|
71
118
|
message: "window/showMessage",
|
72
119
|
params: Interface::ShowMessageParams.new(
|
73
120
|
type: Constant::MessageType::ERROR,
|
@@ -77,7 +124,7 @@ module RubyLsp
|
|
77
124
|
|
78
125
|
nil
|
79
126
|
rescue StandardError => error
|
80
|
-
@
|
127
|
+
@message_queue << Notification.new(
|
81
128
|
message: "window/showMessage",
|
82
129
|
params: Interface::ShowMessageParams.new(
|
83
130
|
type: Constant::MessageType::ERROR,
|
@@ -103,7 +150,7 @@ module RubyLsp
|
|
103
150
|
begin
|
104
151
|
diagnostic(uri)
|
105
152
|
rescue StandardError => error
|
106
|
-
@
|
153
|
+
@message_queue << Notification.new(
|
107
154
|
message: "window/showMessage",
|
108
155
|
params: Interface::ShowMessageParams.new(
|
109
156
|
type: Constant::MessageType::ERROR,
|
@@ -120,7 +167,7 @@ module RubyLsp
|
|
120
167
|
|
121
168
|
sig { params(uri: String).returns(T::Array[Interface::FoldingRange]) }
|
122
169
|
def folding_range(uri)
|
123
|
-
@store.cache_fetch(uri,
|
170
|
+
@store.cache_fetch(uri, "textDocument/foldingRange") do |document|
|
124
171
|
Requests::FoldingRanges.new(document).run
|
125
172
|
end
|
126
173
|
end
|
@@ -132,32 +179,38 @@ module RubyLsp
|
|
132
179
|
).returns(T.nilable(Interface::Hover))
|
133
180
|
end
|
134
181
|
def hover(uri, position)
|
135
|
-
|
136
|
-
|
182
|
+
document = @store.get(uri)
|
183
|
+
return if document.syntax_error?
|
137
184
|
|
138
|
-
|
139
|
-
def document_link(uri)
|
140
|
-
@store.cache_fetch(uri, :document_link) do |document|
|
141
|
-
RubyLsp::Requests::DocumentLink.new(uri, document).run
|
142
|
-
end
|
143
|
-
end
|
185
|
+
target, parent = document.locate_node(position)
|
144
186
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
Requests::DocumentSymbol.new(document).run
|
187
|
+
if !Requests::Hover::ALLOWED_TARGETS.include?(target.class) &&
|
188
|
+
Requests::Hover::ALLOWED_TARGETS.include?(parent.class)
|
189
|
+
target = parent
|
149
190
|
end
|
191
|
+
|
192
|
+
# Instantiate all listeners
|
193
|
+
emitter = EventEmitter.new
|
194
|
+
base_listener = Requests::Hover.new(emitter, @message_queue)
|
195
|
+
listeners = Requests::Hover.listeners.map { |l| l.new(emitter, @message_queue) }
|
196
|
+
|
197
|
+
# Emit events for all listeners
|
198
|
+
emitter.emit_for_target(target)
|
199
|
+
|
200
|
+
# Merge all responses into a single hover
|
201
|
+
listeners.each { |ext| base_listener.merge_response!(ext) }
|
202
|
+
base_listener.response
|
150
203
|
end
|
151
204
|
|
152
|
-
sig { params(uri: String, content_changes: T::Array[Document::EditShape]).returns(Object) }
|
153
|
-
def text_document_did_change(uri, content_changes)
|
154
|
-
@store.push_edits(uri, content_changes)
|
205
|
+
sig { params(uri: String, content_changes: T::Array[Document::EditShape], version: Integer).returns(Object) }
|
206
|
+
def text_document_did_change(uri, content_changes, version)
|
207
|
+
@store.push_edits(uri: uri, edits: content_changes, version: version)
|
155
208
|
VOID
|
156
209
|
end
|
157
210
|
|
158
|
-
sig { params(uri: String, text: String).returns(Object) }
|
159
|
-
def text_document_did_open(uri, text)
|
160
|
-
@store.set(uri, text)
|
211
|
+
sig { params(uri: String, text: String, version: Integer).returns(Object) }
|
212
|
+
def text_document_did_open(uri, text, version)
|
213
|
+
@store.set(uri: uri, source: text, version: version)
|
161
214
|
VOID
|
162
215
|
end
|
163
216
|
|
@@ -174,7 +227,7 @@ module RubyLsp
|
|
174
227
|
).returns(T.nilable(T::Array[T.nilable(Requests::Support::SelectionRange)]))
|
175
228
|
end
|
176
229
|
def selection_range(uri, positions)
|
177
|
-
ranges = @store.cache_fetch(uri,
|
230
|
+
ranges = @store.cache_fetch(uri, "textDocument/selectionRange") do |document|
|
178
231
|
Requests::SelectionRanges.new(document).run
|
179
232
|
end
|
180
233
|
|
@@ -194,7 +247,7 @@ module RubyLsp
|
|
194
247
|
|
195
248
|
sig { params(uri: String).returns(Interface::SemanticTokens) }
|
196
249
|
def semantic_tokens_full(uri)
|
197
|
-
@store.cache_fetch(uri,
|
250
|
+
@store.cache_fetch(uri, "textDocument/semanticTokens/full") do |document|
|
198
251
|
T.cast(
|
199
252
|
Requests::SemanticHighlighting.new(
|
200
253
|
document,
|
@@ -207,7 +260,10 @@ module RubyLsp
|
|
207
260
|
|
208
261
|
sig { params(uri: String).returns(T.nilable(T::Array[Interface::TextEdit])) }
|
209
262
|
def formatting(uri)
|
210
|
-
|
263
|
+
# If formatter is set to `auto` but no supported formatting gem is found, don't attempt to format
|
264
|
+
return if @store.formatter == "none"
|
265
|
+
|
266
|
+
Requests::Formatting.new(@store.get(uri), formatter: @store.formatter).run
|
211
267
|
end
|
212
268
|
|
213
269
|
sig do
|
@@ -250,7 +306,7 @@ module RubyLsp
|
|
250
306
|
def code_action(uri, range, context)
|
251
307
|
document = @store.get(uri)
|
252
308
|
|
253
|
-
Requests::CodeActions.new(
|
309
|
+
Requests::CodeActions.new(document, range, context).run
|
254
310
|
end
|
255
311
|
|
256
312
|
sig { params(params: T::Hash[Symbol, T.untyped]).returns(Interface::CodeAction) }
|
@@ -261,7 +317,7 @@ module RubyLsp
|
|
261
317
|
|
262
318
|
case result
|
263
319
|
when Requests::CodeActionResolve::Error::EmptySelection
|
264
|
-
@
|
320
|
+
@message_queue << Notification.new(
|
265
321
|
message: "window/showMessage",
|
266
322
|
params: Interface::ShowMessageParams.new(
|
267
323
|
type: Constant::MessageType::ERROR,
|
@@ -270,7 +326,7 @@ module RubyLsp
|
|
270
326
|
)
|
271
327
|
raise Requests::CodeActionResolve::CodeActionError
|
272
328
|
when Requests::CodeActionResolve::Error::InvalidTargetRange
|
273
|
-
@
|
329
|
+
@message_queue << Notification.new(
|
274
330
|
message: "window/showMessage",
|
275
331
|
params: Interface::ShowMessageParams.new(
|
276
332
|
type: Constant::MessageType::ERROR,
|
@@ -285,8 +341,8 @@ module RubyLsp
|
|
285
341
|
|
286
342
|
sig { params(uri: String).returns(T.nilable(Interface::FullDocumentDiagnosticReport)) }
|
287
343
|
def diagnostic(uri)
|
288
|
-
response = @store.cache_fetch(uri,
|
289
|
-
Requests::Diagnostics.new(
|
344
|
+
response = @store.cache_fetch(uri, "textDocument/diagnostic") do |document|
|
345
|
+
Requests::Diagnostics.new(document).run
|
290
346
|
end
|
291
347
|
|
292
348
|
Interface::FullDocumentDiagnosticReport.new(kind: "full", items: response.map(&:to_lsp_diagnostic)) if response
|
@@ -312,17 +368,68 @@ module RubyLsp
|
|
312
368
|
params(uri: String, position: Document::PositionShape).returns(T.nilable(T::Array[Interface::CompletionItem]))
|
313
369
|
end
|
314
370
|
def completion(uri, position)
|
315
|
-
|
371
|
+
document = @store.get(uri)
|
372
|
+
return unless document.parsed?
|
373
|
+
|
374
|
+
char_position = document.create_scanner.find_char_position(position)
|
375
|
+
matched, parent = document.locate(
|
376
|
+
T.must(document.tree),
|
377
|
+
char_position,
|
378
|
+
node_types: [SyntaxTree::Command, SyntaxTree::CommandCall, SyntaxTree::CallNode],
|
379
|
+
)
|
380
|
+
|
381
|
+
return unless matched && parent
|
382
|
+
|
383
|
+
target = case matched
|
384
|
+
when SyntaxTree::Command, SyntaxTree::CallNode, SyntaxTree::CommandCall
|
385
|
+
message = matched.message
|
386
|
+
return if message.is_a?(Symbol)
|
387
|
+
return unless message.value == "require"
|
388
|
+
|
389
|
+
args = matched.arguments
|
390
|
+
args = args.arguments if args.is_a?(SyntaxTree::ArgParen)
|
391
|
+
return if args.nil? || args.is_a?(SyntaxTree::ArgsForward)
|
392
|
+
|
393
|
+
argument = args.parts.first
|
394
|
+
return unless argument.is_a?(SyntaxTree::StringLiteral)
|
395
|
+
|
396
|
+
path_node = argument.parts.first
|
397
|
+
return unless path_node.is_a?(SyntaxTree::TStringContent)
|
398
|
+
return unless (path_node.location.start_char..path_node.location.end_char).cover?(char_position)
|
399
|
+
|
400
|
+
path_node
|
401
|
+
end
|
402
|
+
|
403
|
+
return unless target
|
404
|
+
|
405
|
+
emitter = EventEmitter.new
|
406
|
+
listener = Requests::PathCompletion.new(emitter, @message_queue)
|
407
|
+
emitter.emit_for_target(target)
|
408
|
+
listener.response
|
316
409
|
end
|
317
410
|
|
318
411
|
sig { params(options: T::Hash[Symbol, T.untyped]).returns(Interface::InitializeResult) }
|
319
412
|
def initialize_request(options)
|
320
413
|
@store.clear
|
321
|
-
|
414
|
+
|
415
|
+
encodings = options.dig(:capabilities, :general, :positionEncodings)
|
416
|
+
@store.encoding = if encodings.nil? || encodings.empty?
|
417
|
+
Constant::PositionEncodingKind::UTF16
|
418
|
+
elsif encodings.include?(Constant::PositionEncodingKind::UTF8)
|
419
|
+
Constant::PositionEncodingKind::UTF8
|
420
|
+
else
|
421
|
+
encodings.first
|
422
|
+
end
|
423
|
+
|
322
424
|
formatter = options.dig(:initializationOptions, :formatter)
|
323
|
-
@store.formatter = formatter
|
425
|
+
@store.formatter = if formatter == "auto"
|
426
|
+
detected_formatter
|
427
|
+
else
|
428
|
+
formatter
|
429
|
+
end
|
324
430
|
|
325
431
|
configured_features = options.dig(:initializationOptions, :enabledFeatures)
|
432
|
+
experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled)
|
326
433
|
|
327
434
|
enabled_features = case configured_features
|
328
435
|
when Array
|
@@ -351,6 +458,10 @@ module RubyLsp
|
|
351
458
|
Interface::DocumentLinkOptions.new(resolve_provider: false)
|
352
459
|
end
|
353
460
|
|
461
|
+
code_lens_provider = if experimental_features
|
462
|
+
Interface::CodeLensOptions.new(resolve_provider: false)
|
463
|
+
end
|
464
|
+
|
354
465
|
hover_provider = if enabled_features["hover"]
|
355
466
|
Interface::HoverClientCapabilities.new(dynamic_registration: false)
|
356
467
|
end
|
@@ -406,6 +517,7 @@ module RubyLsp
|
|
406
517
|
change: Constant::TextDocumentSyncKind::INCREMENTAL,
|
407
518
|
open_close: true,
|
408
519
|
),
|
520
|
+
position_encoding: @store.encoding,
|
409
521
|
selection_range_provider: enabled_features["selectionRanges"],
|
410
522
|
hover_provider: hover_provider,
|
411
523
|
document_symbol_provider: document_symbol_provider,
|
@@ -419,8 +531,43 @@ module RubyLsp
|
|
419
531
|
diagnostic_provider: diagnostics_provider,
|
420
532
|
inlay_hint_provider: inlay_hint_provider,
|
421
533
|
completion_provider: completion_provider,
|
534
|
+
code_lens_provider: code_lens_provider,
|
422
535
|
),
|
423
536
|
)
|
424
537
|
end
|
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
|
+
sig { void }
|
557
|
+
def check_formatter_is_available
|
558
|
+
# Warn of an unavailable `formatter` setting, e.g. `rubocop` on a project which doesn't have RuboCop.
|
559
|
+
# Syntax Tree will always be available via Ruby LSP so we don't need to check for it.
|
560
|
+
return unless @store.formatter == "rubocop"
|
561
|
+
|
562
|
+
unless defined?(RubyLsp::Requests::Support::RuboCopRunner)
|
563
|
+
@message_queue << Notification.new(
|
564
|
+
message: "window/showMessage",
|
565
|
+
params: Interface::ShowMessageParams.new(
|
566
|
+
type: Constant::MessageType::ERROR,
|
567
|
+
message: "Ruby LSP formatter is set to `rubocop` but RuboCop was not found in the bundle.",
|
568
|
+
),
|
569
|
+
)
|
570
|
+
end
|
571
|
+
end
|
425
572
|
end
|
426
573
|
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
# To register an extension, inherit from this class and implement both `name` and `activate`
|
6
|
+
#
|
7
|
+
# # Example
|
8
|
+
#
|
9
|
+
# ```ruby
|
10
|
+
# module MyGem
|
11
|
+
# class MyExtension < Extension
|
12
|
+
# def activate
|
13
|
+
# # Perform any relevant initialization
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# def name
|
17
|
+
# "My extension name"
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# ```
|
22
|
+
class Extension
|
23
|
+
extend T::Sig
|
24
|
+
extend T::Helpers
|
25
|
+
|
26
|
+
abstract!
|
27
|
+
|
28
|
+
class << self
|
29
|
+
extend T::Sig
|
30
|
+
|
31
|
+
# Automatically track and instantiate extension classes
|
32
|
+
sig { params(child_class: T.class_of(Extension)).void }
|
33
|
+
def inherited(child_class)
|
34
|
+
extensions << child_class.new
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
sig { returns(T::Array[Extension]) }
|
39
|
+
def extensions
|
40
|
+
@extensions ||= T.let([], T.nilable(T::Array[Extension]))
|
41
|
+
end
|
42
|
+
|
43
|
+
# Discovers and loads all extensions. Returns the list of activated extensions
|
44
|
+
sig { returns(T::Array[Extension]) }
|
45
|
+
def load_extensions
|
46
|
+
# Require all extensions entry points, which should be placed under
|
47
|
+
# `some_gem/lib/ruby_lsp/your_gem_name/extension.rb`
|
48
|
+
Gem.find_files("ruby_lsp/**/extension.rb").each do |extension|
|
49
|
+
require File.expand_path(extension)
|
50
|
+
rescue => e
|
51
|
+
warn(e.message)
|
52
|
+
warn(e.backtrace.to_s) # rubocop:disable Lint/RedundantStringCoercion
|
53
|
+
end
|
54
|
+
|
55
|
+
# Activate each one of the discovered extensions. If any problems occur in the extensions, we don't want to
|
56
|
+
# fail to boot the server
|
57
|
+
extensions.each do |extension|
|
58
|
+
extension.activate
|
59
|
+
nil
|
60
|
+
rescue => e
|
61
|
+
extension.add_error(e)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
sig { void }
|
67
|
+
def initialize
|
68
|
+
@errors = T.let([], T::Array[StandardError])
|
69
|
+
end
|
70
|
+
|
71
|
+
sig { params(error: StandardError).returns(T.self_type) }
|
72
|
+
def add_error(error)
|
73
|
+
@errors << error
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
sig { returns(T::Boolean) }
|
78
|
+
def error?
|
79
|
+
@errors.any?
|
80
|
+
end
|
81
|
+
|
82
|
+
sig { returns(String) }
|
83
|
+
def formatted_errors
|
84
|
+
<<~ERRORS
|
85
|
+
#{name}:
|
86
|
+
#{@errors.map(&:message).join("\n")}
|
87
|
+
ERRORS
|
88
|
+
end
|
89
|
+
|
90
|
+
sig { returns(String) }
|
91
|
+
def backtraces
|
92
|
+
@errors.filter_map(&:backtrace).join("\n\n")
|
93
|
+
end
|
94
|
+
|
95
|
+
# Each extension should implement `MyExtension#activate` and use to perform any sort of initialization, such as
|
96
|
+
# reading information into memory or even spawning a separate process
|
97
|
+
sig { abstract.void }
|
98
|
+
def activate; end
|
99
|
+
|
100
|
+
# Extensions should override the `name` method to return the extension name
|
101
|
+
sig { abstract.returns(String) }
|
102
|
+
def name; end
|
103
|
+
end
|
104
|
+
end
|
data/lib/ruby_lsp/internal.rb
CHANGED
@@ -11,5 +11,9 @@ require "ruby-lsp"
|
|
11
11
|
require "ruby_lsp/utils"
|
12
12
|
require "ruby_lsp/server"
|
13
13
|
require "ruby_lsp/executor"
|
14
|
+
require "ruby_lsp/event_emitter"
|
14
15
|
require "ruby_lsp/requests"
|
16
|
+
require "ruby_lsp/listener"
|
15
17
|
require "ruby_lsp/store"
|
18
|
+
require "ruby_lsp/extension"
|
19
|
+
require "ruby_lsp/requests/support/rubocop_runner"
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
# Listener is an abstract class to be used by requests for listening to events emitted when visiting an AST using the
|
6
|
+
# EventEmitter.
|
7
|
+
class Listener
|
8
|
+
extend T::Sig
|
9
|
+
extend T::Helpers
|
10
|
+
extend T::Generic
|
11
|
+
include Requests::Support::Common
|
12
|
+
|
13
|
+
ResponseType = type_member
|
14
|
+
|
15
|
+
abstract!
|
16
|
+
|
17
|
+
sig { params(emitter: EventEmitter, message_queue: Thread::Queue).void }
|
18
|
+
def initialize(emitter, message_queue)
|
19
|
+
@emitter = emitter
|
20
|
+
@message_queue = message_queue
|
21
|
+
end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
extend T::Sig
|
25
|
+
|
26
|
+
sig { returns(T::Array[T.class_of(Listener)]) }
|
27
|
+
def listeners
|
28
|
+
@listeners ||= T.let([], T.nilable(T::Array[T.class_of(Listener)]))
|
29
|
+
end
|
30
|
+
|
31
|
+
sig { params(listener: T.class_of(Listener)).void }
|
32
|
+
def add_listener(listener)
|
33
|
+
listeners << listener
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Override this method with an attr_reader that returns the response of your listener. The listener should
|
38
|
+
# accumulate results in a @response variable and then provide the reader so that it is accessible
|
39
|
+
sig { abstract.returns(ResponseType) }
|
40
|
+
def response; end
|
41
|
+
end
|
42
|
+
end
|
@@ -7,6 +7,7 @@ module RubyLsp
|
|
7
7
|
class BaseRequest < SyntaxTree::Visitor
|
8
8
|
extend T::Sig
|
9
9
|
extend T::Helpers
|
10
|
+
include Support::Common
|
10
11
|
|
11
12
|
abstract!
|
12
13
|
|
@@ -17,11 +18,6 @@ module RubyLsp
|
|
17
18
|
sig { params(document: Document, _kwargs: T.untyped).void }
|
18
19
|
def initialize(document, **_kwargs)
|
19
20
|
@document = document
|
20
|
-
|
21
|
-
# Parsing the document here means we're taking a lazy approach by only doing it when the first feature request
|
22
|
-
# is received by the server. This happens because {Document#parse} remembers if there are new edits to be parsed
|
23
|
-
@document.parse
|
24
|
-
|
25
21
|
super()
|
26
22
|
end
|
27
23
|
|
@@ -31,94 +27,10 @@ module RubyLsp
|
|
31
27
|
# Syntax Tree implements `visit_all` using `map` instead of `each` for users who want to use the pattern
|
32
28
|
# `result = visitor.visit(tree)`. However, we don't use that pattern and should avoid producing a new array for
|
33
29
|
# every single node visited
|
34
|
-
sig { params(nodes: T::Array[SyntaxTree::Node]).void }
|
30
|
+
sig { params(nodes: T::Array[T.nilable(SyntaxTree::Node)]).void }
|
35
31
|
def visit_all(nodes)
|
36
32
|
nodes.each { |node| visit(node) }
|
37
33
|
end
|
38
|
-
|
39
|
-
sig { params(node: SyntaxTree::Node).returns(LanguageServer::Protocol::Interface::Range) }
|
40
|
-
def range_from_syntax_tree_node(node)
|
41
|
-
loc = node.location
|
42
|
-
|
43
|
-
LanguageServer::Protocol::Interface::Range.new(
|
44
|
-
start: LanguageServer::Protocol::Interface::Position.new(
|
45
|
-
line: loc.start_line - 1,
|
46
|
-
character: loc.start_column,
|
47
|
-
),
|
48
|
-
end: LanguageServer::Protocol::Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
|
49
|
-
)
|
50
|
-
end
|
51
|
-
|
52
|
-
sig do
|
53
|
-
params(node: T.any(SyntaxTree::ConstPathRef, SyntaxTree::ConstRef, SyntaxTree::TopConstRef)).returns(String)
|
54
|
-
end
|
55
|
-
def full_constant_name(node)
|
56
|
-
name = +node.constant.value
|
57
|
-
constant = T.let(node, SyntaxTree::Node)
|
58
|
-
|
59
|
-
while constant.is_a?(SyntaxTree::ConstPathRef)
|
60
|
-
constant = constant.parent
|
61
|
-
|
62
|
-
case constant
|
63
|
-
when SyntaxTree::ConstPathRef
|
64
|
-
name.prepend("#{constant.constant.value}::")
|
65
|
-
when SyntaxTree::VarRef
|
66
|
-
name.prepend("#{constant.value.value}::")
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
name
|
71
|
-
end
|
72
|
-
|
73
|
-
sig do
|
74
|
-
params(
|
75
|
-
node: SyntaxTree::Node,
|
76
|
-
position: Integer,
|
77
|
-
node_types: T::Array[T.class_of(SyntaxTree::Node)],
|
78
|
-
).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node)])
|
79
|
-
end
|
80
|
-
def locate(node, position, node_types: [])
|
81
|
-
queue = T.let(node.child_nodes.compact, T::Array[T.nilable(SyntaxTree::Node)])
|
82
|
-
closest = node
|
83
|
-
|
84
|
-
until queue.empty?
|
85
|
-
candidate = queue.shift
|
86
|
-
|
87
|
-
# Skip nil child nodes
|
88
|
-
next if candidate.nil?
|
89
|
-
|
90
|
-
# Add the next child_nodes to the queue to be processed
|
91
|
-
queue.concat(candidate.child_nodes)
|
92
|
-
|
93
|
-
# Skip if the current node doesn't cover the desired position
|
94
|
-
loc = candidate.location
|
95
|
-
next unless (loc.start_char...loc.end_char).cover?(position)
|
96
|
-
|
97
|
-
# If the node's start character is already past the position, then we should've found the closest node already
|
98
|
-
break if position < loc.start_char
|
99
|
-
|
100
|
-
# If there are node types to filter by, and the current node is not one of those types, then skip it
|
101
|
-
next if node_types.any? && node_types.none? { |type| candidate.is_a?(type) }
|
102
|
-
|
103
|
-
# If the current node is narrower than or equal to the previous closest node, then it is more precise
|
104
|
-
closest_loc = closest.location
|
105
|
-
if loc.end_char - loc.start_char <= closest_loc.end_char - closest_loc.start_char
|
106
|
-
parent = T.let(closest, SyntaxTree::Node)
|
107
|
-
closest = candidate
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
[closest, parent]
|
112
|
-
end
|
113
|
-
|
114
|
-
sig { params(node: T.nilable(SyntaxTree::Node), range: T.nilable(T::Range[Integer])).returns(T::Boolean) }
|
115
|
-
def visible?(node, range)
|
116
|
-
return true if range.nil?
|
117
|
-
return false if node.nil?
|
118
|
-
|
119
|
-
loc = node.location
|
120
|
-
range.cover?(loc.start_line - 1) && range.cover?(loc.end_line - 1)
|
121
|
-
end
|
122
34
|
end
|
123
35
|
end
|
124
36
|
end
|