ruby-lsp 0.12.2 → 0.13.0

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