ruby-lsp 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|