ruby-lsp 0.7.6 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
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(