ruby-lsp 0.17.16 → 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 (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