ruby-lsp 0.14.6 → 0.16.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +1 -16
  5. data/exe/ruby-lsp-check +13 -22
  6. data/exe/ruby-lsp-doctor +9 -0
  7. data/lib/ruby_indexer/lib/ruby_indexer/collector.rb +14 -1
  8. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +11 -23
  9. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +4 -0
  10. data/lib/ruby_indexer/test/classes_and_modules_test.rb +46 -0
  11. data/lib/ruby_indexer/test/configuration_test.rb +2 -11
  12. data/lib/ruby_lsp/addon.rb +18 -9
  13. data/lib/ruby_lsp/base_server.rb +149 -0
  14. data/lib/ruby_lsp/document.rb +6 -11
  15. data/lib/ruby_lsp/global_state.rb +169 -0
  16. data/lib/ruby_lsp/internal.rb +4 -1
  17. data/lib/ruby_lsp/listeners/code_lens.rb +22 -13
  18. data/lib/ruby_lsp/listeners/completion.rb +13 -14
  19. data/lib/ruby_lsp/listeners/definition.rb +4 -3
  20. data/lib/ruby_lsp/listeners/document_symbol.rb +91 -3
  21. data/lib/ruby_lsp/listeners/hover.rb +6 -5
  22. data/lib/ruby_lsp/listeners/signature_help.rb +7 -4
  23. data/lib/ruby_lsp/load_sorbet.rb +62 -0
  24. data/lib/ruby_lsp/requests/code_lens.rb +3 -2
  25. data/lib/ruby_lsp/requests/completion.rb +15 -4
  26. data/lib/ruby_lsp/requests/completion_resolve.rb +56 -0
  27. data/lib/ruby_lsp/requests/definition.rb +11 -4
  28. data/lib/ruby_lsp/requests/diagnostics.rb +6 -12
  29. data/lib/ruby_lsp/requests/document_symbol.rb +3 -3
  30. data/lib/ruby_lsp/requests/formatting.rb +7 -43
  31. data/lib/ruby_lsp/requests/hover.rb +4 -4
  32. data/lib/ruby_lsp/requests/request.rb +2 -0
  33. data/lib/ruby_lsp/requests/semantic_highlighting.rb +1 -1
  34. data/lib/ruby_lsp/requests/signature_help.rb +4 -3
  35. data/lib/ruby_lsp/requests/support/common.rb +16 -5
  36. data/lib/ruby_lsp/requests/support/formatter.rb +26 -0
  37. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +1 -1
  38. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +47 -0
  39. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +4 -0
  40. data/lib/ruby_lsp/requests/support/{syntax_tree_formatting_runner.rb → syntax_tree_formatter.rb} +13 -6
  41. data/lib/ruby_lsp/requests/workspace_symbol.rb +5 -4
  42. data/lib/ruby_lsp/requests.rb +3 -1
  43. data/lib/ruby_lsp/server.rb +763 -142
  44. data/lib/ruby_lsp/setup_bundler.rb +13 -1
  45. data/lib/ruby_lsp/store.rb +3 -15
  46. data/lib/ruby_lsp/test_helper.rb +52 -0
  47. data/lib/ruby_lsp/utils.rb +68 -33
  48. metadata +11 -9
  49. data/lib/ruby_lsp/executor.rb +0 -614
  50. data/lib/ruby_lsp/requests/support/dependency_detector.rb +0 -93
  51. data/lib/ruby_lsp/requests/support/formatter_runner.rb +0 -18
  52. data/lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb +0 -34
  53. data/lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb +0 -35
@@ -2,168 +2,789 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module RubyLsp
5
- # rubocop:disable RubyLsp/UseLanguageServerAliases
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
- sig { void }
15
- def initialize
16
- @writer = T.let(Transport::Stdio::Writer.new, Transport::Stdio::Writer)
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
- Thread.main.priority = 1
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 start
61
- $stderr.puts("Starting Ruby LSP v#{VERSION}...")
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
- @job_queue << job
113
- end
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 { returns(Thread) }
120
- def new_worker
121
- Thread.new do
122
- # Thread::Queue#pop is thread safe and will wait until an item is available
123
- loop do
124
- job = T.let(@job_queue.pop, T.nilable(Job))
125
- # The only time when the job is nil is when the queue is closed and we can then terminate the thread
126
- break if job.nil?
127
-
128
- request = job.request
129
- @mutex.synchronize { @jobs.delete(request[:id]) }
130
-
131
- result = if job.cancelled
132
- # We need to return nil to the client even if the request was cancelled
133
- Result.new(response: nil)
134
- else
135
- @executor.execute(request)
136
- end
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
- finalize_request(result, request)
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
- # Finalize a Queue::Result. All IO operations should happen here to avoid any issues with cancelling requests
144
- sig { params(result: Result, request: T::Hash[Symbol, T.untyped]).void }
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
- error = result.error
148
- response = result.response
149
-
150
- if error
151
- @writer.write(
152
- id: request[:id],
153
- error: {
154
- code: Constant::ErrorCodes::INTERNAL_ERROR,
155
- message: error.inspect,
156
- data: {
157
- errorClass: error.class.name,
158
- errorMessage: error.message,
159
- backtrace: error.backtrace&.map { |bt| bt.sub(/^#{Dir.home}/, "~") }&.join("\n"),
160
- },
161
- },
162
- )
163
- elsif response != VOID
164
- @writer.write(id: request[:id], result: response)
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(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(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