ruby-lsp 0.14.6 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/exe/ruby-lsp +1 -16
- data/exe/ruby-lsp-check +13 -22
- data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +11 -23
- data/lib/ruby_indexer/test/configuration_test.rb +2 -11
- data/lib/ruby_lsp/addon.rb +18 -5
- data/lib/ruby_lsp/base_server.rb +147 -0
- data/lib/ruby_lsp/document.rb +0 -5
- data/lib/ruby_lsp/{requests/support/dependency_detector.rb → global_state.rb} +30 -9
- data/lib/ruby_lsp/internal.rb +2 -1
- data/lib/ruby_lsp/listeners/code_lens.rb +13 -9
- data/lib/ruby_lsp/listeners/completion.rb +13 -14
- data/lib/ruby_lsp/listeners/definition.rb +4 -3
- data/lib/ruby_lsp/listeners/document_symbol.rb +91 -3
- data/lib/ruby_lsp/listeners/hover.rb +6 -5
- data/lib/ruby_lsp/listeners/signature_help.rb +7 -4
- data/lib/ruby_lsp/load_sorbet.rb +62 -0
- data/lib/ruby_lsp/requests/code_lens.rb +4 -3
- data/lib/ruby_lsp/requests/completion.rb +15 -4
- data/lib/ruby_lsp/requests/completion_resolve.rb +56 -0
- data/lib/ruby_lsp/requests/definition.rb +11 -4
- data/lib/ruby_lsp/requests/document_symbol.rb +3 -3
- data/lib/ruby_lsp/requests/hover.rb +4 -4
- data/lib/ruby_lsp/requests/signature_help.rb +4 -3
- data/lib/ruby_lsp/requests/support/common.rb +16 -5
- data/lib/ruby_lsp/requests/support/rubocop_runner.rb +4 -0
- data/lib/ruby_lsp/requests/workspace_symbol.rb +5 -4
- data/lib/ruby_lsp/requests.rb +2 -0
- data/lib/ruby_lsp/server.rb +756 -142
- data/lib/ruby_lsp/store.rb +0 -8
- data/lib/ruby_lsp/test_helper.rb +45 -0
- data/lib/ruby_lsp/utils.rb +68 -33
- metadata +8 -5
- data/lib/ruby_lsp/executor.rb +0 -614
data/lib/ruby_lsp/server.rb
CHANGED
@@ -2,167 +2,781 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
module RubyLsp
|
5
|
-
|
6
|
-
Interface = LanguageServer::Protocol::Interface
|
7
|
-
Constant = LanguageServer::Protocol::Constant
|
8
|
-
Transport = LanguageServer::Protocol::Transport
|
9
|
-
# rubocop:enable RubyLsp/UseLanguageServerAliases
|
10
|
-
|
11
|
-
class Server
|
5
|
+
class Server < BaseServer
|
12
6
|
extend T::Sig
|
13
7
|
|
8
|
+
# Only for testing
|
9
|
+
sig { returns(GlobalState) }
|
10
|
+
attr_reader :global_state
|
11
|
+
|
12
|
+
sig { params(test_mode: T::Boolean).void }
|
13
|
+
def initialize(test_mode: false)
|
14
|
+
super
|
15
|
+
@global_state = T.let(GlobalState.new, GlobalState)
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { override.params(message: T::Hash[Symbol, T.untyped]).void }
|
19
|
+
def process_message(message)
|
20
|
+
case message[:method]
|
21
|
+
when "initialize"
|
22
|
+
$stderr.puts("Initializing Ruby LSP v#{VERSION}...")
|
23
|
+
run_initialize(message)
|
24
|
+
when "initialized"
|
25
|
+
$stderr.puts("Finished initializing Ruby LSP!") unless @test_mode
|
26
|
+
run_initialized
|
27
|
+
when "textDocument/didOpen"
|
28
|
+
text_document_did_open(message)
|
29
|
+
when "textDocument/didClose"
|
30
|
+
text_document_did_close(message)
|
31
|
+
when "textDocument/didChange"
|
32
|
+
text_document_did_change(message)
|
33
|
+
when "textDocument/selectionRange"
|
34
|
+
text_document_selection_range(message)
|
35
|
+
when "textDocument/documentSymbol"
|
36
|
+
text_document_document_symbol(message)
|
37
|
+
when "textDocument/documentLink"
|
38
|
+
text_document_document_link(message)
|
39
|
+
when "textDocument/codeLens"
|
40
|
+
text_document_code_lens(message)
|
41
|
+
when "textDocument/semanticTokens/full"
|
42
|
+
text_document_semantic_tokens_full(message)
|
43
|
+
when "textDocument/foldingRange"
|
44
|
+
text_document_folding_range(message)
|
45
|
+
when "textDocument/semanticTokens/range"
|
46
|
+
text_document_semantic_tokens_range(message)
|
47
|
+
when "textDocument/formatting"
|
48
|
+
text_document_formatting(message)
|
49
|
+
when "textDocument/documentHighlight"
|
50
|
+
text_document_document_highlight(message)
|
51
|
+
when "textDocument/onTypeFormatting"
|
52
|
+
text_document_on_type_formatting(message)
|
53
|
+
when "textDocument/hover"
|
54
|
+
text_document_hover(message)
|
55
|
+
when "textDocument/inlayHint"
|
56
|
+
text_document_inlay_hint(message)
|
57
|
+
when "textDocument/codeAction"
|
58
|
+
text_document_code_action(message)
|
59
|
+
when "codeAction/resolve"
|
60
|
+
code_action_resolve(message)
|
61
|
+
when "textDocument/diagnostic"
|
62
|
+
text_document_diagnostic(message)
|
63
|
+
when "textDocument/completion"
|
64
|
+
text_document_completion(message)
|
65
|
+
when "completionItem/resolve"
|
66
|
+
text_document_completion_item_resolve(message)
|
67
|
+
when "textDocument/signatureHelp"
|
68
|
+
text_document_signature_help(message)
|
69
|
+
when "textDocument/definition"
|
70
|
+
text_document_definition(message)
|
71
|
+
when "workspace/didChangeWatchedFiles"
|
72
|
+
workspace_did_change_watched_files(message)
|
73
|
+
when "workspace/symbol"
|
74
|
+
workspace_symbol(message)
|
75
|
+
when "rubyLsp/textDocument/showSyntaxTree"
|
76
|
+
text_document_show_syntax_tree(message)
|
77
|
+
when "rubyLsp/workspace/dependencies"
|
78
|
+
workspace_dependencies(message)
|
79
|
+
when "$/cancelRequest"
|
80
|
+
@mutex.synchronize { @cancelled_requests << message[:params][:id] }
|
81
|
+
end
|
82
|
+
rescue StandardError, LoadError => e
|
83
|
+
# If an error occurred in a request, we have to return an error response or else the editor will hang
|
84
|
+
if message[:id]
|
85
|
+
send_message(Error.new(id: message[:id], code: Constant::ErrorCodes::INTERNAL_ERROR, message: e.full_message))
|
86
|
+
end
|
87
|
+
|
88
|
+
$stderr.puts("Error processing #{message[:method]}: #{e.full_message}")
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
94
|
+
def run_initialize(message)
|
95
|
+
options = message[:params]
|
96
|
+
@global_state.apply_options(options)
|
97
|
+
|
98
|
+
client_name = options.dig(:clientInfo, :name)
|
99
|
+
@store.client_name = client_name if client_name
|
100
|
+
|
101
|
+
encodings = options.dig(:capabilities, :general, :positionEncodings)
|
102
|
+
@store.encoding = if encodings.nil? || encodings.empty?
|
103
|
+
Constant::PositionEncodingKind::UTF16
|
104
|
+
elsif encodings.include?(Constant::PositionEncodingKind::UTF8)
|
105
|
+
Constant::PositionEncodingKind::UTF8
|
106
|
+
else
|
107
|
+
encodings.first
|
108
|
+
end
|
109
|
+
|
110
|
+
progress = options.dig(:capabilities, :window, :workDoneProgress)
|
111
|
+
@store.supports_progress = progress.nil? ? true : progress
|
112
|
+
formatter = options.dig(:initializationOptions, :formatter) || "auto"
|
113
|
+
|
114
|
+
configured_features = options.dig(:initializationOptions, :enabledFeatures)
|
115
|
+
@store.experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled) || false
|
116
|
+
|
117
|
+
configured_hints = options.dig(:initializationOptions, :featuresConfiguration, :inlayHint)
|
118
|
+
T.must(@store.features_configuration.dig(:inlayHint)).configuration.merge!(configured_hints) if configured_hints
|
119
|
+
|
120
|
+
enabled_features = case configured_features
|
121
|
+
when Array
|
122
|
+
# If the configuration is using an array, then absent features are disabled and present ones are enabled. That's
|
123
|
+
# why we use `false` as the default value
|
124
|
+
Hash.new(false).merge!(configured_features.to_h { |feature| [feature, true] })
|
125
|
+
when Hash
|
126
|
+
# If the configuration is already a hash, merge it with a default value of `true`. That way clients don't have
|
127
|
+
# to opt-in to every single feature
|
128
|
+
Hash.new(true).merge!(configured_features)
|
129
|
+
else
|
130
|
+
# If no configuration was passed by the client, just enable every feature
|
131
|
+
Hash.new(true)
|
132
|
+
end
|
133
|
+
|
134
|
+
document_symbol_provider = Requests::DocumentSymbol.provider if enabled_features["documentSymbols"]
|
135
|
+
document_link_provider = Requests::DocumentLink.provider if enabled_features["documentLink"]
|
136
|
+
code_lens_provider = Requests::CodeLens.provider if enabled_features["codeLens"]
|
137
|
+
hover_provider = Requests::Hover.provider if enabled_features["hover"]
|
138
|
+
folding_ranges_provider = Requests::FoldingRanges.provider if enabled_features["foldingRanges"]
|
139
|
+
semantic_tokens_provider = Requests::SemanticHighlighting.provider if enabled_features["semanticHighlighting"]
|
140
|
+
diagnostics_provider = Requests::Diagnostics.provider if enabled_features["diagnostics"]
|
141
|
+
on_type_formatting_provider = Requests::OnTypeFormatting.provider if enabled_features["onTypeFormatting"]
|
142
|
+
code_action_provider = Requests::CodeActions.provider if enabled_features["codeActions"]
|
143
|
+
inlay_hint_provider = Requests::InlayHints.provider if enabled_features["inlayHint"]
|
144
|
+
completion_provider = Requests::Completion.provider if enabled_features["completion"]
|
145
|
+
signature_help_provider = Requests::SignatureHelp.provider if enabled_features["signatureHelp"]
|
146
|
+
|
147
|
+
response = {
|
148
|
+
capabilities: Interface::ServerCapabilities.new(
|
149
|
+
text_document_sync: Interface::TextDocumentSyncOptions.new(
|
150
|
+
change: Constant::TextDocumentSyncKind::INCREMENTAL,
|
151
|
+
open_close: true,
|
152
|
+
),
|
153
|
+
position_encoding: @store.encoding,
|
154
|
+
selection_range_provider: enabled_features["selectionRanges"],
|
155
|
+
hover_provider: hover_provider,
|
156
|
+
document_symbol_provider: document_symbol_provider,
|
157
|
+
document_link_provider: document_link_provider,
|
158
|
+
folding_range_provider: folding_ranges_provider,
|
159
|
+
semantic_tokens_provider: semantic_tokens_provider,
|
160
|
+
document_formatting_provider: enabled_features["formatting"] && formatter != "none",
|
161
|
+
document_highlight_provider: enabled_features["documentHighlights"],
|
162
|
+
code_action_provider: code_action_provider,
|
163
|
+
document_on_type_formatting_provider: on_type_formatting_provider,
|
164
|
+
diagnostic_provider: diagnostics_provider,
|
165
|
+
inlay_hint_provider: inlay_hint_provider,
|
166
|
+
completion_provider: completion_provider,
|
167
|
+
code_lens_provider: code_lens_provider,
|
168
|
+
definition_provider: enabled_features["definition"],
|
169
|
+
workspace_symbol_provider: enabled_features["workspaceSymbol"],
|
170
|
+
signature_help_provider: signature_help_provider,
|
171
|
+
),
|
172
|
+
serverInfo: {
|
173
|
+
name: "Ruby LSP",
|
174
|
+
version: VERSION,
|
175
|
+
},
|
176
|
+
formatter: @global_state.formatter,
|
177
|
+
}
|
178
|
+
|
179
|
+
send_message(Result.new(id: message[:id], response: response))
|
180
|
+
|
181
|
+
# Dynamically registered capabilities
|
182
|
+
file_watching_caps = options.dig(:capabilities, :workspace, :didChangeWatchedFiles)
|
183
|
+
|
184
|
+
# Not every client supports dynamic registration or file watching
|
185
|
+
if file_watching_caps&.dig(:dynamicRegistration) && file_watching_caps&.dig(:relativePatternSupport)
|
186
|
+
send_message(
|
187
|
+
Request.new(
|
188
|
+
id: @current_request_id,
|
189
|
+
method: "client/registerCapability",
|
190
|
+
params: Interface::RegistrationParams.new(
|
191
|
+
registrations: [
|
192
|
+
# Register watching Ruby files
|
193
|
+
Interface::Registration.new(
|
194
|
+
id: "workspace/didChangeWatchedFiles",
|
195
|
+
method: "workspace/didChangeWatchedFiles",
|
196
|
+
register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
|
197
|
+
watchers: [
|
198
|
+
Interface::FileSystemWatcher.new(
|
199
|
+
glob_pattern: "**/*.rb",
|
200
|
+
kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE,
|
201
|
+
),
|
202
|
+
],
|
203
|
+
),
|
204
|
+
),
|
205
|
+
],
|
206
|
+
),
|
207
|
+
),
|
208
|
+
)
|
209
|
+
end
|
210
|
+
|
211
|
+
begin_progress("indexing-progress", "Ruby LSP: indexing files")
|
212
|
+
end
|
213
|
+
|
14
214
|
sig { void }
|
15
|
-
def
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
215
|
+
def run_initialized
|
216
|
+
Addon.load_addons(@outgoing_queue)
|
217
|
+
errored_addons = Addon.addons.select(&:error?)
|
218
|
+
|
219
|
+
if errored_addons.any?
|
220
|
+
send_message(
|
221
|
+
Notification.new(
|
222
|
+
method: "window/showMessage",
|
223
|
+
params: Interface::ShowMessageParams.new(
|
224
|
+
type: Constant::MessageType::WARNING,
|
225
|
+
message: "Error loading addons:\n\n#{errored_addons.map(&:formatted_errors).join("\n\n")}",
|
226
|
+
),
|
227
|
+
),
|
228
|
+
)
|
229
|
+
|
230
|
+
$stderr.puts(errored_addons.map(&:backtraces).join("\n\n"))
|
231
|
+
end
|
232
|
+
|
233
|
+
RubyVM::YJIT.enable if defined?(RubyVM::YJIT.enable)
|
234
|
+
|
235
|
+
indexing_config = {}
|
236
|
+
|
237
|
+
# Need to use the workspace URI, otherwise, this will fail for people working on a project that is a symlink.
|
238
|
+
index_path = File.join(@global_state.workspace_path, ".index.yml")
|
239
|
+
|
240
|
+
if File.exist?(index_path)
|
241
|
+
begin
|
242
|
+
indexing_config = YAML.parse_file(index_path).to_ruby
|
243
|
+
rescue Psych::SyntaxError => e
|
244
|
+
message = "Syntax error while loading configuration: #{e.message}"
|
245
|
+
send_message(
|
246
|
+
Notification.new(
|
247
|
+
method: "window/showMessage",
|
248
|
+
params: Interface::ShowMessageParams.new(
|
249
|
+
type: Constant::MessageType::WARNING,
|
250
|
+
message: message,
|
251
|
+
),
|
252
|
+
),
|
253
|
+
)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
perform_initial_indexing(indexing_config)
|
258
|
+
check_formatter_is_available
|
259
|
+
end
|
260
|
+
|
261
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
262
|
+
def text_document_did_open(message)
|
263
|
+
text_document = message.dig(:params, :textDocument)
|
264
|
+
@store.set(uri: text_document[:uri], source: text_document[:text], version: text_document[:version])
|
265
|
+
rescue Errno::ENOENT
|
266
|
+
# If someone re-opens the editor with a file that was deleted or doesn't exist in the current branch, we don't
|
267
|
+
# want to crash
|
268
|
+
end
|
269
|
+
|
270
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
271
|
+
def text_document_did_close(message)
|
272
|
+
uri = message.dig(:params, :textDocument, :uri)
|
273
|
+
@store.delete(uri)
|
274
|
+
|
275
|
+
# Clear diagnostics for the closed file, so that they no longer appear in the problems tab
|
276
|
+
send_message(
|
277
|
+
Notification.new(
|
278
|
+
method: "textDocument/publishDiagnostics",
|
279
|
+
params: Interface::PublishDiagnosticsParams.new(uri: uri.to_s, diagnostics: []),
|
280
|
+
),
|
54
281
|
)
|
282
|
+
end
|
283
|
+
|
284
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
285
|
+
def text_document_did_change(message)
|
286
|
+
params = message[:params]
|
287
|
+
text_document = params[:textDocument]
|
55
288
|
|
56
|
-
|
289
|
+
@mutex.synchronize do
|
290
|
+
@store.push_edits(uri: text_document[:uri], edits: params[:contentChanges], version: text_document[:version])
|
291
|
+
end
|
57
292
|
end
|
58
293
|
|
59
|
-
sig { void }
|
60
|
-
def
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
when "$/cancelRequest"
|
71
|
-
# Cancel the job if it's still in the queue
|
72
|
-
@mutex.synchronize { @jobs[request[:params][:id]]&.cancel }
|
73
|
-
when "$/setTrace"
|
74
|
-
VOID
|
75
|
-
when "shutdown"
|
76
|
-
$stderr.puts("Shutting down Ruby LSP...")
|
77
|
-
|
78
|
-
@message_queue.close
|
79
|
-
# Close the queue so that we can no longer receive items
|
80
|
-
@job_queue.close
|
81
|
-
# Clear any remaining jobs so that the thread can terminate
|
82
|
-
@job_queue.clear
|
83
|
-
@jobs.clear
|
84
|
-
# Wait until the thread is finished
|
85
|
-
@worker.join
|
86
|
-
@message_dispatcher.join
|
87
|
-
@store.clear
|
88
|
-
|
89
|
-
Addon.addons.each(&:deactivate)
|
90
|
-
finalize_request(Result.new(response: nil), request)
|
91
|
-
when "exit"
|
92
|
-
# We return zero if shutdown has already been received or one otherwise as per the recommendation in the spec
|
93
|
-
# https://microsoft.github.io/language-server-protocol/specification/#exit
|
94
|
-
status = @store.empty? ? 0 : 1
|
95
|
-
$stderr.puts("Shutdown complete with status #{status}")
|
96
|
-
exit(status)
|
97
|
-
else
|
98
|
-
# Default case: push the request to the queue to be executed by the worker
|
99
|
-
job = Job.new(request: request, cancelled: false)
|
100
|
-
|
101
|
-
@mutex.synchronize do
|
102
|
-
# Remember a handle to the job, so that we can cancel it
|
103
|
-
@jobs[request[:id]] = job
|
104
|
-
|
105
|
-
# We must parse the document under a mutex lock or else we might switch threads and accept text edits in the
|
106
|
-
# source. Altering the source reference during parsing will put the parser in an invalid internal state,
|
107
|
-
# since it started parsing with one source but then it changed in the middle
|
108
|
-
uri = request.dig(:params, :textDocument, :uri)
|
109
|
-
@store.get(URI(uri)).parse if uri
|
110
|
-
end
|
294
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
295
|
+
def text_document_selection_range(message)
|
296
|
+
uri = message.dig(:params, :textDocument, :uri)
|
297
|
+
ranges = @store.cache_fetch(uri, "textDocument/selectionRange") do |document|
|
298
|
+
Requests::SelectionRanges.new(document).perform
|
299
|
+
end
|
300
|
+
|
301
|
+
# Per the selection range request spec (https://microsoft.github.io/language-server-protocol/specification#textDocument_selectionRange),
|
302
|
+
# every position in the positions array should have an element at the same index in the response
|
303
|
+
# array. For positions without a valid selection range, the corresponding element in the response
|
304
|
+
# array will be nil.
|
111
305
|
|
112
|
-
|
306
|
+
response = message.dig(:params, :positions).map do |position|
|
307
|
+
ranges.find do |range|
|
308
|
+
range.cover?(position)
|
113
309
|
end
|
114
310
|
end
|
311
|
+
|
312
|
+
send_message(Result.new(id: message[:id], response: response))
|
115
313
|
end
|
116
314
|
|
117
|
-
|
315
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
316
|
+
def run_combined_requests(message)
|
317
|
+
uri = URI(message.dig(:params, :textDocument, :uri))
|
318
|
+
document = @store.get(uri)
|
118
319
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
320
|
+
# If the response has already been cached by another request, return it
|
321
|
+
cached_response = document.cache_get(message[:method])
|
322
|
+
if cached_response
|
323
|
+
send_message(Result.new(id: message[:id], response: cached_response))
|
324
|
+
return
|
325
|
+
end
|
326
|
+
|
327
|
+
# Run requests for the document
|
328
|
+
dispatcher = Prism::Dispatcher.new
|
329
|
+
folding_range = Requests::FoldingRanges.new(document.parse_result.comments, dispatcher)
|
330
|
+
document_symbol = Requests::DocumentSymbol.new(uri, dispatcher)
|
331
|
+
document_link = Requests::DocumentLink.new(uri, document.comments, dispatcher)
|
332
|
+
code_lens = Requests::CodeLens.new(@global_state, uri, dispatcher)
|
333
|
+
|
334
|
+
semantic_highlighting = Requests::SemanticHighlighting.new(dispatcher)
|
335
|
+
dispatcher.dispatch(document.tree)
|
336
|
+
|
337
|
+
# Store all responses retrieve in this round of visits in the cache and then return the response for the request
|
338
|
+
# we actually received
|
339
|
+
document.cache_set("textDocument/foldingRange", folding_range.perform)
|
340
|
+
document.cache_set("textDocument/documentSymbol", document_symbol.perform)
|
341
|
+
document.cache_set("textDocument/documentLink", document_link.perform)
|
342
|
+
document.cache_set("textDocument/codeLens", code_lens.perform)
|
343
|
+
document.cache_set(
|
344
|
+
"textDocument/semanticTokens/full",
|
345
|
+
semantic_highlighting.perform,
|
346
|
+
)
|
347
|
+
send_message(Result.new(id: message[:id], response: document.cache_get(message[:method])))
|
348
|
+
end
|
349
|
+
|
350
|
+
alias_method :text_document_document_symbol, :run_combined_requests
|
351
|
+
alias_method :text_document_document_link, :run_combined_requests
|
352
|
+
alias_method :text_document_code_lens, :run_combined_requests
|
353
|
+
alias_method :text_document_semantic_tokens_full, :run_combined_requests
|
354
|
+
alias_method :text_document_folding_range, :run_combined_requests
|
355
|
+
|
356
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
357
|
+
def text_document_semantic_tokens_range(message)
|
358
|
+
params = message[:params]
|
359
|
+
range = params[:range]
|
360
|
+
uri = params.dig(:textDocument, :uri)
|
361
|
+
document = @store.get(uri)
|
362
|
+
start_line = range.dig(:start, :line)
|
363
|
+
end_line = range.dig(:end, :line)
|
364
|
+
|
365
|
+
dispatcher = Prism::Dispatcher.new
|
366
|
+
request = Requests::SemanticHighlighting.new(dispatcher, range: start_line..end_line)
|
367
|
+
dispatcher.visit(document.tree)
|
368
|
+
|
369
|
+
response = request.perform
|
370
|
+
send_message(Result.new(id: message[:id], response: response))
|
371
|
+
end
|
372
|
+
|
373
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
374
|
+
def text_document_formatting(message)
|
375
|
+
# If formatter is set to `auto` but no supported formatting gem is found, don't attempt to format
|
376
|
+
if @global_state.formatter == "none"
|
377
|
+
send_empty_response(message[:id])
|
378
|
+
return
|
379
|
+
end
|
380
|
+
|
381
|
+
uri = message.dig(:params, :textDocument, :uri)
|
382
|
+
# Do not format files outside of the workspace. For example, if someone is looking at a gem's source code, we
|
383
|
+
# don't want to format it
|
384
|
+
path = uri.to_standardized_path
|
385
|
+
unless path.nil? || path.start_with?(@global_state.workspace_path)
|
386
|
+
send_empty_response(message[:id])
|
387
|
+
return
|
388
|
+
end
|
389
|
+
|
390
|
+
response = Requests::Formatting.new(@store.get(uri), formatter: @global_state.formatter).perform
|
391
|
+
send_message(Result.new(id: message[:id], response: response))
|
392
|
+
rescue Requests::Formatting::InvalidFormatter => error
|
393
|
+
send_message(Notification.window_show_error("Configuration error: #{error.message}"))
|
394
|
+
send_empty_response(message[:id])
|
395
|
+
rescue StandardError, LoadError => error
|
396
|
+
send_message(Notification.window_show_error("Formatting error: #{error.message}"))
|
397
|
+
send_empty_response(message[:id])
|
398
|
+
end
|
399
|
+
|
400
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
401
|
+
def text_document_document_highlight(message)
|
402
|
+
params = message[:params]
|
403
|
+
dispatcher = Prism::Dispatcher.new
|
404
|
+
document = @store.get(params.dig(:textDocument, :uri))
|
405
|
+
request = Requests::DocumentHighlight.new(document, params[:position], dispatcher)
|
406
|
+
dispatcher.dispatch(document.tree)
|
407
|
+
send_message(Result.new(id: message[:id], response: request.perform))
|
408
|
+
end
|
409
|
+
|
410
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
411
|
+
def text_document_on_type_formatting(message)
|
412
|
+
params = message[:params]
|
413
|
+
|
414
|
+
send_message(
|
415
|
+
Result.new(
|
416
|
+
id: message[:id],
|
417
|
+
response: Requests::OnTypeFormatting.new(
|
418
|
+
@store.get(params.dig(:textDocument, :uri)),
|
419
|
+
params[:position],
|
420
|
+
params[:ch],
|
421
|
+
@store.client_name,
|
422
|
+
).perform,
|
423
|
+
),
|
424
|
+
)
|
425
|
+
end
|
426
|
+
|
427
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
428
|
+
def text_document_hover(message)
|
429
|
+
params = message[:params]
|
430
|
+
dispatcher = Prism::Dispatcher.new
|
431
|
+
document = @store.get(params.dig(:textDocument, :uri))
|
432
|
+
|
433
|
+
send_message(
|
434
|
+
Result.new(
|
435
|
+
id: message[:id],
|
436
|
+
response: Requests::Hover.new(
|
437
|
+
document,
|
438
|
+
@global_state,
|
439
|
+
params[:position],
|
440
|
+
dispatcher,
|
441
|
+
typechecker_enabled?(document),
|
442
|
+
).perform,
|
443
|
+
),
|
444
|
+
)
|
445
|
+
end
|
446
|
+
|
447
|
+
sig { params(document: Document).returns(T::Boolean) }
|
448
|
+
def typechecker_enabled?(document)
|
449
|
+
@global_state.typechecker && document.sorbet_sigil_is_true_or_higher
|
450
|
+
end
|
451
|
+
|
452
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
453
|
+
def text_document_inlay_hint(message)
|
454
|
+
params = message[:params]
|
455
|
+
hints_configurations = T.must(@store.features_configuration.dig(:inlayHint))
|
456
|
+
dispatcher = Prism::Dispatcher.new
|
457
|
+
document = @store.get(params.dig(:textDocument, :uri))
|
458
|
+
request = Requests::InlayHints.new(document, params[:range], hints_configurations, dispatcher)
|
459
|
+
dispatcher.visit(document.tree)
|
460
|
+
send_message(Result.new(id: message[:id], response: request.perform))
|
461
|
+
end
|
462
|
+
|
463
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
464
|
+
def text_document_code_action(message)
|
465
|
+
params = message[:params]
|
466
|
+
document = @store.get(params.dig(:textDocument, :uri))
|
467
|
+
|
468
|
+
send_message(
|
469
|
+
Result.new(
|
470
|
+
id: message[:id],
|
471
|
+
response: Requests::CodeActions.new(
|
472
|
+
document,
|
473
|
+
params[:range],
|
474
|
+
params[:context],
|
475
|
+
).perform,
|
476
|
+
),
|
477
|
+
)
|
478
|
+
end
|
479
|
+
|
480
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
481
|
+
def code_action_resolve(message)
|
482
|
+
params = message[:params]
|
483
|
+
uri = URI(params.dig(:data, :uri))
|
484
|
+
document = @store.get(uri)
|
485
|
+
result = Requests::CodeActionResolve.new(document, params).perform
|
486
|
+
|
487
|
+
case result
|
488
|
+
when Requests::CodeActionResolve::Error::EmptySelection
|
489
|
+
send_message(Notification.window_show_error("Invalid selection for Extract Variable refactor"))
|
490
|
+
raise Requests::CodeActionResolve::CodeActionError
|
491
|
+
when Requests::CodeActionResolve::Error::InvalidTargetRange
|
492
|
+
send_message(
|
493
|
+
Notification.window_show_error(
|
494
|
+
"Couldn't find an appropriate location to place extracted refactor",
|
495
|
+
),
|
496
|
+
)
|
497
|
+
raise Requests::CodeActionResolve::CodeActionError
|
498
|
+
else
|
499
|
+
send_message(Result.new(id: message[:id], response: result))
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
504
|
+
def text_document_diagnostic(message)
|
505
|
+
# Do not compute diagnostics for files outside of the workspace. For example, if someone is looking at a gem's
|
506
|
+
# source code, we don't want to show diagnostics for it
|
507
|
+
uri = message.dig(:params, :textDocument, :uri)
|
508
|
+
path = uri.to_standardized_path
|
509
|
+
unless path.nil? || path.start_with?(@global_state.workspace_path)
|
510
|
+
send_empty_response(message[:id])
|
511
|
+
return
|
512
|
+
end
|
513
|
+
|
514
|
+
response = @store.cache_fetch(uri, "textDocument/diagnostic") do |document|
|
515
|
+
Requests::Diagnostics.new(document).perform
|
516
|
+
end
|
517
|
+
|
518
|
+
send_message(
|
519
|
+
Result.new(
|
520
|
+
id: message[:id],
|
521
|
+
response: response && Interface::FullDocumentDiagnosticReport.new(kind: "full", items: response),
|
522
|
+
),
|
523
|
+
)
|
524
|
+
rescue StandardError, LoadError => error
|
525
|
+
send_message(Notification.window_show_error("Error running diagnostics: #{error.message}"))
|
526
|
+
send_empty_response(message[:id])
|
527
|
+
end
|
528
|
+
|
529
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
530
|
+
def text_document_completion(message)
|
531
|
+
params = message[:params]
|
532
|
+
dispatcher = Prism::Dispatcher.new
|
533
|
+
document = @store.get(params.dig(:textDocument, :uri))
|
534
|
+
|
535
|
+
send_message(
|
536
|
+
Result.new(
|
537
|
+
id: message[:id],
|
538
|
+
response: Requests::Completion.new(
|
539
|
+
document,
|
540
|
+
@global_state,
|
541
|
+
params[:position],
|
542
|
+
typechecker_enabled?(document),
|
543
|
+
dispatcher,
|
544
|
+
).perform,
|
545
|
+
),
|
546
|
+
)
|
547
|
+
end
|
548
|
+
|
549
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
550
|
+
def text_document_completion_item_resolve(message)
|
551
|
+
send_message(Result.new(
|
552
|
+
id: message[:id],
|
553
|
+
response: Requests::CompletionResolve.new(@global_state, message[:params]).perform,
|
554
|
+
))
|
555
|
+
end
|
556
|
+
|
557
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
558
|
+
def text_document_signature_help(message)
|
559
|
+
params = message[:params]
|
560
|
+
dispatcher = Prism::Dispatcher.new
|
561
|
+
document = @store.get(params.dig(:textDocument, :uri))
|
562
|
+
|
563
|
+
send_message(
|
564
|
+
Result.new(
|
565
|
+
id: message[:id],
|
566
|
+
response: Requests::SignatureHelp.new(
|
567
|
+
document,
|
568
|
+
@global_state,
|
569
|
+
params[:position],
|
570
|
+
params[:context],
|
571
|
+
dispatcher,
|
572
|
+
typechecker_enabled?(document),
|
573
|
+
).perform,
|
574
|
+
),
|
575
|
+
)
|
576
|
+
end
|
577
|
+
|
578
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
579
|
+
def text_document_definition(message)
|
580
|
+
params = message[:params]
|
581
|
+
dispatcher = Prism::Dispatcher.new
|
582
|
+
document = @store.get(params.dig(:textDocument, :uri))
|
583
|
+
|
584
|
+
send_message(
|
585
|
+
Result.new(
|
586
|
+
id: message[:id],
|
587
|
+
response: Requests::Definition.new(
|
588
|
+
document,
|
589
|
+
@global_state,
|
590
|
+
params[:position],
|
591
|
+
dispatcher,
|
592
|
+
typechecker_enabled?(document),
|
593
|
+
).perform,
|
594
|
+
),
|
595
|
+
)
|
596
|
+
end
|
597
|
+
|
598
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
599
|
+
def workspace_did_change_watched_files(message)
|
600
|
+
changes = message.dig(:params, :changes)
|
601
|
+
index = @global_state.index
|
602
|
+
changes.each do |change|
|
603
|
+
# File change events include folders, but we're only interested in files
|
604
|
+
uri = URI(change[:uri])
|
605
|
+
file_path = uri.to_standardized_path
|
606
|
+
next if file_path.nil? || File.directory?(file_path)
|
607
|
+
|
608
|
+
load_path_entry = $LOAD_PATH.find { |load_path| file_path.start_with?(load_path) }
|
609
|
+
indexable = RubyIndexer::IndexablePath.new(load_path_entry, file_path)
|
137
610
|
|
138
|
-
|
611
|
+
case change[:type]
|
612
|
+
when Constant::FileChangeType::CREATED
|
613
|
+
index.index_single(indexable)
|
614
|
+
when Constant::FileChangeType::CHANGED
|
615
|
+
index.delete(indexable)
|
616
|
+
index.index_single(indexable)
|
617
|
+
when Constant::FileChangeType::DELETED
|
618
|
+
index.delete(indexable)
|
139
619
|
end
|
140
620
|
end
|
621
|
+
|
622
|
+
Addon.file_watcher_addons.each { |addon| T.unsafe(addon).workspace_did_change_watched_files(changes) }
|
141
623
|
end
|
142
624
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
625
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
626
|
+
def workspace_symbol(message)
|
627
|
+
send_message(
|
628
|
+
Result.new(
|
629
|
+
id: message[:id],
|
630
|
+
response: Requests::WorkspaceSymbol.new(
|
631
|
+
@global_state,
|
632
|
+
message.dig(:params, :query),
|
633
|
+
).perform,
|
634
|
+
),
|
635
|
+
)
|
636
|
+
end
|
637
|
+
|
638
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
639
|
+
def text_document_show_syntax_tree(message)
|
640
|
+
params = message[:params]
|
641
|
+
response = {
|
642
|
+
ast: Requests::ShowSyntaxTree.new(
|
643
|
+
@store.get(params.dig(:textDocument, :uri)),
|
644
|
+
params[:range],
|
645
|
+
).perform,
|
646
|
+
}
|
647
|
+
send_message(Result.new(id: message[:id], response: response))
|
648
|
+
end
|
649
|
+
|
650
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
651
|
+
def workspace_dependencies(message)
|
652
|
+
response = begin
|
653
|
+
Bundler.with_original_env do
|
654
|
+
definition = Bundler.definition
|
655
|
+
dep_keys = definition.locked_deps.keys.to_set
|
656
|
+
|
657
|
+
definition.specs.map do |spec|
|
658
|
+
{
|
659
|
+
name: spec.name,
|
660
|
+
version: spec.version,
|
661
|
+
path: spec.full_gem_path,
|
662
|
+
dependency: dep_keys.include?(spec.name),
|
663
|
+
}
|
664
|
+
end
|
665
|
+
end
|
666
|
+
rescue Bundler::GemfileNotFound
|
667
|
+
[]
|
668
|
+
end
|
669
|
+
|
670
|
+
send_message(Result.new(id: message[:id], response: response))
|
671
|
+
end
|
672
|
+
|
673
|
+
sig { override.void }
|
674
|
+
def shutdown
|
675
|
+
Addon.addons.each(&:deactivate)
|
676
|
+
end
|
677
|
+
|
678
|
+
sig { params(config_hash: T::Hash[String, T.untyped]).void }
|
679
|
+
def perform_initial_indexing(config_hash)
|
680
|
+
# The begin progress invocation happens during `initialize`, so that the notification is sent before we are
|
681
|
+
# stuck indexing files
|
682
|
+
RubyIndexer.configuration.apply_config(config_hash)
|
683
|
+
|
684
|
+
Thread.new do
|
685
|
+
begin
|
686
|
+
@global_state.index.index_all do |percentage|
|
687
|
+
progress("indexing-progress", percentage)
|
688
|
+
true
|
689
|
+
rescue ClosedQueueError
|
690
|
+
# Since we run indexing on a separate thread, it's possible to kill the server before indexing is complete.
|
691
|
+
# In those cases, the message queue will be closed and raise a ClosedQueueError. By returning `false`, we
|
692
|
+
# tell the index to stop working immediately
|
693
|
+
false
|
694
|
+
end
|
695
|
+
rescue StandardError => error
|
696
|
+
send_message(Notification.window_show_error("Error while indexing: #{error.message}"))
|
165
697
|
end
|
698
|
+
|
699
|
+
# Always end the progress notification even if indexing failed or else it never goes away and the user has no
|
700
|
+
# way of dismissing it
|
701
|
+
end_progress("indexing-progress")
|
702
|
+
end
|
703
|
+
end
|
704
|
+
|
705
|
+
sig { params(id: String, title: String, percentage: Integer).void }
|
706
|
+
def begin_progress(id, title, percentage: 0)
|
707
|
+
return unless @store.supports_progress
|
708
|
+
|
709
|
+
send_message(Request.new(
|
710
|
+
id: @current_request_id,
|
711
|
+
method: "window/workDoneProgress/create",
|
712
|
+
params: Interface::WorkDoneProgressCreateParams.new(token: id),
|
713
|
+
))
|
714
|
+
|
715
|
+
send_message(Notification.new(
|
716
|
+
method: "$/progress",
|
717
|
+
params: Interface::ProgressParams.new(
|
718
|
+
token: id,
|
719
|
+
value: Interface::WorkDoneProgressBegin.new(
|
720
|
+
kind: "begin",
|
721
|
+
title: title,
|
722
|
+
percentage: percentage,
|
723
|
+
message: "#{percentage}% completed",
|
724
|
+
),
|
725
|
+
),
|
726
|
+
))
|
727
|
+
end
|
728
|
+
|
729
|
+
sig { params(id: String, percentage: Integer).void }
|
730
|
+
def progress(id, percentage)
|
731
|
+
return unless @store.supports_progress
|
732
|
+
|
733
|
+
send_message(
|
734
|
+
Notification.new(
|
735
|
+
method: "$/progress",
|
736
|
+
params: Interface::ProgressParams.new(
|
737
|
+
token: id,
|
738
|
+
value: Interface::WorkDoneProgressReport.new(
|
739
|
+
kind: "report",
|
740
|
+
percentage: percentage,
|
741
|
+
message: "#{percentage}% completed",
|
742
|
+
),
|
743
|
+
),
|
744
|
+
),
|
745
|
+
)
|
746
|
+
end
|
747
|
+
|
748
|
+
sig { params(id: String).void }
|
749
|
+
def end_progress(id)
|
750
|
+
return unless @store.supports_progress
|
751
|
+
|
752
|
+
send_message(
|
753
|
+
Notification.new(
|
754
|
+
method: "$/progress",
|
755
|
+
params: Interface::ProgressParams.new(
|
756
|
+
token: id,
|
757
|
+
value: Interface::WorkDoneProgressEnd.new(kind: "end"),
|
758
|
+
),
|
759
|
+
),
|
760
|
+
)
|
761
|
+
rescue ClosedQueueError
|
762
|
+
# If the server was killed and the message queue is already closed, there's no way to end the progress
|
763
|
+
# notification
|
764
|
+
end
|
765
|
+
|
766
|
+
sig { void }
|
767
|
+
def check_formatter_is_available
|
768
|
+
# Warn of an unavailable `formatter` setting, e.g. `rubocop` on a project which doesn't have RuboCop.
|
769
|
+
# Syntax Tree will always be available via Ruby LSP so we don't need to check for it.
|
770
|
+
return unless @global_state.formatter == "rubocop"
|
771
|
+
|
772
|
+
unless defined?(RubyLsp::Requests::Support::RuboCopRunner)
|
773
|
+
@global_state.formatter = "none"
|
774
|
+
|
775
|
+
send_message(
|
776
|
+
Notification.window_show_error(
|
777
|
+
"Ruby LSP formatter is set to `rubocop` but RuboCop was not found in the Gemfile or gemspec.",
|
778
|
+
),
|
779
|
+
)
|
166
780
|
end
|
167
781
|
end
|
168
782
|
end
|