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