ruby-lsp 0.17.17 → 0.18.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -110
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +5 -4
  5. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +14 -6
  6. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +162 -27
  7. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +5 -4
  8. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +2 -2
  9. data/lib/ruby_indexer/test/classes_and_modules_test.rb +24 -10
  10. data/lib/ruby_indexer/test/constant_test.rb +4 -4
  11. data/lib/ruby_indexer/test/enhancements_test.rb +2 -2
  12. data/lib/ruby_indexer/test/method_test.rb +257 -2
  13. data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
  14. data/lib/ruby_lsp/base_server.rb +21 -1
  15. data/lib/ruby_lsp/document.rb +5 -3
  16. data/lib/ruby_lsp/erb_document.rb +29 -10
  17. data/lib/ruby_lsp/global_state.rb +3 -1
  18. data/lib/ruby_lsp/listeners/code_lens.rb +34 -5
  19. data/lib/ruby_lsp/listeners/signature_help.rb +55 -24
  20. data/lib/ruby_lsp/rbs_document.rb +5 -4
  21. data/lib/ruby_lsp/requests/code_action_resolve.rb +0 -15
  22. data/lib/ruby_lsp/requests/code_actions.rb +0 -10
  23. data/lib/ruby_lsp/requests/code_lens.rb +1 -11
  24. data/lib/ruby_lsp/requests/completion.rb +3 -20
  25. data/lib/ruby_lsp/requests/completion_resolve.rb +0 -8
  26. data/lib/ruby_lsp/requests/definition.rb +6 -20
  27. data/lib/ruby_lsp/requests/diagnostics.rb +0 -10
  28. data/lib/ruby_lsp/requests/document_highlight.rb +7 -14
  29. data/lib/ruby_lsp/requests/document_link.rb +0 -10
  30. data/lib/ruby_lsp/requests/document_symbol.rb +0 -17
  31. data/lib/ruby_lsp/requests/folding_ranges.rb +0 -10
  32. data/lib/ruby_lsp/requests/formatting.rb +0 -16
  33. data/lib/ruby_lsp/requests/hover.rb +9 -9
  34. data/lib/ruby_lsp/requests/inlay_hints.rb +0 -30
  35. data/lib/ruby_lsp/requests/on_type_formatting.rb +0 -10
  36. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +0 -11
  37. data/lib/ruby_lsp/requests/request.rb +17 -1
  38. data/lib/ruby_lsp/requests/selection_ranges.rb +0 -10
  39. data/lib/ruby_lsp/requests/semantic_highlighting.rb +1 -23
  40. data/lib/ruby_lsp/requests/show_syntax_tree.rb +0 -11
  41. data/lib/ruby_lsp/requests/signature_help.rb +5 -20
  42. data/lib/ruby_lsp/requests/support/common.rb +1 -1
  43. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +2 -0
  44. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +0 -11
  45. data/lib/ruby_lsp/requests/workspace_symbol.rb +0 -12
  46. data/lib/ruby_lsp/ruby_document.rb +4 -3
  47. data/lib/ruby_lsp/server.rb +23 -8
  48. data/lib/ruby_lsp/setup_bundler.rb +31 -13
  49. data/lib/ruby_lsp/type_inferrer.rb +6 -2
  50. data/lib/ruby_lsp/utils.rb +11 -1
  51. metadata +3 -4
  52. data/lib/ruby_lsp/check_docs.rb +0 -130
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4b4c9204834ddf87c84009003c5eef5872211eaef6ed64022a93a2749e1849e
4
- data.tar.gz: 0d770cf2f67f90fb974a9fca08d9fb4dea1a73d668984ff96e5cc5785d7c5766
3
+ metadata.gz: 42aef038c33d8c03d661dc1c39c745df320399ab86e6c558504c79c93bac4952
4
+ data.tar.gz: ff4ce59697e498c965a0db7b8df44de12e4c81cebb6ef84d50cc54664647f913
5
5
  SHA512:
6
- metadata.gz: 3534227d67761a6738c5e2ae4a06a08964a0836c4a30c9d306917efdd93aae1285d6ed03992e88486a44e5d3f7e7271997e56bbb12ab908599687c6a1e622cb4
7
- data.tar.gz: 5345bcf362b6206a0be392eebe5db2175fe9d06b1a38f113fe11d979e5fdcc48851cb47e5f016ae49726ffc73432e81ef801522588cee79ebb36fe545e02d965
6
+ metadata.gz: 1a88854595b50bab0ede9aaaa40fe8d765afb6f8744246c8d3407769ad981d00579b20a52f61c4f2c929d8ecdd3578625093b41cf30f6c7224ac64117a201b39
7
+ data.tar.gz: 9093a75fbecd3349e0b6bd80bc899928ea40a4afb2c2a1ae64a5be1bde84bf800280a24910a35ff7eac3b7e470785a649a370214f9f51b8bbd898ede81e09903
data/README.md CHANGED
@@ -15,119 +15,13 @@ experience to Ruby developers using modern standards for cross-editor features,
15
15
  Want to discuss Ruby developer experience? Consider joining the public
16
16
  [Ruby DX Slack workspace](https://join.slack.com/t/ruby-dx/shared_invite/zt-2c8zjlir6-uUDJl8oIwcen_FS_aA~b6Q).
17
17
 
18
- ## Features
18
+ ## Getting Started
19
19
 
20
- ![Ruby LSP demo](vscode/extras/ruby_lsp_demo.gif)
20
+ For VS Code users, you can start by installing the [Ruby LSP extension](https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp) from the VS Code marketplace.
21
21
 
22
- The Ruby LSP features include
22
+ For other editors, please refer to the [EDITORS](https://shopify.github.io/ruby-lsp/editors.html) guide.
23
23
 
24
- - Semantic highlighting
25
- - Symbol search and code outline
26
- - RuboCop errors and warnings (diagnostics)
27
- - Format on save (with RuboCop or Syntax Tree)
28
- - Format on type
29
- - Debugging support
30
- - Running and debugging tests through VS Code's UI
31
- - Go to definition for classes, modules, constants and required files
32
- - Showing documentation on hover for classes, modules and constants
33
- - Completion for classes, modules, constants and require paths
34
- - Fuzzy search classes, modules and constants anywhere in the project and its dependencies (workspace symbol)
35
-
36
- As of July 2024, Ruby LSP has received significant enhancements to its code navigation features. For an in-depth look at these improvements, including video demonstrations, check out this [article](https://railsatscale.com/2024-07-18-mastering-ruby-code-navigation-major-enhancements-in-ruby-lsp-2024/). Despite these advancements, we plan to continue enhancing its code navigation support even further. You can follow our progress on this [GitHub issue](https://github.com/Shopify/ruby-lsp/issues/899).
37
-
38
- See complete information about features [here](https://shopify.github.io/ruby-lsp/RubyLsp/Requests.html).
39
-
40
- If you experience issues, please see the [troubleshooting
41
- guide](https://github.com/Shopify/ruby-lsp/blob/main/TROUBLESHOOTING.md).
42
-
43
- ## Usage
44
-
45
- ### With VS Code
46
-
47
- If using VS Code, all you have to do is install the [Ruby LSP
48
- extension](https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp) to get the extra features in the
49
- editor. Do not install the `ruby-lsp` gem manually.
50
-
51
- For more information on using and configuring the extension, see [vscode/README.md](vscode/README.md).
52
-
53
- ### With other editors
54
-
55
- See [editors](EDITORS.md) for community instructions on setting up the Ruby LSP, which current includes Emacs, Neovim, Sublime Text, and Zed.
56
-
57
- The gem can be installed by doing
58
- ```shell
59
- gem install ruby-lsp
60
- ```
61
-
62
- and the language server can be launched running `ruby-lsp` (without bundle exec in order to properly hook into your
63
- project's dependencies).
64
-
65
- ### Documentation
66
-
67
- See the [documentation](https://shopify.github.io/ruby-lsp) for more in-depth details about the
68
- [supported features](https://shopify.github.io/ruby-lsp/RubyLsp/Requests.html).
69
-
70
- For creating rich themes for Ruby using the semantic highlighting information, see the [semantic highlighting
71
- documentation](SEMANTIC_HIGHLIGHTING.md).
72
-
73
- ### Configuring code indexing
74
-
75
- By default, the Ruby LSP indexes all Ruby files defined in the current project and all of its dependencies, including
76
- default gems, except for
77
-
78
- - Gems that only appear under the `:development` group
79
- - All Ruby files under `test/**/*.rb`
80
-
81
- This behaviour can be overridden and tuned. Learn how to configure it [for VS Code](vscode/README.md#Indexing-Configuration) or [for other editors](EDITORS.md#Indexing-Configuration).
82
-
83
- Note that indexing-dependent behavior, such as definition, hover, completion or workspace symbol will be impacted by
84
- the configuration changes.
85
-
86
- The older approach of using a `.index.yml` file has been deprecated and will be removed in a future release.
87
-
88
- ```yaml
89
- # Exclude files based on a given pattern. Often used to exclude test files or fixtures
90
- excluded_patterns:
91
- - "**/spec/**/*.rb"
92
-
93
- # Include files based on a given pattern. Can be used to index Ruby files that use different extensions
94
- included_patterns:
95
- - "**/bin/*"
96
-
97
- # Exclude gems by name. If a gem is never referenced in the project's code and is only used as a tool, excluding it will
98
- # speed up indexing and reduce the amount of results in features like definition or completion
99
- excluded_gems:
100
- - rubocop
101
- - pathname
102
-
103
- # Include gems by name. Normally used to include development gems that are excluded by default
104
- included_gems:
105
- - prism
106
- ```
107
-
108
- ### Addons
109
-
110
- The Ruby LSP provides an addon system that allows other gems to enhance the base functionality with more editor
111
- features. This is the mechanism that powers addons like
112
-
113
- - [Ruby LSP Rails](https://github.com/Shopify/ruby-lsp-rails)
114
- - [Ruby LSP RSpec](https://github.com/st0012/ruby-lsp-rspec)
115
- - [Ruby LSP rubyfmt](https://github.com/jscharf/ruby-lsp-rubyfmt)
116
-
117
- Additionally, some tools may include a Ruby LSP addon directly, like
118
-
119
- - [Standard Ruby (from v1.39.1)](https://github.com/standardrb/standard/wiki/IDE:-vscode#using-ruby-lsp)
120
-
121
- Other community driven addons can be found in [rubygems](https://rubygems.org/search?query=name%3A+ruby-lsp) by
122
- searching for the `ruby-lsp` prefix.
123
-
124
- For instructions on how to create addons, see the [addons documentation](ADDONS.md).
125
-
126
- ## Learn More
127
-
128
- * [RubyConf 2022: Improving the development experience with language servers](https://www.youtube.com/watch?v=kEfXPTm1aCI) ([Vinicius Stock](https://github.com/vinistock))
129
- * [Remote Ruby: Ruby Language Server with Vinicius Stock](https://remoteruby.com/221)
130
- * [RubyKaigi 2023: Code indexing - How language servers understand our code](https://www.youtube.com/watch?v=ks3tQojSJLU) ([Vinicius Stock](https://github.com/vinistock))
24
+ To learn more about Ruby LSP, please refer to the official [documentation](https://shopify.github.io/ruby-lsp) for [supported features](https://shopify.github.io/ruby-lsp#features).
131
25
 
132
26
  ## Contributing
133
27
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.17.17
1
+ 0.18.1
data/exe/ruby-lsp CHANGED
@@ -64,15 +64,12 @@ if ENV["BUNDLE_GEMFILE"].nil?
64
64
  require_relative "../lib/ruby_lsp/setup_bundler"
65
65
 
66
66
  begin
67
- bundle_gemfile, bundle_path, bundle_app_config = RubyLsp::SetupBundler.new(Dir.pwd, **options).setup!
67
+ env = RubyLsp::SetupBundler.new(Dir.pwd, **options).setup!
68
68
  rescue RubyLsp::SetupBundler::BundleNotLocked
69
69
  warn("Project contains a Gemfile, but no Gemfile.lock. Run `bundle install` to lock gems and restart the server")
70
70
  exit(78)
71
71
  end
72
72
 
73
- env = { "BUNDLE_GEMFILE" => bundle_gemfile }
74
- env["BUNDLE_PATH"] = bundle_path if bundle_path
75
- env["BUNDLE_APP_CONFIG"] = bundle_app_config if bundle_app_config
76
73
  exit exec(env, "bundle exec ruby-lsp #{original_args.join(" ")}")
77
74
  end
78
75
 
@@ -135,4 +132,8 @@ if options[:doctor]
135
132
  return
136
133
  end
137
134
 
135
+ # Ensure all output goes out stderr by default to allow puts/p/pp to work
136
+ # without specifying output device.
137
+ $> = $stderr
138
+
138
139
  RubyLsp::Server.new.start
@@ -17,10 +17,11 @@ module RubyIndexer
17
17
  dispatcher: Prism::Dispatcher,
18
18
  parse_result: Prism::ParseResult,
19
19
  file_path: String,
20
+ collect_comments: T::Boolean,
20
21
  enhancements: T::Array[Enhancement],
21
22
  ).void
22
23
  end
23
- def initialize(index, dispatcher, parse_result, file_path, enhancements: [])
24
+ def initialize(index, dispatcher, parse_result, file_path, collect_comments: false, enhancements: [])
24
25
  @index = index
25
26
  @file_path = file_path
26
27
  @enhancements = enhancements
@@ -40,6 +41,7 @@ module RubyIndexer
40
41
  # A stack of namespace entries that represent where we currently are. Used to properly assign methods to an owner
41
42
  @owner_stack = T.let([], T::Array[Entry::Namespace])
42
43
  @indexing_errors = T.let([], T::Array[String])
44
+ @collect_comments = collect_comments
43
45
 
44
46
  dispatcher.register(
45
47
  self,
@@ -540,9 +542,11 @@ module RubyIndexer
540
542
  )
541
543
  end
542
544
 
543
- sig { params(node: Prism::Node).returns(T::Array[String]) }
545
+ sig { params(node: Prism::Node).returns(T.nilable(String)) }
544
546
  def collect_comments(node)
545
- comments = []
547
+ return unless @collect_comments
548
+
549
+ comments = +""
546
550
 
547
551
  start_line = node.location.start_line - 1
548
552
  start_line -= 1 unless @comments_by_line.key?(start_line)
@@ -551,7 +555,7 @@ module RubyIndexer
551
555
  comment = @comments_by_line[line]
552
556
  break unless comment
553
557
 
554
- comment_content = comment.location.slice.chomp
558
+ comment_content = comment.location.slice
555
559
 
556
560
  # invalid encodings would raise an "invalid byte sequence" exception
557
561
  if !comment_content.valid_encoding? || comment_content.match?(@index.configuration.magic_comment_regex)
@@ -560,9 +564,10 @@ module RubyIndexer
560
564
 
561
565
  comment_content.delete_prefix!("#")
562
566
  comment_content.delete_prefix!(" ")
563
- comments.prepend(comment_content)
567
+ comments.prepend("#{comment_content}\n")
564
568
  end
565
569
 
570
+ comments.chomp!
566
571
  comments
567
572
  end
568
573
 
@@ -685,9 +690,12 @@ module RubyIndexer
685
690
 
686
691
  keyword_rest = parameters_node.keyword_rest
687
692
 
688
- if keyword_rest.is_a?(Prism::KeywordRestParameterNode)
693
+ case keyword_rest
694
+ when Prism::KeywordRestParameterNode
689
695
  keyword_rest_name = parameter_name(keyword_rest) || Entry::KeywordRestParameter::DEFAULT_NAME
690
696
  parameters << Entry::KeywordRestParameter.new(name: keyword_rest_name)
697
+ when Prism::ForwardingParameterNode
698
+ parameters << Entry::ForwardingParameter.new
691
699
  end
692
700
 
693
701
  parameters_node.posts.each do |post|
@@ -24,9 +24,6 @@ module RubyIndexer
24
24
 
25
25
  alias_method :name_location, :location
26
26
 
27
- sig { returns(T::Array[String]) }
28
- attr_reader :comments
29
-
30
27
  sig { returns(Visibility) }
31
28
  attr_accessor :visibility
32
29
 
@@ -35,7 +32,7 @@ module RubyIndexer
35
32
  name: String,
36
33
  file_path: String,
37
34
  location: T.any(Prism::Location, RubyIndexer::Location),
38
- comments: T::Array[String],
35
+ comments: T.nilable(String),
39
36
  ).void
40
37
  end
41
38
  def initialize(name, file_path, location, comments)
@@ -79,6 +76,42 @@ module RubyIndexer
79
76
  File.basename(@file_path)
80
77
  end
81
78
 
79
+ sig { returns(String) }
80
+ def comments
81
+ @comments ||= begin
82
+ # Parse only the comments based on the file path, which is much faster than parsing the entire file
83
+ parsed_comments = Prism.parse_file_comments(@file_path)
84
+
85
+ # Group comments based on whether they belong to a single block of comments
86
+ grouped = parsed_comments.slice_when do |left, right|
87
+ left.location.start_line + 1 != right.location.start_line
88
+ end
89
+
90
+ # Find the group that is either immediately or two lines above the current entry
91
+ correct_group = grouped.find do |group|
92
+ comment_end_line = group.last.location.start_line
93
+ (comment_end_line - 1..comment_end_line).cover?(@location.start_line - 1)
94
+ end
95
+
96
+ # If we found something, we join the comments together. Otherwise, the entry has no documentation and we don't
97
+ # want to accidentally re-parse it, so we set it to an empty string. If an entry is updated, the entire entry
98
+ # object is dropped, so this will not prevent updates
99
+ if correct_group
100
+ correct_group.filter_map do |comment|
101
+ content = comment.slice.chomp
102
+
103
+ if content.valid_encoding?
104
+ content.delete_prefix!("#")
105
+ content.delete_prefix!(" ")
106
+ content
107
+ end
108
+ end.join("\n")
109
+ else
110
+ ""
111
+ end
112
+ end
113
+ end
114
+
82
115
  class ModuleOperation
83
116
  extend T::Sig
84
117
  extend T::Helpers
@@ -116,7 +149,7 @@ module RubyIndexer
116
149
  file_path: String,
117
150
  location: T.any(Prism::Location, RubyIndexer::Location),
118
151
  name_location: T.any(Prism::Location, Location),
119
- comments: T::Array[String],
152
+ comments: T.nilable(String),
120
153
  ).void
121
154
  end
122
155
  def initialize(nesting, file_path, location, name_location, comments)
@@ -177,7 +210,7 @@ module RubyIndexer
177
210
  file_path: String,
178
211
  location: T.any(Prism::Location, RubyIndexer::Location),
179
212
  name_location: T.any(Prism::Location, Location),
180
- comments: T::Array[String],
213
+ comments: T.nilable(String),
181
214
  parent_class: T.nilable(String),
182
215
  ).void
183
216
  end
@@ -195,7 +228,7 @@ module RubyIndexer
195
228
  class SingletonClass < Class
196
229
  extend T::Sig
197
230
 
198
- sig { params(location: Prism::Location, name_location: Prism::Location, comments: T::Array[String]).void }
231
+ sig { params(location: Prism::Location, name_location: Prism::Location, comments: T.nilable(String)).void }
199
232
  def update_singleton_information(location, name_location, comments)
200
233
  # Create a new RubyIndexer::Location object from the Prism location
201
234
  @location = Location.new(
@@ -210,7 +243,7 @@ module RubyIndexer
210
243
  name_location.start_column,
211
244
  name_location.end_column,
212
245
  )
213
- @comments.concat(comments)
246
+ (@comments ||= +"") << comments if comments
214
247
  end
215
248
  end
216
249
 
@@ -302,6 +335,17 @@ module RubyIndexer
302
335
  end
303
336
  end
304
337
 
338
+ # A forwarding method parameter, e.g. `def foo(...)`
339
+ class ForwardingParameter < Parameter
340
+ extend T::Sig
341
+
342
+ sig { void }
343
+ def initialize
344
+ # You can't name a forwarding parameter, it's always called `...`
345
+ super(name: :"...")
346
+ end
347
+ end
348
+
305
349
  class Member < Entry
306
350
  extend T::Sig
307
351
  extend T::Helpers
@@ -311,17 +355,12 @@ module RubyIndexer
311
355
  sig { returns(T.nilable(Entry::Namespace)) }
312
356
  attr_reader :owner
313
357
 
314
- sig { returns(T::Array[RubyIndexer::Entry::Parameter]) }
315
- def parameters
316
- T.must(signatures.first).parameters
317
- end
318
-
319
358
  sig do
320
359
  params(
321
360
  name: String,
322
361
  file_path: String,
323
362
  location: T.any(Prism::Location, RubyIndexer::Location),
324
- comments: T::Array[String],
363
+ comments: T.nilable(String),
325
364
  visibility: Visibility,
326
365
  owner: T.nilable(Entry::Namespace),
327
366
  ).void
@@ -389,7 +428,7 @@ module RubyIndexer
389
428
  file_path: String,
390
429
  location: T.any(Prism::Location, RubyIndexer::Location),
391
430
  name_location: T.any(Prism::Location, Location),
392
- comments: T::Array[String],
431
+ comments: T.nilable(String),
393
432
  signatures: T::Array[Signature],
394
433
  visibility: Visibility,
395
434
  owner: T.nilable(Entry::Namespace),
@@ -440,7 +479,7 @@ module RubyIndexer
440
479
  name: String,
441
480
  file_path: String,
442
481
  location: T.any(Prism::Location, RubyIndexer::Location),
443
- comments: T::Array[String],
482
+ comments: T.nilable(String),
444
483
  ).void
445
484
  end
446
485
  def initialize(target, nesting, name, file_path, location, comments) # rubocop:disable Metrics/ParameterLists
@@ -477,7 +516,7 @@ module RubyIndexer
477
516
  name: String,
478
517
  file_path: String,
479
518
  location: T.any(Prism::Location, RubyIndexer::Location),
480
- comments: T::Array[String],
519
+ comments: T.nilable(String),
481
520
  owner: T.nilable(Entry::Namespace),
482
521
  ).void
483
522
  end
@@ -506,7 +545,7 @@ module RubyIndexer
506
545
  owner: T.nilable(Entry::Namespace),
507
546
  file_path: String,
508
547
  location: T.any(Prism::Location, RubyIndexer::Location),
509
- comments: T::Array[String],
548
+ comments: T.nilable(String),
510
549
  ).void
511
550
  end
512
551
  def initialize(new_name, old_name, owner, file_path, location, comments) # rubocop:disable Metrics/ParameterLists
@@ -530,10 +569,9 @@ module RubyIndexer
530
569
 
531
570
  sig { params(target: T.any(Member, MethodAlias), unresolved_alias: UnresolvedMethodAlias).void }
532
571
  def initialize(target, unresolved_alias)
533
- full_comments = ["Alias for #{target.name}\n"]
534
- full_comments.concat(unresolved_alias.comments)
535
- full_comments << "\n"
536
- full_comments.concat(target.comments)
572
+ full_comments = +"Alias for #{target.name}\n"
573
+ full_comments << "#{unresolved_alias.comments}\n"
574
+ full_comments << target.comments
537
575
 
538
576
  super(
539
577
  unresolved_alias.new_name,
@@ -546,11 +584,6 @@ module RubyIndexer
546
584
  @owner = T.let(unresolved_alias.owner, T.nilable(Entry::Namespace))
547
585
  end
548
586
 
549
- sig { returns(T::Array[Parameter]) }
550
- def parameters
551
- @target.parameters
552
- end
553
-
554
587
  sig { returns(String) }
555
588
  def decorated_parameters
556
589
  @target.decorated_parameters
@@ -586,6 +619,108 @@ module RubyIndexer
586
619
  def format
587
620
  @parameters.map(&:decorated_name).join(", ")
588
621
  end
622
+
623
+ # Returns `true` if the given call node arguments array matches this method signature. This method will prefer
624
+ # returning `true` for situations that cannot be analyzed statically, like the presence of splats, keyword splats
625
+ # or forwarding arguments.
626
+ #
627
+ # Since this method is used to detect which overload should be displayed in signature help, it will also return
628
+ # `true` if there are missing arguments since the user may not be done typing yet. For example:
629
+ #
630
+ # ```ruby
631
+ # def foo(a, b); end
632
+ # # All of the following are considered matches because the user might be in the middle of typing and we have to
633
+ # # show them the signature
634
+ # foo
635
+ # foo(1)
636
+ # foo(1, 2)
637
+ # ```
638
+ sig { params(arguments: T::Array[Prism::Node]).returns(T::Boolean) }
639
+ def matches?(arguments)
640
+ min_pos = 0
641
+ max_pos = T.let(0, T.any(Integer, Float))
642
+ names = []
643
+ has_forward = T.let(false, T::Boolean)
644
+ has_keyword_rest = T.let(false, T::Boolean)
645
+
646
+ @parameters.each do |param|
647
+ case param
648
+ when RequiredParameter
649
+ min_pos += 1
650
+ max_pos += 1
651
+ when OptionalParameter
652
+ max_pos += 1
653
+ when RestParameter
654
+ max_pos = Float::INFINITY
655
+ when ForwardingParameter
656
+ max_pos = Float::INFINITY
657
+ has_forward = true
658
+ when KeywordParameter, OptionalKeywordParameter
659
+ names << param.name
660
+ when KeywordRestParameter
661
+ has_keyword_rest = true
662
+ end
663
+ end
664
+
665
+ keyword_hash_nodes, positional_args = arguments.partition { |arg| arg.is_a?(Prism::KeywordHashNode) }
666
+ keyword_args = T.cast(keyword_hash_nodes.first, T.nilable(Prism::KeywordHashNode))&.elements
667
+ forwarding_arguments, positionals = positional_args.partition do |arg|
668
+ arg.is_a?(Prism::ForwardingArgumentsNode)
669
+ end
670
+
671
+ return true if has_forward && min_pos == 0
672
+
673
+ # If the only argument passed is a forwarding argument, then anything will match
674
+ (positionals.empty? && forwarding_arguments.any?) ||
675
+ (
676
+ # Check if positional arguments match. This includes required, optional, rest arguments. We also need to
677
+ # verify if there's a trailing forwading argument, like `def foo(a, ...); end`
678
+ positional_arguments_match?(positionals, forwarding_arguments, keyword_args, min_pos, max_pos) &&
679
+ # If the positional arguments match, we move on to checking keyword, optional keyword and keyword rest
680
+ # arguments. If there's a forward argument, then it will always match. If the method accepts a keyword rest
681
+ # (**kwargs), then we can't analyze statically because the user could be passing a hash and we don't know
682
+ # what the runtime values inside the hash are.
683
+ #
684
+ # If none of those match, then we verify if the user is passing the expect names for the keyword arguments
685
+ (has_forward || has_keyword_rest || keyword_arguments_match?(keyword_args, names))
686
+ )
687
+ end
688
+
689
+ sig do
690
+ params(
691
+ positional_args: T::Array[Prism::Node],
692
+ forwarding_arguments: T::Array[Prism::Node],
693
+ keyword_args: T.nilable(T::Array[Prism::Node]),
694
+ min_pos: Integer,
695
+ max_pos: T.any(Integer, Float),
696
+ ).returns(T::Boolean)
697
+ end
698
+ def positional_arguments_match?(positional_args, forwarding_arguments, keyword_args, min_pos, max_pos)
699
+ # If the method accepts at least one positional argument and a splat has been passed
700
+ (min_pos > 0 && positional_args.any? { |arg| arg.is_a?(Prism::SplatNode) }) ||
701
+ # If there's at least one positional argument unaccounted for and a keyword splat has been passed
702
+ (min_pos - positional_args.length > 0 && keyword_args&.any? { |arg| arg.is_a?(Prism::AssocSplatNode) }) ||
703
+ # If there's at least one positional argument unaccounted for and a forwarding argument has been passed
704
+ (min_pos - positional_args.length > 0 && forwarding_arguments.any?) ||
705
+ # If the number of positional arguments is within the expected range
706
+ (min_pos > 0 && positional_args.length <= max_pos) ||
707
+ (min_pos == 0 && positional_args.empty?)
708
+ end
709
+
710
+ sig { params(args: T.nilable(T::Array[Prism::Node]), names: T::Array[Symbol]).returns(T::Boolean) }
711
+ def keyword_arguments_match?(args, names)
712
+ return true unless args
713
+ return true if args.any? { |arg| arg.is_a?(Prism::AssocSplatNode) }
714
+
715
+ arg_names = args.filter_map do |arg|
716
+ next unless arg.is_a?(Prism::AssocNode)
717
+
718
+ key = arg.key
719
+ key.value&.to_sym if key.is_a?(Prism::SymbolNode)
720
+ end
721
+
722
+ (arg_names - names).empty?
723
+ end
589
724
  end
590
725
  end
591
726
  end
@@ -312,12 +312,12 @@ module RubyIndexer
312
312
  break unless block.call(progress)
313
313
  end
314
314
 
315
- index_single(path)
315
+ index_single(path, collect_comments: false)
316
316
  end
317
317
  end
318
318
 
319
- sig { params(indexable_path: IndexablePath, source: T.nilable(String)).void }
320
- def index_single(indexable_path, source = nil)
319
+ sig { params(indexable_path: IndexablePath, source: T.nilable(String), collect_comments: T::Boolean).void }
320
+ def index_single(indexable_path, source = nil, collect_comments: true)
321
321
  content = source || File.read(indexable_path.full_path)
322
322
  dispatcher = Prism::Dispatcher.new
323
323
 
@@ -327,6 +327,7 @@ module RubyIndexer
327
327
  dispatcher,
328
328
  result,
329
329
  indexable_path.full_path,
330
+ collect_comments: collect_comments,
330
331
  enhancements: @enhancements,
331
332
  )
332
333
  dispatcher.dispatch(result.value)
@@ -607,7 +608,7 @@ module RubyIndexer
607
608
  attached_ancestor.file_path,
608
609
  attached_ancestor.location,
609
610
  attached_ancestor.name_location,
610
- [],
611
+ nil,
611
612
  nil,
612
613
  )
613
614
  add(singleton, skip_prefix_tree: true)
@@ -272,10 +272,10 @@ module RubyIndexer
272
272
  RBS::AST::Declarations::Constant,
273
273
  RBS::AST::Members::MethodDefinition,
274
274
  RBS::AST::Members::Alias,
275
- )).returns(T::Array[String])
275
+ )).returns(T.nilable(String))
276
276
  end
277
277
  def comments_to_string(declaration)
278
- Array(declaration.comment&.string)
278
+ declaration.comment&.string
279
279
  end
280
280
  end
281
281
  end
@@ -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
@@ -605,5 +605,19 @@ module RubyIndexer
605
605
  assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:2-4:3-7")
606
606
  assert_entry("Qux", Entry::Class, "/fake/path/foo.rb:5-4:6-7")
607
607
  end
608
+
609
+ def test_lazy_comment_fetching_uses_correct_line_breaks_for_rendering
610
+ path = "lib/ruby_lsp/node_context.rb"
611
+ indexable = IndexablePath.new("#{Dir.pwd}/lib", path)
612
+
613
+ @index.index_single(indexable, collect_comments: false)
614
+
615
+ entry = @index["RubyLsp::NodeContext"].first
616
+
617
+ assert_equal(<<~COMMENTS.chomp, entry.comments)
618
+ This class allows listeners to access contextual information about a node in the AST, such as its parent,
619
+ its namespace nesting, and the surrounding CallNode (e.g. a method call).
620
+ COMMENTS
621
+ end
608
622
  end
609
623
  end