ruby-lsp 0.7.6 → 0.8.1

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/exe/ruby-lsp +41 -33
  4. data/exe/ruby-lsp-check +2 -2
  5. data/lib/core_ext/uri.rb +40 -0
  6. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +91 -0
  7. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +122 -0
  8. data/lib/ruby_indexer/lib/ruby_indexer/visitor.rb +121 -0
  9. data/lib/ruby_indexer/ruby_indexer.rb +19 -0
  10. data/lib/ruby_indexer/test/classes_and_modules_test.rb +204 -0
  11. data/lib/ruby_indexer/test/configuration_test.rb +35 -0
  12. data/lib/ruby_indexer/test/constant_test.rb +108 -0
  13. data/lib/ruby_indexer/test/index_test.rb +94 -0
  14. data/lib/ruby_indexer/test/test_case.rb +42 -0
  15. data/lib/ruby_lsp/document.rb +3 -3
  16. data/lib/ruby_lsp/executor.rb +131 -24
  17. data/lib/ruby_lsp/extension.rb +24 -0
  18. data/lib/ruby_lsp/internal.rb +4 -0
  19. data/lib/ruby_lsp/listener.rb +15 -14
  20. data/lib/ruby_lsp/requests/code_actions.rb +3 -3
  21. data/lib/ruby_lsp/requests/code_lens.rb +10 -24
  22. data/lib/ruby_lsp/requests/definition.rb +55 -8
  23. data/lib/ruby_lsp/requests/diagnostics.rb +3 -2
  24. data/lib/ruby_lsp/requests/document_link.rb +4 -3
  25. data/lib/ruby_lsp/requests/formatting.rb +3 -2
  26. data/lib/ruby_lsp/requests/hover.rb +4 -18
  27. data/lib/ruby_lsp/requests/on_type_formatting.rb +4 -6
  28. data/lib/ruby_lsp/requests/support/dependency_detector.rb +5 -0
  29. data/lib/ruby_lsp/requests/support/formatter_runner.rb +1 -1
  30. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +2 -2
  31. data/lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb +2 -3
  32. data/lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb +2 -3
  33. data/lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb +3 -2
  34. data/lib/ruby_lsp/server.rb +10 -2
  35. data/lib/ruby_lsp/setup_bundler.rb +28 -14
  36. data/lib/ruby_lsp/store.rb +20 -13
  37. data/lib/ruby_lsp/utils.rb +1 -1
  38. metadata +27 -3
@@ -15,6 +15,7 @@ module RubyLsp
15
15
  @store = store
16
16
  @test_library = T.let(DependencyDetector.detected_test_library, String)
17
17
  @message_queue = message_queue
18
+ @index = T.let(RubyIndexer::Index.new, RubyIndexer::Index)
18
19
  end
19
20
 
20
21
  sig { params(request: T::Hash[Symbol, T.untyped]).returns(Result) }
@@ -35,7 +36,7 @@ module RubyLsp
35
36
 
36
37
  sig { params(request: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
37
38
  def run(request)
38
- uri = request.dig(:params, :textDocument, :uri)
39
+ uri = URI(request.dig(:params, :textDocument, :uri).to_s)
39
40
 
40
41
  case request[:method]
41
42
  when "initialize"
@@ -57,6 +58,14 @@ module RubyLsp
57
58
  warn(errored_extensions.map(&:backtraces).join("\n\n"))
58
59
  end
59
60
 
61
+ if @store.experimental_features
62
+ # The begin progress invocation happens during `initialize`, so that the notification is sent before we are
63
+ # stuck indexing files
64
+ RubyIndexer.configuration.load_config
65
+ @index.index_all
66
+ end_progress("indexing-progress")
67
+ end
68
+
60
69
  check_formatter_is_available
61
70
 
62
71
  warn("Ruby LSP is ready")
@@ -70,7 +79,7 @@ module RubyLsp
70
79
  when "textDocument/didClose"
71
80
  @message_queue << Notification.new(
72
81
  message: "textDocument/publishDiagnostics",
73
- params: Interface::PublishDiagnosticsParams.new(uri: uri, diagnostics: []),
82
+ params: Interface::PublishDiagnosticsParams.new(uri: uri.to_s, diagnostics: []),
74
83
  )
75
84
 
76
85
  text_document_did_close(uri)
@@ -169,30 +178,64 @@ module RubyLsp
169
178
  completion(uri, request.dig(:params, :position))
170
179
  when "textDocument/definition"
171
180
  definition(uri, request.dig(:params, :position))
181
+ when "workspace/didChangeWatchedFiles"
182
+ did_change_watched_files(request.dig(:params, :changes))
172
183
  when "rubyLsp/textDocument/showSyntaxTree"
173
184
  show_syntax_tree(uri, request.dig(:params, :range))
174
185
  end
175
186
  end
176
187
 
177
- sig { params(uri: String, range: T.nilable(Document::RangeShape)).returns({ ast: String }) }
188
+ sig { params(changes: T::Array[{ uri: String, type: Integer }]).returns(Object) }
189
+ def did_change_watched_files(changes)
190
+ changes.each do |change|
191
+ # File change events include folders, but we're only interested in files
192
+ uri = URI(change[:uri])
193
+ file_path = uri.to_standardized_path
194
+ next if file_path.nil? || File.directory?(file_path)
195
+
196
+ case change[:type]
197
+ when Constant::FileChangeType::CREATED
198
+ @index.index_single(file_path)
199
+ when Constant::FileChangeType::CHANGED
200
+ @index.delete(file_path)
201
+ @index.index_single(file_path)
202
+ when Constant::FileChangeType::DELETED
203
+ @index.delete(file_path)
204
+ end
205
+ end
206
+
207
+ VOID
208
+ end
209
+
210
+ sig { params(uri: URI::Generic, range: T.nilable(Document::RangeShape)).returns({ ast: String }) }
178
211
  def show_syntax_tree(uri, range)
179
212
  { ast: Requests::ShowSyntaxTree.new(@store.get(uri), range).run }
180
213
  end
181
214
 
182
- sig { params(uri: String, position: Document::PositionShape).returns(T.nilable(Interface::Location)) }
215
+ sig do
216
+ params(
217
+ uri: URI::Generic,
218
+ position: Document::PositionShape,
219
+ ).returns(T.nilable(T.any(T::Array[Interface::Location], Interface::Location)))
220
+ end
183
221
  def definition(uri, position)
184
222
  document = @store.get(uri)
185
223
  return if document.syntax_error?
186
224
 
187
- target, _parent = document.locate_node(position, node_types: [SyntaxTree::Command])
225
+ target, parent, nesting = document.locate_node(
226
+ position,
227
+ node_types: [SyntaxTree::Command, SyntaxTree::Const, SyntaxTree::ConstPathRef],
228
+ )
229
+
230
+ target = parent if target.is_a?(SyntaxTree::Const) && parent.is_a?(SyntaxTree::ConstPathRef)
188
231
 
189
232
  emitter = EventEmitter.new
190
- base_listener = Requests::Definition.new(uri, emitter, @message_queue)
233
+ base_listener = Requests::Definition.new(uri, nesting, @index, emitter, @message_queue)
191
234
  emitter.emit_for_target(target)
192
235
  base_listener.response
193
236
  end
194
237
 
195
- sig { params(uri: String).returns(T::Array[Interface::FoldingRange]) }
238
+ sig { params(uri: URI::Generic).returns(T::Array[Interface::FoldingRange]) }
196
239
  def folding_range(uri)
197
240
  @store.cache_fetch(uri, "textDocument/foldingRange") do |document|
198
241
  Requests::FoldingRanges.new(document).run
@@ -201,7 +244,7 @@ module RubyLsp
201
244
 
202
245
  sig do
203
246
  params(
204
- uri: String,
247
+ uri: URI::Generic,
205
248
  position: Document::PositionShape,
206
249
  ).returns(T.nilable(Interface::Hover))
207
250
  end
@@ -227,19 +270,19 @@ module RubyLsp
227
270
  hover.response
228
271
  end
229
272
 
230
- sig { params(uri: String, content_changes: T::Array[Document::EditShape], version: Integer).returns(Object) }
273
+ sig { params(uri: URI::Generic, content_changes: T::Array[Document::EditShape], version: Integer).returns(Object) }
231
274
  def text_document_did_change(uri, content_changes, version)
232
275
  @store.push_edits(uri: uri, edits: content_changes, version: version)
233
276
  VOID
234
277
  end
235
278
 
236
- sig { params(uri: String, text: String, version: Integer).returns(Object) }
279
+ sig { params(uri: URI::Generic, text: String, version: Integer).returns(Object) }
237
280
  def text_document_did_open(uri, text, version)
238
281
  @store.set(uri: uri, source: text, version: version)
239
282
  VOID
240
283
  end
241
284
 
242
- sig { params(uri: String).returns(Object) }
285
+ sig { params(uri: URI::Generic).returns(Object) }
243
286
  def text_document_did_close(uri)
244
287
  @store.delete(uri)
245
288
  VOID
@@ -247,7 +290,7 @@ module RubyLsp
247
290
 
248
291
  sig do
249
292
  params(
250
- uri: String,
293
+ uri: URI::Generic,
251
294
  positions: T::Array[Document::PositionShape],
252
295
  ).returns(T.nilable(T::Array[T.nilable(Requests::Support::SelectionRange)]))
253
296
  end
@@ -270,7 +313,7 @@ module RubyLsp
270
313
  end
271
314
  end
272
315
 
273
- sig { params(uri: String).returns(T.nilable(T::Array[Interface::TextEdit])) }
316
+ sig { params(uri: URI::Generic).returns(T.nilable(T::Array[Interface::TextEdit])) }
274
317
  def formatting(uri)
275
318
  # If formatter is set to `auto` but no supported formatting gem is found, don't attempt to format
276
319
  return if @store.formatter == "none"
@@ -280,7 +323,7 @@ module RubyLsp
280
323
 
281
324
  sig do
282
325
  params(
283
- uri: String,
326
+ uri: URI::Generic,
284
327
  position: Document::PositionShape,
285
328
  character: String,
286
329
  ).returns(T::Array[Interface::TextEdit])
@@ -291,7 +334,7 @@ module RubyLsp
291
334
 
292
335
  sig do
293
336
  params(
294
- uri: String,
337
+ uri: URI::Generic,
295
338
  position: Document::PositionShape,
296
339
  ).returns(T.nilable(T::Array[Interface::DocumentHighlight]))
297
340
  end
@@ -306,7 +349,7 @@ module RubyLsp
306
349
  listener.response
307
350
  end
308
351
 
309
- sig { params(uri: String, range: Document::RangeShape).returns(T.nilable(T::Array[Interface::InlayHint])) }
352
+ sig { params(uri: URI::Generic, range: Document::RangeShape).returns(T.nilable(T::Array[Interface::InlayHint])) }
310
353
  def inlay_hint(uri, range)
311
354
  document = @store.get(uri)
312
355
  return if document.syntax_error?
@@ -322,7 +365,7 @@ module RubyLsp
322
365
 
323
366
  sig do
324
367
  params(
325
- uri: String,
368
+ uri: URI::Generic,
326
369
  range: Document::RangeShape,
327
370
  context: T::Hash[Symbol, T.untyped],
328
371
  ).returns(T.nilable(T::Array[Interface::CodeAction]))
@@ -335,7 +378,7 @@ module RubyLsp
335
378
 
336
379
  sig { params(params: T::Hash[Symbol, T.untyped]).returns(Interface::CodeAction) }
337
380
  def code_action_resolve(params)
338
- uri = params.dig(:data, :uri)
381
+ uri = URI(params.dig(:data, :uri))
339
382
  document = @store.get(uri)
340
383
  result = Requests::CodeActionResolve.new(document, params).run
341
384
 
@@ -363,7 +406,7 @@ module RubyLsp
363
406
  end
364
407
  end
365
408
 
366
- sig { params(uri: String).returns(T.nilable(Interface::FullDocumentDiagnosticReport)) }
409
+ sig { params(uri: URI::Generic).returns(T.nilable(Interface::FullDocumentDiagnosticReport)) }
367
410
  def diagnostic(uri)
368
411
  response = @store.cache_fetch(uri, "textDocument/diagnostic") do |document|
369
412
  Requests::Diagnostics.new(document).run
@@ -372,7 +415,7 @@ module RubyLsp
372
415
  Interface::FullDocumentDiagnosticReport.new(kind: "full", items: response.map(&:to_lsp_diagnostic)) if response
373
416
  end
374
417
 
375
- sig { params(uri: String, range: Document::RangeShape).returns(Interface::SemanticTokens) }
418
+ sig { params(uri: URI::Generic, range: Document::RangeShape).returns(Interface::SemanticTokens) }
376
419
  def semantic_tokens_range(uri, range)
377
420
  document = @store.get(uri)
378
421
  start_line = range.dig(:start, :line)
@@ -390,7 +433,10 @@ module RubyLsp
390
433
  end
391
434
 
392
435
  sig do
393
- params(uri: String, position: Document::PositionShape).returns(T.nilable(T::Array[Interface::CompletionItem]))
436
+ params(
437
+ uri: URI::Generic,
438
+ position: Document::PositionShape,
439
+ ).returns(T.nilable(T::Array[Interface::CompletionItem]))
394
440
  end
395
441
  def completion(uri, position)
396
442
  document = @store.get(uri)
@@ -433,6 +479,37 @@ module RubyLsp
433
479
  listener.response
434
480
  end
435
481
 
482
+ sig { params(id: String, title: String).void }
483
+ def begin_progress(id, title)
484
+ return unless @store.supports_progress
485
+
486
+ @message_queue << Request.new(
487
+ message: "window/workDoneProgress/create",
488
+ params: Interface::WorkDoneProgressCreateParams.new(token: id),
489
+ )
490
+
491
+ @message_queue << Notification.new(
492
+ message: "$/progress",
493
+ params: Interface::ProgressParams.new(
494
+ token: id,
495
+ value: Interface::WorkDoneProgressBegin.new(kind: "begin", title: title),
496
+ ),
497
+ )
498
+ end
499
+
500
+ sig { params(id: String).void }
501
+ def end_progress(id)
502
+ return unless @store.supports_progress
503
+
504
+ @message_queue << Notification.new(
505
+ message: "$/progress",
506
+ params: Interface::ProgressParams.new(
507
+ token: id,
508
+ value: Interface::WorkDoneProgressEnd.new(kind: "end"),
509
+ ),
510
+ )
511
+ end
512
+
436
513
  sig { params(options: T::Hash[Symbol, T.untyped]).returns(Interface::InitializeResult) }
437
514
  def initialize_request(options)
438
515
  @store.clear
@@ -446,6 +523,7 @@ module RubyLsp
446
523
  encodings.first
447
524
  end
448
525
 
526
+ @store.supports_progress = options.dig(:capabilities, :window, :workDoneProgress) || true
449
527
  formatter = options.dig(:initializationOptions, :formatter) || "auto"
450
528
  @store.formatter = if formatter == "auto"
451
529
  DependencyDetector.detected_formatter
@@ -454,9 +532,7 @@ module RubyLsp
454
532
  end
455
533
 
456
534
  configured_features = options.dig(:initializationOptions, :enabledFeatures)
457
-
458
- # Uncomment the line below and use the variable to gate features behind the experimental flag
459
- # experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled)
535
+ @store.experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled) || false
460
536
 
461
537
  enabled_features = case configured_features
462
538
  when Array
@@ -538,6 +614,37 @@ module RubyLsp
538
614
  )
539
615
  end
540
616
 
617
+ if @store.experimental_features
618
+ # Dynamically registered capabilities
619
+ file_watching_caps = options.dig(:capabilities, :workspace, :didChangeWatchedFiles)
620
+
621
+ # Not every client supports dynamic registration or file watching
622
+ if file_watching_caps&.dig(:dynamicRegistration) && file_watching_caps&.dig(:relativePatternSupport)
623
+ @message_queue << Request.new(
624
+ message: "client/registerCapability",
625
+ params: Interface::RegistrationParams.new(
626
+ registrations: [
627
+ # Register watching Ruby files
628
+ Interface::Registration.new(
629
+ id: "workspace/didChangeWatchedFiles",
630
+ method: "workspace/didChangeWatchedFiles",
631
+ register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
632
+ watchers: [
633
+ Interface::FileSystemWatcher.new(
634
+ glob_pattern: "**/*.rb",
635
+ kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE,
636
+ ),
637
+ ],
638
+ ),
639
+ ),
640
+ ],
641
+ ),
642
+ )
643
+ end
644
+
645
+ begin_progress("indexing-progress", "Ruby LSP: indexing files")
646
+ end
647
+
541
648
  Interface::InitializeResult.new(
542
649
  capabilities: Interface::ServerCapabilities.new(
543
650
  text_document_sync: Interface::TextDocumentSyncOptions.new(
@@ -97,8 +97,32 @@ module RubyLsp
97
97
  sig { abstract.void }
98
98
  def activate; end
99
99
 
100
+ # Each extension should implement `MyExtension#deactivate` and use to perform any clean up, like shutting down a
101
+ # child process
102
+ sig { abstract.void }
103
+ def deactivate; end
104
+
100
105
  # Extensions should override the `name` method to return the extension name
101
106
  sig { abstract.returns(String) }
102
107
  def name; end
108
+
109
+ # Creates a new CodeLens listener. This method is invoked on every CodeLens request
110
+ sig do
111
+ overridable.params(
112
+ uri: URI::Generic,
113
+ emitter: EventEmitter,
114
+ message_queue: Thread::Queue,
115
+ ).returns(T.nilable(Listener[T::Array[Interface::CodeLens]]))
116
+ end
117
+ def create_code_lens_listener(uri, emitter, message_queue); end
118
+
119
+ # Creates a new Hover listener. This method is invoked on every Hover request
120
+ sig do
121
+ overridable.params(
122
+ emitter: EventEmitter,
123
+ message_queue: Thread::Queue,
124
+ ).returns(T.nilable(Listener[T.nilable(Interface::Hover)]))
125
+ end
126
+ def create_hover_listener(emitter, message_queue); end
103
127
  end
104
128
  end
@@ -3,12 +3,16 @@
3
3
 
4
4
  require "sorbet-runtime"
5
5
  require "syntax_tree"
6
+ require "yarp"
6
7
  require "language_server-protocol"
7
8
  require "benchmark"
8
9
  require "bundler"
9
10
  require "uri"
11
+ require "cgi"
10
12
 
11
13
  require "ruby-lsp"
14
+ require "ruby_indexer/ruby_indexer"
15
+ require "core_ext/uri"
12
16
  require "ruby_lsp/utils"
13
17
  require "ruby_lsp/server"
14
18
  require "ruby_lsp/executor"
@@ -18,25 +18,26 @@ module RubyLsp
18
18
  def initialize(emitter, message_queue)
19
19
  @emitter = emitter
20
20
  @message_queue = message_queue
21
- end
22
-
23
- class << self
24
- extend T::Sig
25
-
26
- sig { returns(T::Array[T.class_of(Listener)]) }
27
- def listeners
28
- @listeners ||= T.let([], T.nilable(T::Array[T.class_of(Listener)]))
29
- end
30
-
31
- sig { params(listener: T.class_of(Listener)).void }
32
- def add_listener(listener)
33
- listeners << listener
34
- end
21
+ @external_listeners = T.let([], T::Array[RubyLsp::Listener[ResponseType]])
35
22
  end
36
23
 
37
24
  # Override this method with an attr_reader that returns the response of your listener. The listener should
38
25
  # accumulate results in a @response variable and then provide the reader so that it is accessible
39
26
  sig { abstract.returns(ResponseType) }
40
27
  def response; end
28
+
29
+ # Merge responses from all external listeners into the base listener's response. We do this to return a single
30
+ # response to the editor including the results of all extensions
31
+ sig { void }
32
+ def merge_external_listeners_responses!
33
+ @external_listeners.each { |l| merge_response!(l) }
34
+ end
35
+
36
+ # Does nothing by default. Requests that accept extensions should override this method to define how to merge
37
+ # responses coming from external listeners
38
+ sig { overridable.params(other: Listener[T.untyped]).returns(T.self_type) }
39
+ def merge_response!(other)
40
+ self
41
+ end
41
42
  end
42
43
  end
@@ -29,7 +29,7 @@ module RubyLsp
29
29
  def initialize(document, range, context)
30
30
  super(document)
31
31
 
32
- @uri = T.let(document.uri, String)
32
+ @uri = T.let(document.uri, URI::Generic)
33
33
  @range = range
34
34
  @context = context
35
35
  end
@@ -63,14 +63,14 @@ module RubyLsp
63
63
  )
64
64
  end
65
65
 
66
- sig { params(range: Document::RangeShape, uri: String).returns(Interface::CodeAction) }
66
+ sig { params(range: Document::RangeShape, uri: URI::Generic).returns(Interface::CodeAction) }
67
67
  def refactor_code_action(range, uri)
68
68
  Interface::CodeAction.new(
69
69
  title: "Refactor: Extract Variable",
70
70
  kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
71
71
  data: {
72
72
  range: range,
73
- uri: uri,
73
+ uri: uri.to_s,
74
74
  },
75
75
  )
76
76
  end
@@ -31,15 +31,17 @@ module RubyLsp
31
31
  sig { override.returns(ResponseType) }
32
32
  attr_reader :response
33
33
 
34
- sig { params(uri: String, emitter: EventEmitter, message_queue: Thread::Queue, test_library: String).void }
34
+ sig { params(uri: URI::Generic, emitter: EventEmitter, message_queue: Thread::Queue, test_library: String).void }
35
35
  def initialize(uri, emitter, message_queue, test_library)
36
36
  super(emitter, message_queue)
37
37
 
38
- @uri = T.let(uri, String)
39
- @external_listeners = T.let([], T::Array[RubyLsp::Listener[ResponseType]])
38
+ @uri = T.let(uri, URI::Generic)
39
+ @external_listeners.concat(
40
+ Extension.extensions.filter_map { |ext| ext.create_code_lens_listener(uri, emitter, message_queue) },
41
+ )
40
42
  @test_library = T.let(test_library, String)
41
43
  @response = T.let([], ResponseType)
42
- @path = T.let(T.must(URI(uri).path), String)
44
+ @path = T.let(uri.to_standardized_path, T.nilable(String))
43
45
  # visibility_stack is a stack of [current_visibility, previous_visibility]
44
46
  @visibility_stack = T.let([["public", "public"]], T::Array[T::Array[T.nilable(String)]])
45
47
  @class_stack = T.let([], T::Array[String])
@@ -55,22 +57,6 @@ module RubyLsp
55
57
  :after_call,
56
58
  :on_vcall,
57
59
  )
58
-
59
- register_external_listeners!
60
- end
61
-
62
- sig { void }
63
- def register_external_listeners!
64
- self.class.listeners.each do |l|
65
- @external_listeners << T.unsafe(l).new(@uri, @emitter, @message_queue)
66
- end
67
- end
68
-
69
- sig { void }
70
- def merge_external_listeners_responses!
71
- @external_listeners.each do |l|
72
- merge_response!(l)
73
- end
74
60
  end
75
61
 
76
62
  sig { params(node: SyntaxTree::ClassDeclaration).void }
@@ -120,7 +106,7 @@ module RubyLsp
120
106
  if ACCESS_MODIFIERS.include?(node_message) && node.arguments.parts.any?
121
107
  visibility, _ = @visibility_stack.pop
122
108
  @visibility_stack.push([node_message, visibility])
123
- elsif @path.include?("Gemfile") && node_message.include?("gem") && node.arguments.parts.any?
109
+ elsif @path&.include?("Gemfile") && node_message.include?("gem") && node.arguments.parts.any?
124
110
  remote = resolve_gem_remote(node)
125
111
  return unless remote
126
112
 
@@ -163,7 +149,7 @@ module RubyLsp
163
149
  end
164
150
  end
165
151
 
166
- sig { params(other: Listener[ResponseType]).returns(T.self_type) }
152
+ sig { override.params(other: Listener[ResponseType]).returns(T.self_type) }
167
153
  def merge_response!(other)
168
154
  @response.concat(other.response)
169
155
  self
@@ -174,7 +160,7 @@ module RubyLsp
174
160
  sig { params(node: SyntaxTree::Node, name: String, command: String, kind: Symbol).void }
175
161
  def add_test_code_lens(node, name:, command:, kind:)
176
162
  # don't add code lenses if the test library is not supported or unknown
177
- return unless SUPPORTED_TEST_LIBRARIES.include?(@test_library)
163
+ return unless SUPPORTED_TEST_LIBRARIES.include?(@test_library) && @path
178
164
 
179
165
  arguments = [
180
166
  @path,
@@ -231,7 +217,7 @@ module RubyLsp
231
217
 
232
218
  sig { params(class_name: String, method_name: T.nilable(String)).returns(String) }
233
219
  def generate_test_command(class_name:, method_name: nil)
234
- command = BASE_COMMAND + @path
220
+ command = BASE_COMMAND + T.must(@path)
235
221
 
236
222
  case @test_library
237
223
  when "minitest"
@@ -20,18 +20,41 @@ module RubyLsp
20
20
  extend T::Sig
21
21
  extend T::Generic
22
22
 
23
- ResponseType = type_member { { fixed: T.nilable(Interface::Location) } }
23
+ ResponseType = type_member { { fixed: T.nilable(T.any(T::Array[Interface::Location], Interface::Location)) } }
24
24
 
25
25
  sig { override.returns(ResponseType) }
26
26
  attr_reader :response
27
27
 
28
- sig { params(uri: String, emitter: EventEmitter, message_queue: Thread::Queue).void }
29
- def initialize(uri, emitter, message_queue)
28
+ HAS_TYPECHECKER = T.let(DependencyDetector.typechecker?, T::Boolean)
29
+
30
+ sig do
31
+ params(
32
+ uri: URI::Generic,
33
+ nesting: T::Array[String],
34
+ index: RubyIndexer::Index,
35
+ emitter: EventEmitter,
36
+ message_queue: Thread::Queue,
37
+ ).void
38
+ end
39
+ def initialize(uri, nesting, index, emitter, message_queue)
30
40
  super(emitter, message_queue)
31
41
 
32
42
  @uri = uri
43
+ @nesting = nesting
44
+ @index = index
33
45
  @response = T.let(nil, ResponseType)
34
- emitter.register(self, :on_command)
46
+ emitter.register(self, :on_command, :on_const, :on_const_path_ref)
47
+ end
48
+
49
+ sig { params(node: SyntaxTree::ConstPathRef).void }
50
+ def on_const_path_ref(node)
51
+ name = full_constant_name(node)
52
+ find_in_index(name)
53
+ end
54
+
55
+ sig { params(node: SyntaxTree::Const).void }
56
+ def on_const(node)
57
+ find_in_index(node.value)
35
58
  end
36
59
 
37
60
  sig { params(node: SyntaxTree::Command).void }
@@ -53,7 +76,7 @@ module RubyLsp
53
76
 
54
77
  if candidate
55
78
  @response = Interface::Location.new(
56
- uri: "file://#{candidate}",
79
+ uri: URI::Generic.from_path(path: candidate).to_s,
57
80
  range: Interface::Range.new(
58
81
  start: Interface::Position.new(line: 0, character: 0),
59
82
  end: Interface::Position.new(line: 0, character: 0),
@@ -61,13 +84,13 @@ module RubyLsp
61
84
  )
62
85
  end
63
86
  when "require_relative"
64
- current_file = T.must(URI.parse(@uri).path)
65
- current_folder = Pathname.new(current_file).dirname
87
+ path = @uri.to_standardized_path
88
+ current_folder = path ? Pathname.new(CGI.unescape(path)).dirname : Dir.pwd
66
89
  candidate = File.expand_path(File.join(current_folder, required_file))
67
90
 
68
91
  if candidate
69
92
  @response = Interface::Location.new(
70
- uri: "file://#{candidate}",
93
+ uri: URI::Generic.from_path(path: candidate).to_s,
71
94
  range: Interface::Range.new(
72
95
  start: Interface::Position.new(line: 0, character: 0),
73
96
  end: Interface::Position.new(line: 0, character: 0),
@@ -79,6 +102,30 @@ module RubyLsp
79
102
 
80
103
  private
81
104
 
105
+ sig { params(value: String).void }
106
+ def find_in_index(value)
107
+ entries = @index.resolve(value, @nesting)
108
+ return unless entries
109
+
110
+ workspace_path = T.must(WORKSPACE_URI.to_standardized_path)
111
+
112
+ @response = entries.filter_map do |entry|
113
+ location = entry.location
114
+ # If the project has Sorbet, then we only want to handle go to definition for constants defined in gems, as an
115
+ # additional behavior on top of jumping to RBIs. Sorbet can already handle go to definition for all constants
116
+ # in the project, even if the files are typed false
117
+ next if HAS_TYPECHECKER && entry.file_path.start_with?(workspace_path)
118
+
119
+ Interface::Location.new(
120
+ uri: URI::Generic.from_path(path: entry.file_path).to_s,
121
+ range: Interface::Range.new(
122
+ start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
123
+ end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
124
+ ),
125
+ )
126
+ end
127
+ end
128
+
82
129
  sig { params(file: String).returns(T.nilable(String)) }
83
130
  def find_file_in_load_path(file)
84
131
  return unless file.include?("/")
@@ -25,7 +25,7 @@ module RubyLsp
25
25
  def initialize(document)
26
26
  super(document)
27
27
 
28
- @uri = T.let(document.uri, String)
28
+ @uri = T.let(document.uri, URI::Generic)
29
29
  end
30
30
 
31
31
  sig { override.returns(T.nilable(T.all(T::Array[Support::RuboCopDiagnostic], Object))) }
@@ -36,7 +36,8 @@ module RubyLsp
36
36
  return unless defined?(Support::RuboCopDiagnosticsRunner)
37
37
 
38
38
  # Don't try to run RuboCop diagnostics for files outside the current working directory
39
- return unless URI(@uri).path&.start_with?(T.must(WORKSPACE_URI.path))
39
+ path = @uri.to_standardized_path
40
+ return unless path.nil? || path.start_with?(T.must(WORKSPACE_URI.to_standardized_path))
40
41
 
41
42
  Support::RuboCopDiagnosticsRunner.instance.run(@uri, @document)
42
43
  end
@@ -75,13 +75,14 @@ module RubyLsp
75
75
  sig { override.returns(ResponseType) }
76
76
  attr_reader :response
77
77
 
78
- sig { params(uri: String, emitter: EventEmitter, message_queue: Thread::Queue).void }
78
+ sig { params(uri: URI::Generic, emitter: EventEmitter, message_queue: Thread::Queue).void }
79
79
  def initialize(uri, emitter, message_queue)
80
80
  super(emitter, message_queue)
81
81
 
82
82
  # Match the version based on the version in the RBI file name. Notice that the `@` symbol is sanitized to `%40`
83
83
  # in the URI
84
- version_match = /(?<=%40)[\d.]+(?=\.rbi$)/.match(uri)
84
+ path = uri.to_standardized_path
85
+ version_match = path ? /(?<=%40)[\d.]+(?=\.rbi$)/.match(path) : nil
85
86
  @gem_version = T.let(version_match && version_match[0], T.nilable(String))
86
87
  @response = T.let([], T::Array[Interface::DocumentLink])
87
88
 
@@ -95,7 +96,7 @@ module RubyLsp
95
96
 
96
97
  uri = T.cast(URI(T.must(match[0])), URI::Source)
97
98
  gem_version = T.must(resolve_version(uri))
98
- file_path = self.class.gem_paths.dig(uri.gem_name, gem_version, uri.path)
99
+ file_path = self.class.gem_paths.dig(uri.gem_name, gem_version, CGI.unescape(uri.path))
99
100
  return if file_path.nil?
100
101
 
101
102
  @response << Interface::DocumentLink.new(