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.
- checksums.yaml +4 -4
- data/README.md +4 -110
- data/VERSION +1 -1
- data/exe/ruby-lsp +5 -4
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +14 -6
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +157 -27
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +5 -4
- data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +2 -2
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +10 -10
- data/lib/ruby_indexer/test/constant_test.rb +4 -4
- data/lib/ruby_indexer/test/enhancements_test.rb +2 -2
- data/lib/ruby_indexer/test/method_test.rb +257 -2
- data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
- data/lib/ruby_lsp/base_server.rb +21 -1
- data/lib/ruby_lsp/document.rb +5 -3
- data/lib/ruby_lsp/erb_document.rb +29 -10
- data/lib/ruby_lsp/global_state.rb +3 -1
- data/lib/ruby_lsp/listeners/code_lens.rb +34 -5
- data/lib/ruby_lsp/listeners/signature_help.rb +55 -24
- data/lib/ruby_lsp/rbs_document.rb +5 -4
- data/lib/ruby_lsp/requests/code_action_resolve.rb +0 -15
- data/lib/ruby_lsp/requests/code_actions.rb +0 -10
- data/lib/ruby_lsp/requests/code_lens.rb +1 -11
- data/lib/ruby_lsp/requests/completion.rb +3 -20
- data/lib/ruby_lsp/requests/completion_resolve.rb +0 -8
- data/lib/ruby_lsp/requests/definition.rb +6 -20
- data/lib/ruby_lsp/requests/diagnostics.rb +0 -10
- data/lib/ruby_lsp/requests/document_highlight.rb +7 -14
- data/lib/ruby_lsp/requests/document_link.rb +0 -10
- data/lib/ruby_lsp/requests/document_symbol.rb +0 -17
- data/lib/ruby_lsp/requests/folding_ranges.rb +0 -10
- data/lib/ruby_lsp/requests/formatting.rb +0 -16
- data/lib/ruby_lsp/requests/hover.rb +9 -9
- data/lib/ruby_lsp/requests/inlay_hints.rb +0 -30
- data/lib/ruby_lsp/requests/on_type_formatting.rb +0 -10
- data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +0 -11
- data/lib/ruby_lsp/requests/request.rb +17 -1
- data/lib/ruby_lsp/requests/selection_ranges.rb +0 -10
- data/lib/ruby_lsp/requests/semantic_highlighting.rb +1 -23
- data/lib/ruby_lsp/requests/show_syntax_tree.rb +0 -11
- data/lib/ruby_lsp/requests/signature_help.rb +5 -20
- data/lib/ruby_lsp/requests/support/common.rb +1 -1
- data/lib/ruby_lsp/requests/support/rubocop_runner.rb +2 -0
- data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +0 -11
- data/lib/ruby_lsp/requests/workspace_symbol.rb +0 -12
- data/lib/ruby_lsp/ruby_document.rb +4 -3
- data/lib/ruby_lsp/server.rb +23 -8
- data/lib/ruby_lsp/setup_bundler.rb +31 -13
- data/lib/ruby_lsp/type_inferrer.rb +6 -2
- data/lib/ruby_lsp/utils.rb +11 -1
- metadata +3 -4
- 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
|
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
|
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
|
348
|
+
assert_includes(entry.comments, "Returns `true` if any element of `self` meets a given criterion.")
|
349
349
|
end
|
350
350
|
|
351
351
|
private
|
data/lib/ruby_lsp/base_server.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/ruby_lsp/document.rb
CHANGED
@@ -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(
|
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
|
-
|
110
|
-
|
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 {
|
12
|
-
def
|
13
|
-
|
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, :
|
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
|
-
@
|
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
|
-
@
|
126
|
+
@host_language << char
|
108
127
|
|
109
128
|
if next_char == "\n"
|
110
129
|
@ruby << next_char
|
111
|
-
@
|
130
|
+
@host_language << next_char
|
112
131
|
@current_pos += 1
|
113
132
|
end
|
114
133
|
when "\n"
|
115
134
|
@ruby << char
|
116
|
-
@
|
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
|
-
@
|
145
|
+
@host_language << " " * char.length
|
127
146
|
else
|
128
147
|
@ruby << " " * char.length
|
129
|
-
@
|
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
|
-
|
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 " + "
|
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:
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
83
|
-
)
|
84
|
-
@response_builder.replace(signature_help)
|
114
|
+
)
|
115
|
+
end
|
85
116
|
end
|
86
117
|
end
|
87
118
|
end
|