ruby-lsp 0.14.6 → 0.16.4

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.
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