ruby-lsp 0.12.2 → 0.13.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp-check +20 -4
  5. data/exe/ruby-lsp-doctor +2 -2
  6. data/lib/ruby_indexer/lib/ruby_indexer/{visitor.rb → collector.rb} +144 -61
  7. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +9 -4
  8. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +89 -12
  9. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +22 -4
  10. data/lib/ruby_indexer/ruby_indexer.rb +1 -1
  11. data/lib/ruby_indexer/test/configuration_test.rb +10 -0
  12. data/lib/ruby_indexer/test/index_test.rb +64 -0
  13. data/lib/ruby_indexer/test/method_test.rb +80 -0
  14. data/lib/ruby_lsp/addon.rb +9 -13
  15. data/lib/ruby_lsp/document.rb +7 -9
  16. data/lib/ruby_lsp/executor.rb +54 -51
  17. data/lib/ruby_lsp/internal.rb +4 -0
  18. data/lib/ruby_lsp/listener.rb +4 -5
  19. data/lib/ruby_lsp/requests/code_action_resolve.rb +8 -4
  20. data/lib/ruby_lsp/requests/code_lens.rb +16 -7
  21. data/lib/ruby_lsp/requests/completion.rb +60 -8
  22. data/lib/ruby_lsp/requests/definition.rb +55 -29
  23. data/lib/ruby_lsp/requests/diagnostics.rb +0 -5
  24. data/lib/ruby_lsp/requests/document_highlight.rb +20 -11
  25. data/lib/ruby_lsp/requests/document_link.rb +2 -3
  26. data/lib/ruby_lsp/requests/document_symbol.rb +3 -3
  27. data/lib/ruby_lsp/requests/folding_ranges.rb +12 -15
  28. data/lib/ruby_lsp/requests/formatting.rb +0 -5
  29. data/lib/ruby_lsp/requests/hover.rb +23 -4
  30. data/lib/ruby_lsp/requests/inlay_hints.rb +42 -4
  31. data/lib/ruby_lsp/requests/on_type_formatting.rb +18 -4
  32. data/lib/ruby_lsp/requests/semantic_highlighting.rb +41 -16
  33. data/lib/ruby_lsp/requests/support/common.rb +22 -2
  34. data/lib/ruby_lsp/requests/support/dependency_detector.rb +0 -1
  35. data/lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb +3 -8
  36. data/lib/ruby_lsp/requests/workspace_symbol.rb +6 -11
  37. data/lib/ruby_lsp/ruby_document.rb +14 -0
  38. data/lib/ruby_lsp/setup_bundler.rb +2 -0
  39. data/lib/ruby_lsp/store.rb +5 -3
  40. data/lib/ruby_lsp/utils.rb +8 -3
  41. metadata +8 -7
@@ -193,5 +193,69 @@ module RubyIndexer
193
193
 
194
194
  assert_instance_of(Entry::UnresolvedAlias, entry)
195
195
  end
196
+
197
+ def test_visitor_does_not_visit_unnecessary_nodes
198
+ concats = (0...10_000).map do |i|
199
+ <<~STRING
200
+ "string#{i}" \\
201
+ STRING
202
+ end.join
203
+
204
+ index(<<~RUBY)
205
+ module Foo
206
+ local_var = #{concats}
207
+ "final"
208
+ @class_instance_var = #{concats}
209
+ "final"
210
+ @@class_var = #{concats}
211
+ "final"
212
+ $global_var = #{concats}
213
+ "final"
214
+ CONST = #{concats}
215
+ "final"
216
+ end
217
+ RUBY
218
+ end
219
+
220
+ def test_resolve_method_with_known_receiver
221
+ index(<<~RUBY)
222
+ module Foo
223
+ module Bar
224
+ def baz; end
225
+ end
226
+ end
227
+ RUBY
228
+
229
+ entry = T.must(@index.resolve_method("baz", "Foo::Bar"))
230
+ assert_equal("baz", entry.name)
231
+ assert_equal("Foo::Bar", T.must(entry.owner).name)
232
+ end
233
+
234
+ def test_prefix_search_for_methods
235
+ index(<<~RUBY)
236
+ module Foo
237
+ module Bar
238
+ def baz; end
239
+ end
240
+ end
241
+ RUBY
242
+
243
+ entries = @index.prefix_search("ba")
244
+ refute_empty(entries)
245
+
246
+ entry = T.must(entries.first).first
247
+ assert_equal("baz", entry.name)
248
+ end
249
+
250
+ def test_indexing_prism_fixtures_succeeds
251
+ fixtures = Dir.glob("test/fixtures/prism/test/prism/fixtures/**/*.txt")
252
+
253
+ fixtures.each do |fixture|
254
+ indexable_path = IndexablePath.new("", fixture)
255
+ @index.index_single(indexable_path)
256
+ end
257
+
258
+ refute_empty(@index.instance_variable_get(:@entries))
259
+ end
196
260
  end
197
261
  end
@@ -69,5 +69,85 @@ module RubyIndexer
69
69
  assert_equal(:"(a, (b, ))", parameter.name)
70
70
  assert_instance_of(Entry::RequiredParameter, parameter)
71
71
  end
72
+
73
+ def test_method_with_optional_parameters
74
+ index(<<~RUBY)
75
+ class Foo
76
+ def bar(a = 123)
77
+ end
78
+ end
79
+ RUBY
80
+
81
+ assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
82
+ entry = T.must(@index["bar"].first)
83
+ assert_equal(1, entry.parameters.length)
84
+ parameter = entry.parameters.first
85
+ assert_equal(:a, parameter.name)
86
+ assert_instance_of(Entry::OptionalParameter, parameter)
87
+ end
88
+
89
+ def test_method_with_keyword_parameters
90
+ index(<<~RUBY)
91
+ class Foo
92
+ def bar(a:, b: 123)
93
+ end
94
+ end
95
+ RUBY
96
+
97
+ assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5")
98
+ entry = T.must(@index["bar"].first)
99
+ assert_equal(2, entry.parameters.length)
100
+ a, b = entry.parameters
101
+
102
+ assert_equal(:a, a.name)
103
+ assert_instance_of(Entry::KeywordParameter, a)
104
+
105
+ assert_equal(:b, b.name)
106
+ assert_instance_of(Entry::OptionalKeywordParameter, b)
107
+ end
108
+
109
+ def test_keeps_track_of_method_owner
110
+ index(<<~RUBY)
111
+ class Foo
112
+ def bar
113
+ end
114
+ end
115
+ RUBY
116
+
117
+ entry = T.must(@index["bar"].first)
118
+ owner_name = T.must(entry.owner).name
119
+
120
+ assert_equal("Foo", owner_name)
121
+ end
122
+
123
+ def test_keeps_track_of_attributes
124
+ index(<<~RUBY)
125
+ class Foo
126
+ # Hello there
127
+ attr_reader :bar, :other
128
+ attr_writer :baz
129
+ attr_accessor :qux
130
+ end
131
+ RUBY
132
+
133
+ assert_entry("bar", Entry::Accessor, "/fake/path/foo.rb:2-15:2-18")
134
+ assert_equal("Hello there", @index["bar"].first.comments.join("\n"))
135
+ assert_entry("other", Entry::Accessor, "/fake/path/foo.rb:2-21:2-26")
136
+ assert_equal("Hello there", @index["other"].first.comments.join("\n"))
137
+ assert_entry("baz=", Entry::Accessor, "/fake/path/foo.rb:3-15:3-18")
138
+ assert_entry("qux", Entry::Accessor, "/fake/path/foo.rb:4-17:4-20")
139
+ assert_entry("qux=", Entry::Accessor, "/fake/path/foo.rb:4-17:4-20")
140
+ end
141
+
142
+ def test_ignores_attributes_invoked_on_constant
143
+ index(<<~RUBY)
144
+ class Foo
145
+ end
146
+
147
+ Foo.attr_reader :bar
148
+ RUBY
149
+
150
+ assert_no_entry("bar")
151
+ end
72
152
  end
73
153
  end
@@ -41,8 +41,8 @@ module RubyLsp
41
41
  end
42
42
 
43
43
  # Discovers and loads all addons. Returns the list of activated addons
44
- sig { returns(T::Array[Addon]) }
45
- def load_addons
44
+ sig { params(message_queue: Thread::Queue).returns(T::Array[Addon]) }
45
+ def load_addons(message_queue)
46
46
  # Require all addons entry points, which should be placed under
47
47
  # `some_gem/lib/ruby_lsp/your_gem_name/addon.rb`
48
48
  Gem.find_files("ruby_lsp/**/addon.rb").each do |addon|
@@ -55,7 +55,7 @@ module RubyLsp
55
55
  # Activate each one of the discovered addons. If any problems occur in the addons, we don't want to
56
56
  # fail to boot the server
57
57
  addons.each do |addon|
58
- addon.activate
58
+ addon.activate(message_queue)
59
59
  nil
60
60
  rescue => e
61
61
  addon.add_error(e)
@@ -94,8 +94,8 @@ module RubyLsp
94
94
 
95
95
  # Each addon should implement `MyAddon#activate` and use to perform any sort of initialization, such as
96
96
  # reading information into memory or even spawning a separate process
97
- sig { abstract.void }
98
- def activate; end
97
+ sig { abstract.params(message_queue: Thread::Queue).void }
98
+ def activate(message_queue); end
99
99
 
100
100
  # Each addon should implement `MyAddon#deactivate` and use to perform any clean up, like shutting down a
101
101
  # child process
@@ -111,10 +111,9 @@ module RubyLsp
111
111
  overridable.params(
112
112
  uri: URI::Generic,
113
113
  dispatcher: Prism::Dispatcher,
114
- message_queue: Thread::Queue,
115
114
  ).returns(T.nilable(Listener[T::Array[Interface::CodeLens]]))
116
115
  end
117
- def create_code_lens_listener(uri, dispatcher, message_queue); end
116
+ def create_code_lens_listener(uri, dispatcher); end
118
117
 
119
118
  # Creates a new Hover listener. This method is invoked on every Hover request
120
119
  sig do
@@ -122,19 +121,17 @@ module RubyLsp
122
121
  nesting: T::Array[String],
123
122
  index: RubyIndexer::Index,
124
123
  dispatcher: Prism::Dispatcher,
125
- message_queue: Thread::Queue,
126
124
  ).returns(T.nilable(Listener[T.nilable(Interface::Hover)]))
127
125
  end
128
- def create_hover_listener(nesting, index, dispatcher, message_queue); end
126
+ def create_hover_listener(nesting, index, dispatcher); end
129
127
 
130
128
  # Creates a new DocumentSymbol listener. This method is invoked on every DocumentSymbol request
131
129
  sig do
132
130
  overridable.params(
133
131
  dispatcher: Prism::Dispatcher,
134
- message_queue: Thread::Queue,
135
132
  ).returns(T.nilable(Listener[T::Array[Interface::DocumentSymbol]]))
136
133
  end
137
- def create_document_symbol_listener(dispatcher, message_queue); end
134
+ def create_document_symbol_listener(dispatcher); end
138
135
 
139
136
  # Creates a new Definition listener. This method is invoked on every Definition request
140
137
  sig do
@@ -143,9 +140,8 @@ module RubyLsp
143
140
  nesting: T::Array[String],
144
141
  index: RubyIndexer::Index,
145
142
  dispatcher: Prism::Dispatcher,
146
- message_queue: Thread::Queue,
147
143
  ).returns(T.nilable(Listener[T.nilable(T.any(T::Array[Interface::Location], Interface::Location))]))
148
144
  end
149
- def create_definition_listener(uri, nesting, index, dispatcher, message_queue); end
145
+ def create_definition_listener(uri, nesting, index, dispatcher); end
150
146
  end
151
147
  end
@@ -4,6 +4,9 @@
4
4
  module RubyLsp
5
5
  class Document
6
6
  extend T::Sig
7
+ extend T::Helpers
8
+
9
+ abstract!
7
10
 
8
11
  PositionShape = T.type_alias { { line: Integer, character: Integer } }
9
12
  RangeShape = T.type_alias { { start: PositionShape, end: PositionShape } }
@@ -28,8 +31,8 @@ module RubyLsp
28
31
  @source = T.let(source, String)
29
32
  @version = T.let(version, Integer)
30
33
  @uri = T.let(uri, URI::Generic)
31
- @needs_parsing = T.let(false, T::Boolean)
32
- @parse_result = T.let(Prism.parse(@source), Prism::ParseResult)
34
+ @needs_parsing = T.let(true, T::Boolean)
35
+ @parse_result = T.let(parse, Prism::ParseResult)
33
36
  end
34
37
 
35
38
  sig { returns(Prism::ProgramNode) }
@@ -91,13 +94,8 @@ module RubyLsp
91
94
  @cache.clear
92
95
  end
93
96
 
94
- sig { void }
95
- def parse
96
- return unless @needs_parsing
97
-
98
- @needs_parsing = false
99
- @parse_result = Prism.parse(@source)
100
- end
97
+ sig { abstract.returns(Prism::ParseResult) }
98
+ def parse; end
101
99
 
102
100
  sig { returns(T::Boolean) }
103
101
  def syntax_error?
@@ -41,7 +41,7 @@ module RubyLsp
41
41
  when "initialize"
42
42
  initialize_request(request.dig(:params))
43
43
  when "initialized"
44
- Addon.load_addons
44
+ Addon.load_addons(@message_queue)
45
45
 
46
46
  errored_addons = Addon.addons.select(&:error?)
47
47
 
@@ -57,6 +57,8 @@ module RubyLsp
57
57
  warn(errored_addons.map(&:backtraces).join("\n\n"))
58
58
  end
59
59
 
60
+ RubyVM::YJIT.enable if defined? RubyVM::YJIT.enable
61
+
60
62
  perform_initial_indexing
61
63
  check_formatter_is_available
62
64
 
@@ -93,12 +95,12 @@ module RubyLsp
93
95
 
94
96
  # Run listeners for the document
95
97
  dispatcher = Prism::Dispatcher.new
96
- folding_range = Requests::FoldingRanges.new(document.parse_result.comments, dispatcher, @message_queue)
97
- document_symbol = Requests::DocumentSymbol.new(dispatcher, @message_queue)
98
- document_link = Requests::DocumentLink.new(uri, document.comments, dispatcher, @message_queue)
99
- code_lens = Requests::CodeLens.new(uri, dispatcher, @message_queue)
98
+ folding_range = Requests::FoldingRanges.new(document.parse_result.comments, dispatcher)
99
+ document_symbol = Requests::DocumentSymbol.new(dispatcher)
100
+ document_link = Requests::DocumentLink.new(uri, document.comments, dispatcher)
101
+ code_lens = Requests::CodeLens.new(uri, dispatcher)
100
102
 
101
- semantic_highlighting = Requests::SemanticHighlighting.new(dispatcher, @message_queue)
103
+ semantic_highlighting = Requests::SemanticHighlighting.new(dispatcher)
102
104
  dispatcher.dispatch(document.tree)
103
105
 
104
106
  # Store all responses retrieve in this round of visits in the cache and then return the response for the request
@@ -263,13 +265,7 @@ module RubyLsp
263
265
  target = parent if target.is_a?(Prism::ConstantReadNode) && parent.is_a?(Prism::ConstantPathNode)
264
266
 
265
267
  dispatcher = Prism::Dispatcher.new
266
- base_listener = Requests::Definition.new(
267
- uri,
268
- nesting,
269
- @index,
270
- dispatcher,
271
- @message_queue,
272
- )
268
+ base_listener = Requests::Definition.new(uri, nesting, @index, dispatcher)
273
269
  dispatcher.dispatch_once(target)
274
270
  base_listener.response
275
271
  end
@@ -295,7 +291,7 @@ module RubyLsp
295
291
 
296
292
  # Instantiate all listeners
297
293
  dispatcher = Prism::Dispatcher.new
298
- hover = Requests::Hover.new(@index, nesting, dispatcher, @message_queue)
294
+ hover = Requests::Hover.new(@index, nesting, dispatcher)
299
295
 
300
296
  # Emit events for all listeners
301
297
  dispatcher.dispatch_once(target)
@@ -353,6 +349,11 @@ module RubyLsp
353
349
  # If formatter is set to `auto` but no supported formatting gem is found, don't attempt to format
354
350
  return if @store.formatter == "none"
355
351
 
352
+ # Do not format files outside of the workspace. For example, if someone is looking at a gem's source code, we
353
+ # don't want to format it
354
+ path = uri.to_standardized_path
355
+ return unless path.nil? || path.start_with?(T.must(@store.workspace_uri.to_standardized_path))
356
+
356
357
  Requests::Formatting.new(@store.get(uri), formatter: @store.formatter).run
357
358
  end
358
359
 
@@ -378,7 +379,7 @@ module RubyLsp
378
379
 
379
380
  target, parent = document.locate_node(position)
380
381
  dispatcher = Prism::Dispatcher.new
381
- listener = Requests::DocumentHighlight.new(target, parent, dispatcher, @message_queue)
382
+ listener = Requests::DocumentHighlight.new(target, parent, dispatcher)
382
383
  dispatcher.visit(document.tree)
383
384
  listener.response
384
385
  end
@@ -391,7 +392,7 @@ module RubyLsp
391
392
  end_line = range.dig(:end, :line)
392
393
 
393
394
  dispatcher = Prism::Dispatcher.new
394
- listener = Requests::InlayHints.new(start_line..end_line, dispatcher, @message_queue)
395
+ listener = Requests::InlayHints.new(start_line..end_line, dispatcher)
395
396
  dispatcher.visit(document.tree)
396
397
  listener.response
397
398
  end
@@ -441,6 +442,11 @@ module RubyLsp
441
442
 
442
443
  sig { params(uri: URI::Generic).returns(T.nilable(Interface::FullDocumentDiagnosticReport)) }
443
444
  def diagnostic(uri)
445
+ # Do not compute diagnostics for files outside of the workspace. For example, if someone is looking at a gem's
446
+ # source code, we don't want to show diagnostics for it
447
+ path = uri.to_standardized_path
448
+ return unless path.nil? || path.start_with?(T.must(@store.workspace_uri.to_standardized_path))
449
+
444
450
  response = @store.cache_fetch(uri, "textDocument/diagnostic") do |document|
445
451
  Requests::Diagnostics.new(document).run
446
452
  end
@@ -455,11 +461,7 @@ module RubyLsp
455
461
  end_line = range.dig(:end, :line)
456
462
 
457
463
  dispatcher = Prism::Dispatcher.new
458
- listener = Requests::SemanticHighlighting.new(
459
- dispatcher,
460
- @message_queue,
461
- range: start_line..end_line,
462
- )
464
+ listener = Requests::SemanticHighlighting.new(dispatcher, range: start_line..end_line)
463
465
  dispatcher.visit(document.tree)
464
466
 
465
467
  Requests::Support::SemanticTokenEncoder.new.encode(listener.response)
@@ -474,34 +476,32 @@ module RubyLsp
474
476
  def completion(uri, position)
475
477
  document = @store.get(uri)
476
478
 
477
- char_position = document.create_scanner.find_char_position(position)
478
-
479
- # When the user types in the first letter of a constant name, we actually receive the position of the next
480
- # immediate character. We check to see if the character is uppercase and then remove the offset to try to locate
481
- # the node, as it could not be a constant
482
- target_node_types = if ("A".."Z").cover?(document.source[char_position - 1])
483
- char_position -= 1
484
- [Prism::ConstantReadNode, Prism::ConstantPathNode]
485
- else
486
- [Prism::CallNode]
487
- end
488
-
489
- matched, parent, nesting = document.locate(document.tree, char_position, node_types: target_node_types)
479
+ # Completion always receives the position immediately after the character that was just typed. Here we adjust it
480
+ # back by 1, so that we find the right node
481
+ char_position = document.create_scanner.find_char_position(position) - 1
482
+ matched, parent, nesting = document.locate(
483
+ document.tree,
484
+ char_position,
485
+ node_types: [Prism::CallNode, Prism::ConstantReadNode, Prism::ConstantPathNode],
486
+ )
490
487
  return unless matched && parent
491
488
 
492
489
  target = case matched
493
490
  when Prism::CallNode
494
491
  message = matched.message
495
- return unless message == "require"
496
492
 
497
- args = matched.arguments&.arguments
498
- return if args.nil? || args.is_a?(Prism::ForwardingArgumentsNode)
493
+ if message == "require"
494
+ args = matched.arguments&.arguments
495
+ return if args.nil? || args.is_a?(Prism::ForwardingArgumentsNode)
499
496
 
500
- argument = args.first
501
- return unless argument.is_a?(Prism::StringNode)
502
- return unless (argument.location.start_offset..argument.location.end_offset).cover?(char_position)
497
+ argument = args.first
498
+ return unless argument.is_a?(Prism::StringNode)
499
+ return unless (argument.location.start_offset..argument.location.end_offset).cover?(char_position)
503
500
 
504
- argument
501
+ argument
502
+ else
503
+ matched
504
+ end
505
505
  when Prism::ConstantReadNode, Prism::ConstantPathNode
506
506
  if parent.is_a?(Prism::ConstantPathNode) && matched.is_a?(Prism::ConstantReadNode)
507
507
  parent
@@ -513,12 +513,7 @@ module RubyLsp
513
513
  return unless target
514
514
 
515
515
  dispatcher = Prism::Dispatcher.new
516
- listener = Requests::Completion.new(
517
- @index,
518
- nesting,
519
- dispatcher,
520
- @message_queue,
521
- )
516
+ listener = Requests::Completion.new(@index, nesting, dispatcher)
522
517
  dispatcher.dispatch_once(target)
523
518
  listener.response
524
519
  end
@@ -579,10 +574,13 @@ module RubyLsp
579
574
  # notification
580
575
  end
581
576
 
582
- sig { params(options: T::Hash[Symbol, T.untyped]).returns(Interface::InitializeResult) }
577
+ sig { params(options: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
583
578
  def initialize_request(options)
584
579
  @store.clear
585
580
 
581
+ workspace_uri = options.dig(:workspaceFolders, 0, :uri)
582
+ @store.workspace_uri = URI(workspace_uri) if workspace_uri
583
+
586
584
  encodings = options.dig(:capabilities, :general, :positionEncodings)
587
585
  @store.encoding = if encodings.nil? || encodings.empty?
588
586
  Constant::PositionEncodingKind::UTF16
@@ -680,7 +678,7 @@ module RubyLsp
680
678
  completion_provider = if enabled_features["completion"]
681
679
  Interface::CompletionOptions.new(
682
680
  resolve_provider: false,
683
- trigger_characters: ["/", *"A".."Z"],
681
+ trigger_characters: ["/"],
684
682
  completion_item: {
685
683
  labelDetailsSupport: true,
686
684
  },
@@ -716,7 +714,7 @@ module RubyLsp
716
714
 
717
715
  begin_progress("indexing-progress", "Ruby LSP: indexing files")
718
716
 
719
- Interface::InitializeResult.new(
717
+ {
720
718
  capabilities: Interface::ServerCapabilities.new(
721
719
  text_document_sync: Interface::TextDocumentSyncOptions.new(
722
720
  change: Constant::TextDocumentSyncKind::INCREMENTAL,
@@ -740,7 +738,12 @@ module RubyLsp
740
738
  definition_provider: enabled_features["definition"],
741
739
  workspace_symbol_provider: enabled_features["workspaceSymbol"],
742
740
  ),
743
- )
741
+ serverInfo: {
742
+ name: "Ruby LSP",
743
+ version: VERSION,
744
+ },
745
+ formatter: @store.formatter,
746
+ }
744
747
  end
745
748
 
746
749
  sig { void }
@@ -24,6 +24,10 @@ require "ruby_lsp/server"
24
24
  require "ruby_lsp/executor"
25
25
  require "ruby_lsp/requests"
26
26
  require "ruby_lsp/listener"
27
+ require "ruby_lsp/document"
28
+ require "ruby_lsp/ruby_document"
27
29
  require "ruby_lsp/store"
28
30
  require "ruby_lsp/addon"
29
31
  require "ruby_lsp/requests/support/rubocop_runner"
32
+
33
+ Bundler.ui.level = :silent
@@ -14,10 +14,9 @@ module RubyLsp
14
14
 
15
15
  abstract!
16
16
 
17
- sig { params(dispatcher: Prism::Dispatcher, message_queue: Thread::Queue).void }
18
- def initialize(dispatcher, message_queue)
17
+ sig { params(dispatcher: Prism::Dispatcher).void }
18
+ def initialize(dispatcher)
19
19
  @dispatcher = dispatcher
20
- @message_queue = message_queue
21
20
  end
22
21
 
23
22
  sig { returns(ResponseType) }
@@ -43,8 +42,8 @@ module RubyLsp
43
42
  # When inheriting from ExtensibleListener, the `super` of constructor must be called **after** the subclass's own
44
43
  # ivars have been initialized. This is because the constructor of ExtensibleListener calls
45
44
  # `initialize_external_listener` which may depend on the subclass's ivars.
46
- sig { params(dispatcher: Prism::Dispatcher, message_queue: Thread::Queue).void }
47
- def initialize(dispatcher, message_queue)
45
+ sig { params(dispatcher: Prism::Dispatcher).void }
46
+ def initialize(dispatcher)
48
47
  super
49
48
  @response_merged = T.let(false, T::Boolean)
50
49
  @external_listeners = T.let(
@@ -87,15 +87,19 @@ module RubyLsp
87
87
  :start,
88
88
  :line,
89
89
  ) && closest_node_loc.end_line - 1 >= source_range.dig(:end, :line)
90
- indentation_line = closest_node_loc.start_line - 1
91
- target_line = indentation_line
90
+ indentation_line_number = closest_node_loc.start_line - 1
91
+ target_line = indentation_line_number
92
92
  else
93
93
  target_line = closest_node_loc.end_line
94
- indentation_line = closest_node_loc.end_line - 1
94
+ indentation_line_number = closest_node_loc.end_line - 1
95
95
  end
96
96
 
97
97
  lines = @document.source.lines
98
- indentation = T.must(T.must(lines[indentation_line])[/\A */]).size
98
+
99
+ indentation_line = lines[indentation_line_number]
100
+ return Error::InvalidTargetRange unless indentation_line
101
+
102
+ indentation = T.must(indentation_line[/\A */]).size
99
103
 
100
104
  target_range = {
101
105
  start: { line: target_line, character: indentation },
@@ -47,16 +47,18 @@ module RubyLsp
47
47
  sig { override.returns(ResponseType) }
48
48
  attr_reader :_response
49
49
 
50
- sig { params(uri: URI::Generic, dispatcher: Prism::Dispatcher, message_queue: Thread::Queue).void }
51
- def initialize(uri, dispatcher, message_queue)
50
+ sig { params(uri: URI::Generic, dispatcher: Prism::Dispatcher).void }
51
+ def initialize(uri, dispatcher)
52
52
  @uri = T.let(uri, URI::Generic)
53
53
  @_response = T.let([], ResponseType)
54
54
  @path = T.let(uri.to_standardized_path, T.nilable(String))
55
55
  # visibility_stack is a stack of [current_visibility, previous_visibility]
56
56
  @visibility_stack = T.let([[:public, :public]], T::Array[T::Array[T.nilable(Symbol)]])
57
57
  @class_stack = T.let([], T::Array[String])
58
+ @group_id = T.let(1, Integer)
59
+ @group_id_stack = T.let([], T::Array[Integer])
58
60
 
59
- super(dispatcher, message_queue)
61
+ super(dispatcher)
60
62
 
61
63
  dispatcher.register(
62
64
  self,
@@ -82,12 +84,16 @@ module RubyLsp
82
84
  kind: :group,
83
85
  )
84
86
  end
87
+
88
+ @group_id_stack.push(@group_id)
89
+ @group_id += 1
85
90
  end
86
91
 
87
92
  sig { params(node: Prism::ClassNode).void }
88
93
  def on_class_node_leave(node)
89
94
  @visibility_stack.pop
90
95
  @class_stack.pop
96
+ @group_id_stack.pop
91
97
  end
92
98
 
93
99
  sig { params(node: Prism::DefNode).void }
@@ -146,7 +152,7 @@ module RubyLsp
146
152
 
147
153
  sig { override.params(addon: Addon).returns(T.nilable(Listener[ResponseType])) }
148
154
  def initialize_external_listener(addon)
149
- addon.create_code_lens_listener(@uri, @dispatcher, @message_queue)
155
+ addon.create_code_lens_listener(@uri, @dispatcher)
150
156
  end
151
157
 
152
158
  sig { override.params(other: Listener[ResponseType]).returns(T.self_type) }
@@ -174,12 +180,15 @@ module RubyLsp
174
180
  },
175
181
  ]
176
182
 
183
+ grouping_data = { group_id: @group_id_stack.last, kind: kind }
184
+ grouping_data[:id] = @group_id if kind == :group
185
+
177
186
  @_response << create_code_lens(
178
187
  node,
179
188
  title: "Run",
180
189
  command_name: "rubyLsp.runTest",
181
190
  arguments: arguments,
182
- data: { type: "test", kind: kind },
191
+ data: { type: "test", **grouping_data },
183
192
  )
184
193
 
185
194
  @_response << create_code_lens(
@@ -187,7 +196,7 @@ module RubyLsp
187
196
  title: "Run In Terminal",
188
197
  command_name: "rubyLsp.runTestInTerminal",
189
198
  arguments: arguments,
190
- data: { type: "test_in_terminal", kind: kind },
199
+ data: { type: "test_in_terminal", **grouping_data },
191
200
  )
192
201
 
193
202
  @_response << create_code_lens(
@@ -195,7 +204,7 @@ module RubyLsp
195
204
  title: "Debug",
196
205
  command_name: "rubyLsp.debugTest",
197
206
  arguments: arguments,
198
- data: { type: "debug", kind: kind },
207
+ data: { type: "debug", **grouping_data },
199
208
  )
200
209
  end
201
210