ruby-lsp 0.17.16 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -110
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +10 -8
  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 +31 -12
  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/index_test.rb +41 -0
  13. data/lib/ruby_indexer/test/method_test.rb +257 -2
  14. data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
  15. data/lib/ruby_lsp/addon.rb +3 -2
  16. data/lib/ruby_lsp/base_server.rb +21 -1
  17. data/lib/ruby_lsp/document.rb +5 -3
  18. data/lib/ruby_lsp/erb_document.rb +29 -10
  19. data/lib/ruby_lsp/global_state.rb +15 -1
  20. data/lib/ruby_lsp/listeners/code_lens.rb +34 -5
  21. data/lib/ruby_lsp/listeners/folding_ranges.rb +1 -1
  22. data/lib/ruby_lsp/listeners/semantic_highlighting.rb +28 -0
  23. data/lib/ruby_lsp/listeners/signature_help.rb +55 -24
  24. data/lib/ruby_lsp/rbs_document.rb +5 -4
  25. data/lib/ruby_lsp/requests/code_action_resolve.rb +0 -15
  26. data/lib/ruby_lsp/requests/code_actions.rb +0 -10
  27. data/lib/ruby_lsp/requests/code_lens.rb +1 -11
  28. data/lib/ruby_lsp/requests/completion.rb +3 -20
  29. data/lib/ruby_lsp/requests/completion_resolve.rb +0 -8
  30. data/lib/ruby_lsp/requests/definition.rb +6 -20
  31. data/lib/ruby_lsp/requests/diagnostics.rb +0 -10
  32. data/lib/ruby_lsp/requests/document_highlight.rb +7 -14
  33. data/lib/ruby_lsp/requests/document_link.rb +0 -10
  34. data/lib/ruby_lsp/requests/document_symbol.rb +0 -17
  35. data/lib/ruby_lsp/requests/folding_ranges.rb +0 -10
  36. data/lib/ruby_lsp/requests/formatting.rb +3 -17
  37. data/lib/ruby_lsp/requests/hover.rb +9 -9
  38. data/lib/ruby_lsp/requests/inlay_hints.rb +0 -30
  39. data/lib/ruby_lsp/requests/on_type_formatting.rb +0 -10
  40. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +0 -11
  41. data/lib/ruby_lsp/requests/request.rb +17 -1
  42. data/lib/ruby_lsp/requests/selection_ranges.rb +0 -10
  43. data/lib/ruby_lsp/requests/semantic_highlighting.rb +1 -23
  44. data/lib/ruby_lsp/requests/show_syntax_tree.rb +0 -11
  45. data/lib/ruby_lsp/requests/signature_help.rb +5 -20
  46. data/lib/ruby_lsp/requests/support/common.rb +1 -1
  47. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +2 -0
  48. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +0 -11
  49. data/lib/ruby_lsp/requests/workspace_symbol.rb +0 -12
  50. data/lib/ruby_lsp/ruby_document.rb +4 -3
  51. data/lib/ruby_lsp/server.rb +23 -8
  52. data/lib/ruby_lsp/setup_bundler.rb +31 -13
  53. data/lib/ruby_lsp/type_inferrer.rb +6 -2
  54. data/lib/ruby_lsp/utils.rb +11 -1
  55. metadata +7 -14
  56. data/lib/ruby_lsp/check_docs.rb +0 -130
@@ -213,10 +213,10 @@ module RubyIndexer
213
213
  RUBY
214
214
 
215
215
  foo_entry = @index["Foo"].first
216
- assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments.join("\n"))
216
+ assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments)
217
217
 
218
218
  bar_entry = @index["Bar"].first
219
- assert_equal("This Bar comment has 1 line padding", bar_entry.comments.join("\n"))
219
+ assert_equal("This Bar comment has 1 line padding", bar_entry.comments)
220
220
  end
221
221
 
222
222
  def test_skips_comments_containing_invalid_encodings
@@ -239,10 +239,10 @@ module RubyIndexer
239
239
  RUBY
240
240
 
241
241
  foo_entry = @index["Foo"].first
242
- assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments.join("\n"))
242
+ assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments)
243
243
 
244
244
  bar_entry = @index["Foo::Bar"].first
245
- assert_equal("This is a Bar comment", bar_entry.comments.join("\n"))
245
+ assert_equal("This is a Bar comment", bar_entry.comments)
246
246
  end
247
247
 
248
248
  def test_comments_can_be_attached_to_a_reopened_class
@@ -255,10 +255,10 @@ module RubyIndexer
255
255
  RUBY
256
256
 
257
257
  first_foo_entry = @index["Foo"][0]
258
- assert_equal("This is a Foo comment", first_foo_entry.comments.join("\n"))
258
+ assert_equal("This is a Foo comment", first_foo_entry.comments)
259
259
 
260
260
  second_foo_entry = @index["Foo"][1]
261
- assert_equal("This is another Foo comment", second_foo_entry.comments.join("\n"))
261
+ assert_equal("This is another Foo comment", second_foo_entry.comments)
262
262
  end
263
263
 
264
264
  def test_comments_removes_the_leading_pound_and_space
@@ -271,10 +271,10 @@ module RubyIndexer
271
271
  RUBY
272
272
 
273
273
  first_foo_entry = @index["Foo"][0]
274
- assert_equal("This is a Foo comment", first_foo_entry.comments.join("\n"))
274
+ assert_equal("This is a Foo comment", first_foo_entry.comments)
275
275
 
276
276
  second_foo_entry = @index["Bar"][0]
277
- assert_equal("This is a Bar comment", second_foo_entry.comments.join("\n"))
277
+ assert_equal("This is a Bar comment", second_foo_entry.comments)
278
278
  end
279
279
 
280
280
  def test_private_class_and_module_indexing
@@ -483,7 +483,7 @@ module RubyIndexer
483
483
 
484
484
  foo = T.must(@index["Foo::<Class:Foo>"].first)
485
485
  assert_equal(4, foo.location.start_line)
486
- assert_equal("Some extra comments", foo.comments.join("\n"))
486
+ assert_equal("Some extra comments", foo.comments)
487
487
  end
488
488
 
489
489
  def test_dynamic_singleton_class_blocks
@@ -501,7 +501,7 @@ module RubyIndexer
501
501
  # That pattern cannot be properly analyzed statically and assuming that it's always a regular singleton simplifies
502
502
  # the implementation considerably.
503
503
  assert_equal(3, singleton.location.start_line)
504
- assert_equal("Some extra comments", singleton.comments.join("\n"))
504
+ assert_equal("Some extra comments", singleton.comments)
505
505
  end
506
506
 
507
507
  def test_namespaces_inside_singleton_blocks
@@ -86,16 +86,16 @@ module RubyIndexer
86
86
  A::BAZ = 1
87
87
  RUBY
88
88
 
89
- foo_comment = @index["FOO"].first.comments.join("\n")
89
+ foo_comment = @index["FOO"].first.comments
90
90
  assert_equal("FOO comment", foo_comment)
91
91
 
92
- a_foo_comment = @index["A::FOO"].first.comments.join("\n")
92
+ a_foo_comment = @index["A::FOO"].first.comments
93
93
  assert_equal("A::FOO comment", a_foo_comment)
94
94
 
95
- bar_comment = @index["BAR"].first.comments.join("\n")
95
+ bar_comment = @index["BAR"].first.comments
96
96
  assert_equal("::BAR comment", bar_comment)
97
97
 
98
- a_baz_comment = @index["A::BAZ"].first.comments.join("\n")
98
+ a_baz_comment = @index["A::BAZ"].first.comments
99
99
  assert_equal("A::BAZ comment", a_baz_comment)
100
100
  end
101
101
 
@@ -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,
@@ -1822,5 +1822,46 @@ module RubyIndexer
1822
1822
  @index.linearized_ancestors_of("Foo::Child::<Class:Child>"),
1823
1823
  )
1824
1824
  end
1825
+
1826
+ def test_resolving_circular_method_aliases_on_class_reopen
1827
+ index(<<~RUBY)
1828
+ class Foo
1829
+ alias bar ==
1830
+ def ==(other) = true
1831
+ end
1832
+
1833
+ class Foo
1834
+ alias == bar
1835
+ end
1836
+ RUBY
1837
+
1838
+ method = @index.resolve_method("==", "Foo").first
1839
+ assert_kind_of(Entry::Method, method)
1840
+ assert_equal("==", method.name)
1841
+
1842
+ candidates = @index.method_completion_candidates("=", "Foo")
1843
+ assert_equal(["==", "==="], candidates.map(&:name))
1844
+ end
1845
+
1846
+ def test_entries_for
1847
+ index(<<~RUBY)
1848
+ class Foo; end
1849
+
1850
+ module Bar
1851
+ def my_def; end
1852
+ def self.my_singleton_def; end
1853
+ end
1854
+ RUBY
1855
+
1856
+ entries = @index.entries_for("/fake/path/foo.rb", Entry)
1857
+ assert_equal(["Foo", "Bar", "my_def", "Bar::<Class:Bar>", "my_singleton_def"], entries.map(&:name))
1858
+
1859
+ entries = @index.entries_for("/fake/path/foo.rb", RubyIndexer::Entry::Namespace)
1860
+ assert_equal(["Foo", "Bar", "Bar::<Class:Bar>"], entries.map(&:name))
1861
+ end
1862
+
1863
+ def test_entries_for_returns_nil_if_no_matches
1864
+ assert_nil(@index.entries_for("non_existing_file.rb", Entry::Namespace))
1865
+ end
1825
1866
  end
1826
1867
  end
@@ -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
@@ -30,6 +30,8 @@ module RubyLsp
30
30
  # Addon instances that have declared a handler to accept file watcher events
31
31
  @file_watcher_addons = T.let([], T::Array[Addon])
32
32
 
33
+ AddonNotFoundError = Class.new(StandardError)
34
+
33
35
  class << self
34
36
  extend T::Sig
35
37
 
@@ -78,11 +80,10 @@ module RubyLsp
78
80
  errors
79
81
  end
80
82
 
81
- # Intended for use by tests for addons
82
83
  sig { params(addon_name: String).returns(Addon) }
83
84
  def get(addon_name)
84
85
  addon = addons.find { |addon| addon.name == addon_name }
85
- raise "Could not find addon '#{addon_name}'" unless addon
86
+ raise AddonNotFoundError, "Could not find addon '#{addon_name}'" unless addon
86
87
 
87
88
  addon
88
89
  end
@@ -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
@@ -40,6 +40,13 @@ module RubyLsp
40
40
  @supports_watching_files = T.let(false, T::Boolean)
41
41
  @experimental_features = T.let(false, T::Boolean)
42
42
  @type_inferrer = T.let(TypeInferrer.new(@index, @experimental_features), TypeInferrer)
43
+ @addon_settings = T.let({}, T::Hash[String, T.untyped])
44
+ @supports_request_delegation = T.let(false, T::Boolean)
45
+ end
46
+
47
+ sig { params(addon_name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
48
+ def settings_for_addon(addon_name)
49
+ @addon_settings[addon_name]
43
50
  end
44
51
 
45
52
  sig { params(identifier: String, instance: Requests::Support::Formatter).void }
@@ -119,6 +126,13 @@ module RubyLsp
119
126
  @experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled) || false
120
127
  @type_inferrer.experimental_features = @experimental_features
121
128
 
129
+ addon_settings = options.dig(:initializationOptions, :addonSettings)
130
+ if addon_settings
131
+ addon_settings.transform_keys!(&:to_s)
132
+ @addon_settings.merge!(addon_settings)
133
+ end
134
+
135
+ @supports_request_delegation = options.dig(:capabilities, :experimental, :requestDelegation) || false
122
136
  notifications
123
137
  end
124
138