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