ruby-lsp 0.17.17 → 0.18.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -110
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +5 -4
  5. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +14 -6
  6. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +157 -27
  7. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +5 -4
  8. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +2 -2
  9. data/lib/ruby_indexer/test/classes_and_modules_test.rb +10 -10
  10. data/lib/ruby_indexer/test/constant_test.rb +4 -4
  11. data/lib/ruby_indexer/test/enhancements_test.rb +2 -2
  12. data/lib/ruby_indexer/test/method_test.rb +257 -2
  13. data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
  14. data/lib/ruby_lsp/base_server.rb +21 -1
  15. data/lib/ruby_lsp/document.rb +5 -3
  16. data/lib/ruby_lsp/erb_document.rb +29 -10
  17. data/lib/ruby_lsp/global_state.rb +3 -1
  18. data/lib/ruby_lsp/listeners/code_lens.rb +34 -5
  19. data/lib/ruby_lsp/listeners/signature_help.rb +55 -24
  20. data/lib/ruby_lsp/rbs_document.rb +5 -4
  21. data/lib/ruby_lsp/requests/code_action_resolve.rb +0 -15
  22. data/lib/ruby_lsp/requests/code_actions.rb +0 -10
  23. data/lib/ruby_lsp/requests/code_lens.rb +1 -11
  24. data/lib/ruby_lsp/requests/completion.rb +3 -20
  25. data/lib/ruby_lsp/requests/completion_resolve.rb +0 -8
  26. data/lib/ruby_lsp/requests/definition.rb +6 -20
  27. data/lib/ruby_lsp/requests/diagnostics.rb +0 -10
  28. data/lib/ruby_lsp/requests/document_highlight.rb +7 -14
  29. data/lib/ruby_lsp/requests/document_link.rb +0 -10
  30. data/lib/ruby_lsp/requests/document_symbol.rb +0 -17
  31. data/lib/ruby_lsp/requests/folding_ranges.rb +0 -10
  32. data/lib/ruby_lsp/requests/formatting.rb +0 -16
  33. data/lib/ruby_lsp/requests/hover.rb +9 -9
  34. data/lib/ruby_lsp/requests/inlay_hints.rb +0 -30
  35. data/lib/ruby_lsp/requests/on_type_formatting.rb +0 -10
  36. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +0 -11
  37. data/lib/ruby_lsp/requests/request.rb +17 -1
  38. data/lib/ruby_lsp/requests/selection_ranges.rb +0 -10
  39. data/lib/ruby_lsp/requests/semantic_highlighting.rb +1 -23
  40. data/lib/ruby_lsp/requests/show_syntax_tree.rb +0 -11
  41. data/lib/ruby_lsp/requests/signature_help.rb +5 -20
  42. data/lib/ruby_lsp/requests/support/common.rb +1 -1
  43. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +2 -0
  44. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +0 -11
  45. data/lib/ruby_lsp/requests/workspace_symbol.rb +0 -12
  46. data/lib/ruby_lsp/ruby_document.rb +4 -3
  47. data/lib/ruby_lsp/server.rb +23 -8
  48. data/lib/ruby_lsp/setup_bundler.rb +31 -13
  49. data/lib/ruby_lsp/type_inferrer.rb +6 -2
  50. data/lib/ruby_lsp/utils.rb +11 -1
  51. metadata +3 -4
  52. data/lib/ruby_lsp/check_docs.rb +0 -130
@@ -38,7 +38,7 @@ module RubyIndexer
38
38
  file_path,
39
39
  location,
40
40
  location,
41
- [],
41
+ nil,
42
42
  [Entry::Signature.new([Entry::RequiredParameter.new(name: :a)])],
43
43
  Entry::Visibility::PUBLIC,
44
44
  owner,
@@ -120,7 +120,7 @@ module RubyIndexer
120
120
  file_path,
121
121
  location,
122
122
  location,
123
- [],
123
+ nil,
124
124
  [],
125
125
  Entry::Visibility::PUBLIC,
126
126
  owner,
@@ -330,6 +330,33 @@ module RubyIndexer
330
330
  assert_empty(parameters)
331
331
  end
332
332
 
333
+ def test_methods_with_argument_forwarding
334
+ index(<<~RUBY)
335
+ class Foo
336
+ def bar(...)
337
+ end
338
+
339
+ def baz(a, ...)
340
+ end
341
+ end
342
+ RUBY
343
+
344
+ entry = T.must(@index["bar"].first)
345
+ assert_instance_of(Entry::Method, entry, "Expected `bar` to be indexed")
346
+
347
+ parameters = entry.signatures.first.parameters
348
+ assert_equal(1, parameters.length)
349
+ assert_instance_of(Entry::ForwardingParameter, parameters.first)
350
+
351
+ entry = T.must(@index["baz"].first)
352
+ assert_instance_of(Entry::Method, entry, "Expected `baz` to be indexed")
353
+
354
+ parameters = entry.signatures.first.parameters
355
+ assert_equal(2, parameters.length)
356
+ assert_instance_of(Entry::RequiredParameter, parameters[0])
357
+ assert_instance_of(Entry::ForwardingParameter, parameters[1])
358
+ end
359
+
333
360
  def test_keeps_track_of_method_owner
334
361
  index(<<~RUBY)
335
362
  class Foo
@@ -355,9 +382,9 @@ module RubyIndexer
355
382
  RUBY
356
383
 
357
384
  assert_entry("bar", Entry::Accessor, "/fake/path/foo.rb:2-15:2-18")
358
- assert_equal("Hello there", @index["bar"].first.comments.join("\n"))
385
+ assert_equal("Hello there", @index["bar"].first.comments)
359
386
  assert_entry("other", Entry::Accessor, "/fake/path/foo.rb:2-21:2-26")
360
- assert_equal("Hello there", @index["other"].first.comments.join("\n"))
387
+ assert_equal("Hello there", @index["other"].first.comments)
361
388
  assert_entry("baz=", Entry::Accessor, "/fake/path/foo.rb:3-15:3-18")
362
389
  assert_entry("qux", Entry::Accessor, "/fake/path/foo.rb:4-17:4-20")
363
390
  assert_entry("qux=", Entry::Accessor, "/fake/path/foo.rb:4-17:4-20")
@@ -461,5 +488,233 @@ module RubyIndexer
461
488
  assert_equal(6, name_location.start_column)
462
489
  assert_equal(9, name_location.end_column)
463
490
  end
491
+
492
+ def test_signature_matches_for_a_method_with_positional_params
493
+ index(<<~RUBY)
494
+ class Foo
495
+ def bar(a, b = 123)
496
+ end
497
+ end
498
+ RUBY
499
+
500
+ entry = T.must(@index["bar"].first)
501
+
502
+ # Matching calls
503
+ assert_signature_matches(entry, "bar()")
504
+ assert_signature_matches(entry, "bar(1)")
505
+ assert_signature_matches(entry, "bar(1, 2)")
506
+ assert_signature_matches(entry, "bar(...)")
507
+ assert_signature_matches(entry, "bar(1, ...)")
508
+ assert_signature_matches(entry, "bar(*a)")
509
+ assert_signature_matches(entry, "bar(1, *a)")
510
+ assert_signature_matches(entry, "bar(1, *a, 2)")
511
+ assert_signature_matches(entry, "bar(*a, 2)")
512
+ assert_signature_matches(entry, "bar(1, **a)")
513
+ assert_signature_matches(entry, "bar(1) {}")
514
+ # This call is impossible to analyze statically because it depends on whether there are elements inside `a` or
515
+ # not. If there's nothing, the call will fail. But if there's anything inside, the hash will become the first
516
+ # positional argument
517
+ assert_signature_matches(entry, "bar(**a)")
518
+
519
+ # Non matching calls
520
+
521
+ refute_signature_matches(entry, "bar(1, 2, 3)")
522
+ refute_signature_matches(entry, "bar(1, b: 2)")
523
+ refute_signature_matches(entry, "bar(1, 2, c: 3)")
524
+ end
525
+
526
+ def test_signature_matches_for_a_method_with_argument_forwarding
527
+ index(<<~RUBY)
528
+ class Foo
529
+ def bar(...)
530
+ end
531
+ end
532
+ RUBY
533
+
534
+ entry = T.must(@index["bar"].first)
535
+
536
+ # All calls match a forwarding parameter
537
+ assert_signature_matches(entry, "bar(1)")
538
+ assert_signature_matches(entry, "bar(1, 2)")
539
+ assert_signature_matches(entry, "bar(...)")
540
+ assert_signature_matches(entry, "bar(1, ...)")
541
+ assert_signature_matches(entry, "bar(*a)")
542
+ assert_signature_matches(entry, "bar(1, *a)")
543
+ assert_signature_matches(entry, "bar(1, *a, 2)")
544
+ assert_signature_matches(entry, "bar(*a, 2)")
545
+ assert_signature_matches(entry, "bar(1, **a)")
546
+ assert_signature_matches(entry, "bar(1) {}")
547
+ assert_signature_matches(entry, "bar()")
548
+ assert_signature_matches(entry, "bar(1, 2, 3)")
549
+ assert_signature_matches(entry, "bar(1, 2, a: 1, b: 5) {}")
550
+ end
551
+
552
+ def test_signature_matches_for_post_forwarding_parameter
553
+ index(<<~RUBY)
554
+ class Foo
555
+ def bar(a, ...)
556
+ end
557
+ end
558
+ RUBY
559
+
560
+ entry = T.must(@index["bar"].first)
561
+
562
+ # All calls with at least one positional argument match
563
+ assert_signature_matches(entry, "bar(1)")
564
+ assert_signature_matches(entry, "bar(1, 2)")
565
+ assert_signature_matches(entry, "bar(...)")
566
+ assert_signature_matches(entry, "bar(1, ...)")
567
+ assert_signature_matches(entry, "bar(*a)")
568
+ assert_signature_matches(entry, "bar(1, *a)")
569
+ assert_signature_matches(entry, "bar(1, *a, 2)")
570
+ assert_signature_matches(entry, "bar(*a, 2)")
571
+ assert_signature_matches(entry, "bar(1, **a)")
572
+ assert_signature_matches(entry, "bar(1) {}")
573
+ assert_signature_matches(entry, "bar(1, 2, 3)")
574
+ assert_signature_matches(entry, "bar(1, 2, a: 1, b: 5) {}")
575
+ assert_signature_matches(entry, "bar()")
576
+ end
577
+
578
+ def test_signature_matches_for_destructured_parameters
579
+ index(<<~RUBY)
580
+ class Foo
581
+ def bar(a, (b, c))
582
+ end
583
+ end
584
+ RUBY
585
+
586
+ entry = T.must(@index["bar"].first)
587
+
588
+ # All calls with at least one positional argument match
589
+ assert_signature_matches(entry, "bar()")
590
+ assert_signature_matches(entry, "bar(1)")
591
+ assert_signature_matches(entry, "bar(1, 2)")
592
+ assert_signature_matches(entry, "bar(...)")
593
+ assert_signature_matches(entry, "bar(1, ...)")
594
+ assert_signature_matches(entry, "bar(*a)")
595
+ assert_signature_matches(entry, "bar(1, *a)")
596
+ assert_signature_matches(entry, "bar(*a, 2)")
597
+ # This matches because `bar(1, *[], 2)` would result in `bar(1, 2)`, which is a valid call
598
+ assert_signature_matches(entry, "bar(1, *a, 2)")
599
+ assert_signature_matches(entry, "bar(1, **a)")
600
+ assert_signature_matches(entry, "bar(1) {}")
601
+
602
+ refute_signature_matches(entry, "bar(1, 2, 3)")
603
+ refute_signature_matches(entry, "bar(1, 2, a: 1, b: 5) {}")
604
+ end
605
+
606
+ def test_signature_matches_for_post_parameters
607
+ index(<<~RUBY)
608
+ class Foo
609
+ def bar(*splat, a)
610
+ end
611
+ end
612
+ RUBY
613
+
614
+ entry = T.must(@index["bar"].first)
615
+
616
+ # All calls with at least one positional argument match
617
+ assert_signature_matches(entry, "bar(1)")
618
+ assert_signature_matches(entry, "bar(1, 2)")
619
+ assert_signature_matches(entry, "bar(...)")
620
+ assert_signature_matches(entry, "bar(1, ...)")
621
+ assert_signature_matches(entry, "bar(*a)")
622
+ assert_signature_matches(entry, "bar(1, *a)")
623
+ assert_signature_matches(entry, "bar(*a, 2)")
624
+ assert_signature_matches(entry, "bar(1, *a, 2)")
625
+ assert_signature_matches(entry, "bar(1, **a)")
626
+ assert_signature_matches(entry, "bar(1, 2, 3)")
627
+ assert_signature_matches(entry, "bar(1) {}")
628
+ assert_signature_matches(entry, "bar()")
629
+
630
+ refute_signature_matches(entry, "bar(1, 2, a: 1, b: 5) {}")
631
+ end
632
+
633
+ def test_signature_matches_for_keyword_parameters
634
+ index(<<~RUBY)
635
+ class Foo
636
+ def bar(a:, b: 123)
637
+ end
638
+ end
639
+ RUBY
640
+
641
+ entry = T.must(@index["bar"].first)
642
+
643
+ assert_signature_matches(entry, "bar(...)")
644
+ assert_signature_matches(entry, "bar()")
645
+ assert_signature_matches(entry, "bar(a: 1)")
646
+ assert_signature_matches(entry, "bar(a: 1, b: 32)")
647
+
648
+ refute_signature_matches(entry, "bar(a: 1, c: 2)")
649
+ refute_signature_matches(entry, "bar(1, ...)")
650
+ refute_signature_matches(entry, "bar(1) {}")
651
+ refute_signature_matches(entry, "bar(1, *a)")
652
+ refute_signature_matches(entry, "bar(*a, 2)")
653
+ refute_signature_matches(entry, "bar(1, *a, 2)")
654
+ refute_signature_matches(entry, "bar(1, **a)")
655
+ refute_signature_matches(entry, "bar(*a)")
656
+ refute_signature_matches(entry, "bar(1)")
657
+ refute_signature_matches(entry, "bar(1, 2)")
658
+ refute_signature_matches(entry, "bar(1, 2, a: 1, b: 5) {}")
659
+ end
660
+
661
+ def test_signature_matches_for_keyword_splats
662
+ index(<<~RUBY)
663
+ class Foo
664
+ def bar(a, b:, **kwargs)
665
+ end
666
+ end
667
+ RUBY
668
+
669
+ entry = T.must(@index["bar"].first)
670
+
671
+ assert_signature_matches(entry, "bar(...)")
672
+ assert_signature_matches(entry, "bar()")
673
+ assert_signature_matches(entry, "bar(1)")
674
+ assert_signature_matches(entry, "bar(1, b: 2)")
675
+ assert_signature_matches(entry, "bar(1, b: 2, c: 3, d: 4)")
676
+
677
+ refute_signature_matches(entry, "bar(1, 2, b: 2)")
678
+ end
679
+
680
+ def test_partial_signature_matches
681
+ # It's important to match signatures partially, because we want to figure out which signature we should show while
682
+ # the user is in the middle of typing
683
+ index(<<~RUBY)
684
+ class Foo
685
+ def bar(a:, b:)
686
+ end
687
+
688
+ def baz(a, b)
689
+ end
690
+ end
691
+ RUBY
692
+
693
+ entry = T.must(@index["bar"].first)
694
+ assert_signature_matches(entry, "bar(a: 1)")
695
+
696
+ entry = T.must(@index["baz"].first)
697
+ assert_signature_matches(entry, "baz(1)")
698
+ end
699
+
700
+ private
701
+
702
+ sig { params(entry: Entry::Method, call_string: String).void }
703
+ def assert_signature_matches(entry, call_string)
704
+ sig = T.must(entry.signatures.first)
705
+ arguments = parse_prism_args(call_string)
706
+ assert(sig.matches?(arguments), "Expected #{call_string} to match #{entry.name}#{entry.decorated_parameters}")
707
+ end
708
+
709
+ sig { params(entry: Entry::Method, call_string: String).void }
710
+ def refute_signature_matches(entry, call_string)
711
+ sig = T.must(entry.signatures.first)
712
+ arguments = parse_prism_args(call_string)
713
+ refute(sig.matches?(arguments), "Expected #{call_string} to not match #{entry.name}#{entry.decorated_parameters}")
714
+ end
715
+
716
+ def parse_prism_args(s)
717
+ Array(Prism.parse(s).value.statements.body.first.arguments&.arguments)
718
+ end
464
719
  end
465
720
  end
@@ -345,7 +345,7 @@ module RubyIndexer
345
345
  assert_equal("all?", entry.old_name)
346
346
  assert_equal("Array", entry.owner.name)
347
347
  assert(entry.file_path.end_with?("core/array.rbs"))
348
- assert_includes(entry.comments[0], "Returns `true` if any element of `self` meets a given criterion.")
348
+ assert_includes(entry.comments, "Returns `true` if any element of `self` meets a given criterion.")
349
349
  end
350
350
 
351
351
  private
@@ -31,6 +31,7 @@ module RubyLsp
31
31
  Thread,
32
32
  )
33
33
 
34
+ @global_state = T.let(GlobalState.new, GlobalState)
34
35
  Thread.main.priority = 1
35
36
  end
36
37
 
@@ -52,7 +53,26 @@ module RubyLsp
52
53
  message[:params][:textDocument][:uri] = parsed_uri
53
54
 
54
55
  # We don't want to try to parse documents on text synchronization notifications
55
- @store.get(parsed_uri).parse unless method.start_with?("textDocument/did")
56
+ unless method.start_with?("textDocument/did")
57
+ document = @store.get(parsed_uri)
58
+
59
+ # If the client supports request delegation and we're working with an ERB document and there was
60
+ # something to parse, then we have to maintain the client updated about the virtual state of the host
61
+ # language source
62
+ if document.parse! && @global_state.supports_request_delegation && document.is_a?(ERBDocument)
63
+ send_message(
64
+ Notification.new(
65
+ method: "delegate/textDocument/virtualState",
66
+ params: {
67
+ textDocument: {
68
+ uri: uri,
69
+ text: document.host_language_source,
70
+ },
71
+ },
72
+ ),
73
+ )
74
+ end
75
+ end
56
76
  rescue Store::NonExistingDocumentError
57
77
  # If we receive a request for a file that no longer exists, we don't want to fail
58
78
  end
@@ -51,7 +51,8 @@ module RubyLsp
51
51
  @version = T.let(version, Integer)
52
52
  @uri = T.let(uri, URI::Generic)
53
53
  @needs_parsing = T.let(true, T::Boolean)
54
- @parse_result = T.let(parse, ParseResultType)
54
+ @parse_result = T.let(T.unsafe(nil), ParseResultType)
55
+ parse!
55
56
  end
56
57
 
57
58
  sig { params(other: Document[T.untyped]).returns(T::Boolean) }
@@ -106,8 +107,9 @@ module RubyLsp
106
107
  @cache.clear
107
108
  end
108
109
 
109
- sig { abstract.returns(ParseResultType) }
110
- def parse; end
110
+ # Returns `true` if the document was parsed and `false` if nothing needed parsing
111
+ sig { abstract.returns(T::Boolean) }
112
+ def parse!; end
111
113
 
112
114
  sig { abstract.returns(T::Boolean) }
113
115
  def syntax_error?; end
@@ -6,17 +6,30 @@ module RubyLsp
6
6
  extend T::Sig
7
7
  extend T::Generic
8
8
 
9
+ sig { returns(String) }
10
+ attr_reader :host_language_source
11
+
9
12
  ParseResultType = type_member { { fixed: Prism::ParseResult } }
10
13
 
11
- sig { override.returns(ParseResultType) }
12
- def parse
13
- return @parse_result unless @needs_parsing
14
+ sig { params(source: String, version: Integer, uri: URI::Generic, encoding: Encoding).void }
15
+ def initialize(source:, version:, uri:, encoding: Encoding::UTF_8)
16
+ # This has to be initialized before calling super because we call `parse` in the parent constructor, which
17
+ # overrides this with the proper virtual host language source
18
+ @host_language_source = T.let("", String)
19
+ super
20
+ end
21
+
22
+ sig { override.returns(T::Boolean) }
23
+ def parse!
24
+ return false unless @needs_parsing
14
25
 
15
26
  @needs_parsing = false
16
27
  scanner = ERBScanner.new(@source)
17
28
  scanner.scan
29
+ @host_language_source = scanner.host_language
18
30
  # assigning empty scopes to turn Prism into eval mode
19
31
  @parse_result = Prism.parse(scanner.ruby, scopes: [[]])
32
+ true
20
33
  end
21
34
 
22
35
  sig { override.returns(T::Boolean) }
@@ -39,16 +52,22 @@ module RubyLsp
39
52
  RubyDocument.locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types)
40
53
  end
41
54
 
55
+ sig { params(char_position: Integer).returns(T.nilable(T::Boolean)) }
56
+ def inside_host_language?(char_position)
57
+ char = @host_language_source[char_position]
58
+ char && char != " "
59
+ end
60
+
42
61
  class ERBScanner
43
62
  extend T::Sig
44
63
 
45
64
  sig { returns(String) }
46
- attr_reader :ruby, :html
65
+ attr_reader :ruby, :host_language
47
66
 
48
67
  sig { params(source: String).void }
49
68
  def initialize(source)
50
69
  @source = source
51
- @html = T.let(+"", String)
70
+ @host_language = T.let(+"", String)
52
71
  @ruby = T.let(+"", String)
53
72
  @current_pos = T.let(0, Integer)
54
73
  @inside_ruby = T.let(false, T::Boolean)
@@ -104,16 +123,16 @@ module RubyLsp
104
123
  end
105
124
  when "\r"
106
125
  @ruby << char
107
- @html << char
126
+ @host_language << char
108
127
 
109
128
  if next_char == "\n"
110
129
  @ruby << next_char
111
- @html << next_char
130
+ @host_language << next_char
112
131
  @current_pos += 1
113
132
  end
114
133
  when "\n"
115
134
  @ruby << char
116
- @html << char
135
+ @host_language << char
117
136
  else
118
137
  push_char(T.must(char))
119
138
  end
@@ -123,10 +142,10 @@ module RubyLsp
123
142
  def push_char(char)
124
143
  if @inside_ruby
125
144
  @ruby << char
126
- @html << " " * char.length
145
+ @host_language << " " * char.length
127
146
  else
128
147
  @ruby << " " * char.length
129
- @html << char
148
+ @host_language << char
130
149
  end
131
150
  end
132
151
 
@@ -21,7 +21,7 @@ module RubyLsp
21
21
  attr_reader :encoding
22
22
 
23
23
  sig { returns(T::Boolean) }
24
- attr_reader :supports_watching_files, :experimental_features
24
+ attr_reader :supports_watching_files, :experimental_features, :supports_request_delegation
25
25
 
26
26
  sig { returns(TypeInferrer) }
27
27
  attr_reader :type_inferrer
@@ -41,6 +41,7 @@ module RubyLsp
41
41
  @experimental_features = T.let(false, T::Boolean)
42
42
  @type_inferrer = T.let(TypeInferrer.new(@index, @experimental_features), TypeInferrer)
43
43
  @addon_settings = T.let({}, T::Hash[String, T.untyped])
44
+ @supports_request_delegation = T.let(false, T::Boolean)
44
45
  end
45
46
 
46
47
  sig { params(addon_name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
@@ -131,6 +132,7 @@ module RubyLsp
131
132
  @addon_settings.merge!(addon_settings)
132
133
  end
133
134
 
135
+ @supports_request_delegation = options.dig(:capabilities, :experimental, :requestDelegation) || false
134
136
  notifications
135
137
  end
136
138
 
@@ -44,6 +44,7 @@ module RubyLsp
44
44
  @group_id_stack = T.let([], T::Array[Integer])
45
45
  # We want to avoid adding code lenses for nested definitions
46
46
  @def_depth = T.let(0, Integer)
47
+ @spec_id = T.let(0, Integer)
47
48
 
48
49
  dispatcher.register(
49
50
  self,
@@ -70,6 +71,7 @@ module RubyLsp
70
71
  name: class_name,
71
72
  command: generate_test_command(group_stack: @group_stack),
72
73
  kind: :group,
74
+ id: generate_fully_qualified_id(group_stack: @group_stack),
73
75
  )
74
76
 
75
77
  @group_id_stack.push(@group_id)
@@ -106,6 +108,7 @@ module RubyLsp
106
108
  name: method_name,
107
109
  command: generate_test_command(method_name: method_name, group_stack: @group_stack),
108
110
  kind: :example,
111
+ id: generate_fully_qualified_id(group_stack: @group_stack, method_name: method_name),
109
112
  )
110
113
  end
111
114
  end
@@ -166,19 +169,20 @@ module RubyLsp
166
169
  @visibility_stack.push([prev_visibility, prev_visibility])
167
170
  if node.name == DESCRIBE_KEYWORD
168
171
  @group_id_stack.pop
172
+ @group_stack.pop
169
173
  end
170
174
  end
171
175
 
172
176
  private
173
177
 
174
- sig { params(node: Prism::Node, name: String, command: String, kind: Symbol).void }
175
- def add_test_code_lens(node, name:, command:, kind:)
178
+ sig { params(node: Prism::Node, name: String, command: String, kind: Symbol, id: String).void }
179
+ def add_test_code_lens(node, name:, command:, kind:, id: name)
176
180
  # don't add code lenses if the test library is not supported or unknown
177
181
  return unless SUPPORTED_TEST_LIBRARIES.include?(@global_state.test_library) && @path
178
182
 
179
183
  arguments = [
180
184
  @path,
181
- name,
185
+ id,
182
186
  command,
183
187
  {
184
188
  start_line: node.location.start_line - 1,
@@ -186,6 +190,7 @@ module RubyLsp
186
190
  end_line: node.location.end_line - 1,
187
191
  end_column: node.location.end_column,
188
192
  },
193
+ name,
189
194
  ]
190
195
 
191
196
  grouping_data = { group_id: @group_id_stack.last, kind: kind }
@@ -247,7 +252,7 @@ module RubyLsp
247
252
  # We know the entire path, do an exact match
248
253
  " --name " + Shellwords.escape(group_stack.join("::")) + "#" + Shellwords.escape(method_name)
249
254
  elsif spec_name
250
- " --name " + "/#{Shellwords.escape(spec_name)}/"
255
+ " --name " + "\"/^#{Shellwords.escape(group_stack.join("::"))}##{Shellwords.escape(spec_name)}$/\""
251
256
  else
252
257
  # Execute all tests of the selected class and tests in
253
258
  # modules/classes nested inside of that class
@@ -282,15 +287,39 @@ module RubyLsp
282
287
 
283
288
  return unless name
284
289
 
290
+ if kind == :example
291
+ # Increment spec_id for each example
292
+ @spec_id += 1
293
+ else
294
+ # Reset spec_id when entering a new group
295
+ @spec_id = 0
296
+ @group_stack.push(name)
297
+ end
298
+
285
299
  if @path
300
+ method_name = format("test_%04d_%s", @spec_id, name) if kind == :example
286
301
  add_test_code_lens(
287
302
  node,
288
303
  name: name,
289
- command: generate_test_command(spec_name: name),
304
+ command: generate_test_command(group_stack: @group_stack, spec_name: method_name),
290
305
  kind: kind,
306
+ id: generate_fully_qualified_id(group_stack: @group_stack, method_name: method_name),
291
307
  )
292
308
  end
293
309
  end
310
+
311
+ sig { params(group_stack: T::Array[String], method_name: T.nilable(String)).returns(String) }
312
+ def generate_fully_qualified_id(group_stack:, method_name: nil)
313
+ if method_name
314
+ # For tests, this will be the test class and method name: `Foo::BarTest#test_baz`.
315
+ # For specs, this will be the nested descriptions and formatted test name: `a::b::c#test_001_foo`.
316
+ group_stack.join("::") + "#" + method_name
317
+ else
318
+ # For tests, this will be the test class: `Foo::BarTest`.
319
+ # For specs, this will be the nested descriptions: `a::b::c`.
320
+ group_stack.join("::")
321
+ end
322
+ end
294
323
  end
295
324
  end
296
325
  end
@@ -42,17 +42,46 @@ module RubyLsp
42
42
  target_method = methods.first
43
43
  return unless target_method
44
44
 
45
- parameters = target_method.parameters
46
- name = target_method.name
45
+ signatures = target_method.signatures
47
46
 
48
47
  # If the method doesn't have any parameters, there's no need to show signature help
49
- return if parameters.empty?
48
+ return if signatures.empty?
49
+
50
+ name = target_method.name
51
+ title = +""
52
+
53
+ extra_links = if type.is_a?(TypeInferrer::GuessedType)
54
+ title << "\n\nGuessed receiver: #{type.name}"
55
+ "[Learn more about guessed types](#{GUESSED_TYPES_URL})"
56
+ end
50
57
 
51
- label = "#{name}(#{parameters.map(&:decorated_name).join(", ")})"
58
+ active_signature, active_parameter = determine_active_signature_and_parameter(node, signatures)
52
59
 
60
+ signature_help = Interface::SignatureHelp.new(
61
+ signatures: generate_signatures(signatures, name, methods, title, extra_links),
62
+ active_signature: active_signature,
63
+ active_parameter: active_parameter,
64
+ )
65
+ @response_builder.replace(signature_help)
66
+ end
67
+
68
+ private
69
+
70
+ sig do
71
+ params(node: Prism::CallNode, signatures: T::Array[RubyIndexer::Entry::Signature]).returns([Integer, Integer])
72
+ end
73
+ def determine_active_signature_and_parameter(node, signatures)
53
74
  arguments_node = node.arguments
54
75
  arguments = arguments_node&.arguments || []
55
- active_parameter = (arguments.length - 1).clamp(0, parameters.length - 1)
76
+
77
+ # Find the first signature that matches the current arguments. If the user is invoking a method incorrectly and
78
+ # none of the signatures match, we show the first one
79
+ active_sig_index = signatures.find_index do |signature|
80
+ signature.matches?(arguments)
81
+ end || 0
82
+
83
+ parameter_length = [T.must(signatures[active_sig_index]).parameters.length - 1, 0].max
84
+ active_parameter = (arguments.length - 1).clamp(0, parameter_length)
56
85
 
57
86
  # If there are arguments, then we need to check if there's a trailing comma after the end of the last argument
58
87
  # to advance the active parameter to the next one
@@ -61,27 +90,29 @@ module RubyLsp
61
90
  active_parameter += 1
62
91
  end
63
92
 
64
- title = +""
65
-
66
- extra_links = if type.is_a?(TypeInferrer::GuessedType)
67
- title << "\n\nGuessed receiver: #{type.name}"
68
- "[Learn more about guessed types](#{GUESSED_TYPES_URL})"
69
- end
93
+ [active_sig_index, active_parameter]
94
+ end
70
95
 
71
- signature_help = Interface::SignatureHelp.new(
72
- signatures: [
73
- Interface::SignatureInformation.new(
74
- label: label,
75
- parameters: parameters.map { |param| Interface::ParameterInformation.new(label: param.name) },
76
- documentation: Interface::MarkupContent.new(
77
- kind: "markdown",
78
- value: markdown_from_index_entries(title, methods, extra_links: extra_links),
79
- ),
96
+ sig do
97
+ params(
98
+ signatures: T::Array[RubyIndexer::Entry::Signature],
99
+ method_name: String,
100
+ methods: T::Array[RubyIndexer::Entry],
101
+ title: String,
102
+ extra_links: T.nilable(String),
103
+ ).returns(T::Array[Interface::SignatureInformation])
104
+ end
105
+ def generate_signatures(signatures, method_name, methods, title, extra_links)
106
+ signatures.map do |signature|
107
+ Interface::SignatureInformation.new(
108
+ label: "#{method_name}(#{signature.format})",
109
+ parameters: signature.parameters.map { |param| Interface::ParameterInformation.new(label: param.name) },
110
+ documentation: Interface::MarkupContent.new(
111
+ kind: "markdown",
112
+ value: markdown_from_index_entries(title, methods, extra_links: extra_links),
80
113
  ),
81
- ],
82
- active_parameter: active_parameter,
83
- )
84
- @response_builder.replace(signature_help)
114
+ )
115
+ end
85
116
  end
86
117
  end
87
118
  end