ruby-lsp 0.14.5 → 0.15.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/VERSION +1 -1
- data/exe/ruby-lsp +1 -16
- data/exe/ruby-lsp-check +13 -22
- data/lib/ruby_indexer/lib/ruby_indexer/collector.rb +21 -0
- data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +12 -24
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +16 -0
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +3 -2
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +46 -0
- data/lib/ruby_indexer/test/configuration_test.rb +2 -11
- data/lib/ruby_indexer/test/index_test.rb +5 -0
- 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 +66 -18
- 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 +18 -5
- data/lib/ruby_lsp/requests/document_symbol.rb +3 -3
- data/lib/ruby_lsp/requests/hover.rb +9 -5
- data/lib/ruby_lsp/requests/request.rb +51 -0
- data/lib/ruby_lsp/requests/signature_help.rb +4 -3
- data/lib/ruby_lsp/requests/support/common.rb +25 -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 -612
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
|