ruby-lsp 0.14.6 → 0.16.0

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