ruby-lsp 0.20.1 → 0.21.1

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +18 -3
  5. data/exe/ruby-lsp-launcher +127 -0
  6. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +56 -2
  7. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +21 -6
  8. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +1 -1
  9. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +5 -5
  10. data/lib/ruby_indexer/test/classes_and_modules_test.rb +2 -2
  11. data/lib/ruby_indexer/test/enhancements_test.rb +51 -19
  12. data/lib/ruby_indexer/test/index_test.rb +2 -2
  13. data/lib/ruby_indexer/test/instance_variables_test.rb +1 -1
  14. data/lib/ruby_indexer/test/method_test.rb +26 -0
  15. data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
  16. data/lib/ruby_lsp/addon.rb +9 -2
  17. data/lib/ruby_lsp/base_server.rb +14 -5
  18. data/lib/ruby_lsp/client_capabilities.rb +60 -0
  19. data/lib/ruby_lsp/document.rb +1 -1
  20. data/lib/ruby_lsp/global_state.rb +25 -19
  21. data/lib/ruby_lsp/internal.rb +2 -0
  22. data/lib/ruby_lsp/listeners/completion.rb +62 -0
  23. data/lib/ruby_lsp/listeners/definition.rb +48 -13
  24. data/lib/ruby_lsp/listeners/hover.rb +52 -0
  25. data/lib/ruby_lsp/requests/code_action_resolve.rb +1 -1
  26. data/lib/ruby_lsp/requests/completion.rb +7 -1
  27. data/lib/ruby_lsp/requests/completion_resolve.rb +1 -1
  28. data/lib/ruby_lsp/requests/definition.rb +26 -11
  29. data/lib/ruby_lsp/requests/document_symbol.rb +2 -1
  30. data/lib/ruby_lsp/requests/hover.rb +24 -6
  31. data/lib/ruby_lsp/requests/rename.rb +1 -1
  32. data/lib/ruby_lsp/requests/request.rb +1 -1
  33. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +11 -1
  34. data/lib/ruby_lsp/scripts/compose_bundle.rb +20 -0
  35. data/lib/ruby_lsp/scripts/compose_bundle_windows.rb +8 -0
  36. data/lib/ruby_lsp/server.rb +54 -16
  37. data/lib/ruby_lsp/setup_bundler.rb +111 -22
  38. data/lib/ruby_lsp/utils.rb +8 -0
  39. metadata +8 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58327a9f9a3d85375cbbf81e3e170a948d5c8dfbdcdcac0a1fdef5f685d20c95
4
- data.tar.gz: 45572eb6645bce73d2079bed65f0d92c98921517926b968bba96788769a3aada
3
+ metadata.gz: c270ba4b6a7348ccb821e1bae776f4bd9ac704973b251791d2104d79861878c1
4
+ data.tar.gz: 5d27d76eca727dca7e439aa177db04fb3ec17e1169c19a267ba2cffc8f9e19a3
5
5
  SHA512:
6
- metadata.gz: c506eeff4d24060e3afdb4ad9da319c12b725050c8f25a9348ec7dea9082f8adced7671b7b1eb8f7f09412942139e47a2df5059b5672d2b6a75f0ffc5a59885d
7
- data.tar.gz: d9a89ce24946e5f9cc47b5a48086f6fd977176d31332a0b634950dc00a41fc9a36e4753eae2896e2da0129e92bff4278265eaa970252a5292c783e5bc82258e9
6
+ metadata.gz: 8502d09966be4e7ec79c276b4d078edc0f4e150c86ff24bb6e6f5b9c28e4f89393760983b7344c108e356a5bc79daa314806c69b81441338465b00f8bb693cc1
7
+ data.tar.gz: 0d84c86826022c8e71311bef8070f8b5364cd45f10f242fcb5b8cf96f9dd391a9a45a034b264ee8b2cec236ce69257a1a102d9e769bbdf3b8c0dc9baff8650e9
data/README.md CHANGED
@@ -29,8 +29,8 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopif
29
29
  be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor
30
30
  Covenant](CODE_OF_CONDUCT.md) code of conduct.
31
31
 
32
- If you wish to contribute, see [CONTRIBUTING](CONTRIBUTING.md) for development instructions and check out our pinned
33
- [roadmap issue](https://github.com/Shopify/ruby-lsp/issues) for a list of tasks to get started.
32
+ If you wish to contribute, see [Contributing](https://shopify.github.io/ruby-lsp/contributing.html) for development instructions and check out our
33
+ [Design and roadmap](https://shopify.github.io/ruby-lsp/design-and-roadmap.html) for a list of tasks to get started.
34
34
 
35
35
  ## License
36
36
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.20.1
1
+ 0.21.1
data/exe/ruby-lsp CHANGED
@@ -33,6 +33,10 @@ parser = OptionParser.new do |opts|
33
33
  options[:doctor] = true
34
34
  end
35
35
 
36
+ opts.on("--use-launcher", "[EXPERIMENTAL] Use launcher mechanism to handle missing dependencies gracefully") do
37
+ options[:launcher] = true
38
+ end
39
+
36
40
  opts.on("-h", "--help", "Print this help") do
37
41
  puts opts.help
38
42
  puts
@@ -54,6 +58,17 @@ end
54
58
  # using `BUNDLE_GEMFILE=.ruby-lsp/Gemfile bundle exec ruby-lsp` so that we have access to the gems that are a part of
55
59
  # the application's bundle
56
60
  if ENV["BUNDLE_GEMFILE"].nil?
61
+ # Substitute the current process by the launcher. RubyGems activates all dependencies of a gem's executable eagerly,
62
+ # but we can't have that happen because we want to invoke Bundler.setup ourselves with the composed bundle and avoid
63
+ # duplicate spec activation errors. Replacing the process with the launcher executable will clear the activated specs,
64
+ # which gives us the opportunity to control which specs are activated and enter degraded mode if any gems failed to
65
+ # install rather than failing to boot the server completely
66
+ if options[:launcher]
67
+ command = +File.expand_path("ruby-lsp-launcher", __dir__)
68
+ command << " --debug" if options[:debug]
69
+ exit exec(command)
70
+ end
71
+
57
72
  require_relative "../lib/ruby_lsp/setup_bundler"
58
73
 
59
74
  begin
@@ -69,12 +84,12 @@ if ENV["BUNDLE_GEMFILE"].nil?
69
84
  "bundle"
70
85
  end
71
86
 
72
- exit exec(env, "#{base_bundle} exec ruby-lsp #{original_args.join(" ")}")
87
+ exit exec(env, "#{base_bundle} exec ruby-lsp #{original_args.join(" ")}".strip)
73
88
  end
74
89
 
75
- require "ruby_lsp/load_sorbet"
76
-
77
90
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
91
+
92
+ require "ruby_lsp/load_sorbet"
78
93
  require "ruby_lsp/internal"
79
94
 
80
95
  T::Utils.run_all_sig_blocks
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # !!!!!!!
5
+ # No gems can be required in this file until we invoke bundler setup except inside the forked process that sets up the
6
+ # composed bundle
7
+ # !!!!!!!
8
+
9
+ setup_error = nil
10
+
11
+ # Read the initialize request before even starting the server. We need to do this to figure out the workspace URI.
12
+ # Editors are not required to spawn the language server process on the same directory as the workspace URI, so we need
13
+ # to ensure that we're setting up the bundle in the right place
14
+ $stdin.binmode
15
+ headers = $stdin.gets("\r\n\r\n")
16
+ content_length = headers[/Content-Length: (\d+)/i, 1].to_i
17
+ raw_initialize = $stdin.read(content_length)
18
+
19
+ # Compose the Ruby LSP bundle in a forked process so that we can require gems without polluting the main process
20
+ # `$LOAD_PATH` and `Gem.loaded_specs`. Windows doesn't support forking, so we need a separate path to support it
21
+ pid = if Gem.win_platform?
22
+ # Since we can't fork on Windows and spawn won't carry over the existing load paths, we need to explicitly pass that
23
+ # down to the child process or else requiring gems during composing the bundle will fail
24
+ load_path = $LOAD_PATH.flat_map do |path|
25
+ ["-I", File.expand_path(path)]
26
+ end
27
+
28
+ Process.spawn(
29
+ Gem.ruby,
30
+ *load_path,
31
+ File.expand_path("../lib/ruby_lsp/scripts/compose_bundle_windows.rb", __dir__),
32
+ raw_initialize,
33
+ )
34
+ else
35
+ fork do
36
+ require_relative "../lib/ruby_lsp/scripts/compose_bundle"
37
+ compose(raw_initialize)
38
+ end
39
+ end
40
+
41
+ begin
42
+ # Wait until the composed Bundle is finished
43
+ Process.wait(pid)
44
+ rescue Errno::ECHILD
45
+ # In theory, the child process can finish before we even get to the wait call, but that is not an error
46
+ end
47
+
48
+ begin
49
+ bundle_env_path = File.join(".ruby-lsp", "bundle_env")
50
+ # We can't require `bundler/setup` because that file prematurely exits the process if setup fails. However, we can't
51
+ # simply require bundler either because the version required might conflict with the one locked in the composed
52
+ # bundle. We need the composed bundle sub-process to inform us of the locked Bundler version, so that we can then
53
+ # activate the right spec and require the exact Bundler version required by the app
54
+ if File.exist?(bundle_env_path)
55
+ env = File.readlines(bundle_env_path).to_h { |line| line.chomp.split("=", 2) }
56
+ ENV.merge!(env)
57
+
58
+ if env["BUNDLER_VERSION"]
59
+ Gem::Specification.find_by_name("bundler", env["BUNDLER_VERSION"]).activate
60
+ end
61
+
62
+ require "bundler"
63
+ Bundler.ui.level = :silent
64
+ Bundler.setup
65
+ $stderr.puts("Composed Bundle set up successfully")
66
+ end
67
+ rescue StandardError => e
68
+ # If installing gems failed for any reason, we don't want to exit the process prematurely. We can still provide most
69
+ # features in a degraded mode. We simply save the error so that we can report to the user that certain gems might be
70
+ # missing, but we respect the LSP life cycle
71
+ setup_error = e
72
+ $stderr.puts("Failed to set up composed Bundle\n#{e.full_message}")
73
+
74
+ # If Bundler.setup fails, we need to restore the original $LOAD_PATH so that we can still require the Ruby LSP server
75
+ # in degraded mode
76
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
77
+ ensure
78
+ require "fileutils"
79
+ FileUtils.rm(bundle_env_path) if File.exist?(bundle_env_path)
80
+ end
81
+
82
+ error_path = File.join(".ruby-lsp", "install_error")
83
+
84
+ install_error = if File.exist?(error_path)
85
+ Marshal.load(File.read(error_path))
86
+ end
87
+
88
+ # Now that the bundle is set up, we can begin actually launching the server. Note that `Bundler.setup` will have already
89
+ # configured the load path using the version of the Ruby LSP present in the composed bundle. Do not push any Ruby LSP
90
+ # paths into the load path manually or we may end up requiring the wrong version of the gem
91
+ require "ruby_lsp/load_sorbet"
92
+ require "ruby_lsp/internal"
93
+
94
+ T::Utils.run_all_sig_blocks
95
+
96
+ if ARGV.include?("--debug")
97
+ if ["x64-mingw-ucrt", "x64-mingw32"].include?(RUBY_PLATFORM)
98
+ $stderr.puts "Debugging is not supported on Windows"
99
+ else
100
+ begin
101
+ ENV.delete("RUBY_DEBUG_IRB_CONSOLE")
102
+ require "debug/open_nonstop"
103
+ rescue LoadError
104
+ $stderr.puts("You need to install the debug gem to use the --debug flag")
105
+ end
106
+ end
107
+ end
108
+
109
+ # Ensure all output goes out stderr by default to allow puts/p/pp to work without specifying output device.
110
+ $> = $stderr
111
+
112
+ initialize_request = JSON.parse(raw_initialize, symbolize_names: true) if raw_initialize
113
+
114
+ begin
115
+ RubyLsp::Server.new(
116
+ install_error: install_error,
117
+ setup_error: setup_error,
118
+ initialize_request: initialize_request,
119
+ ).start
120
+ rescue ArgumentError
121
+ # If the launcher is booting an outdated version of the server, then the initializer doesn't accept a keyword splat
122
+ # and we already read the initialize request from the stdin pipe. In this case, we need to process the initialize
123
+ # request manually and then start the main loop
124
+ server = RubyLsp::Server.new
125
+ server.process_message(initialize_request)
126
+ server.start
127
+ end
@@ -312,12 +312,16 @@ module RubyIndexer
312
312
  @visibility_stack.push(Entry::Visibility::PROTECTED)
313
313
  when :private
314
314
  @visibility_stack.push(Entry::Visibility::PRIVATE)
315
+ when :module_function
316
+ handle_module_function(node)
315
317
  end
316
318
 
317
319
  @enhancements.each do |enhancement|
318
- enhancement.on_call_node(@index, @owner_stack.last, node, @file_path, @code_units_cache)
320
+ enhancement.on_call_node_enter(@owner_stack.last, node, @file_path, @code_units_cache)
319
321
  rescue StandardError => e
320
- @indexing_errors << "Indexing error in #{@file_path} with '#{enhancement.class.name}' enhancement: #{e.message}"
322
+ @indexing_errors << <<~MSG
323
+ Indexing error in #{@file_path} with '#{enhancement.class.name}' on call node enter enhancement: #{e.message}
324
+ MSG
321
325
  end
322
326
  end
323
327
 
@@ -332,6 +336,14 @@ module RubyIndexer
332
336
  @visibility_stack.pop
333
337
  end
334
338
  end
339
+
340
+ @enhancements.each do |enhancement|
341
+ enhancement.on_call_node_leave(@owner_stack.last, node, @file_path, @code_units_cache)
342
+ rescue StandardError => e
343
+ @indexing_errors << <<~MSG
344
+ Indexing error in #{@file_path} with '#{enhancement.class.name}' on call node leave enhancement: #{e.message}
345
+ MSG
346
+ end
335
347
  end
336
348
 
337
349
  sig { params(node: Prism::DefNode).void }
@@ -751,6 +763,48 @@ module RubyIndexer
751
763
  end
752
764
  end
753
765
 
766
+ sig { params(node: Prism::CallNode).void }
767
+ def handle_module_function(node)
768
+ arguments_node = node.arguments
769
+ return unless arguments_node
770
+
771
+ owner_name = @owner_stack.last&.name
772
+ return unless owner_name
773
+
774
+ arguments_node.arguments.each do |argument|
775
+ method_name = case argument
776
+ when Prism::StringNode
777
+ argument.content
778
+ when Prism::SymbolNode
779
+ argument.value
780
+ end
781
+ next unless method_name
782
+
783
+ entries = @index.resolve_method(method_name, owner_name)
784
+ next unless entries
785
+
786
+ entries.each do |entry|
787
+ entry_owner_name = entry.owner&.name
788
+ next unless entry_owner_name
789
+
790
+ entry.visibility = Entry::Visibility::PRIVATE
791
+
792
+ singleton = @index.existing_or_new_singleton_class(entry_owner_name)
793
+ location = Location.from_prism_location(argument.location, @code_units_cache)
794
+ @index.add(Entry::Method.new(
795
+ method_name,
796
+ @file_path,
797
+ location,
798
+ location,
799
+ collect_comments(node)&.concat(entry.comments),
800
+ entry.signatures,
801
+ Entry::Visibility::PUBLIC,
802
+ singleton,
803
+ ))
804
+ end
805
+ end
806
+ end
807
+
754
808
  sig { returns(Entry::Visibility) }
755
809
  def current_visibility
756
810
  T.must(@visibility_stack.last)
@@ -2,20 +2,35 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module RubyIndexer
5
- module Enhancement
5
+ class Enhancement
6
6
  extend T::Sig
7
7
  extend T::Helpers
8
8
 
9
- interface!
9
+ abstract!
10
10
 
11
- requires_ancestor { Object }
11
+ sig { params(index: Index).void }
12
+ def initialize(index)
13
+ @index = index
14
+ end
12
15
 
13
16
  # The `on_extend` indexing enhancement is invoked whenever an extend is encountered in the code. It can be used to
14
17
  # register for an included callback, similar to what `ActiveSupport::Concern` does in order to auto-extend the
15
18
  # `ClassMethods` modules
16
19
  sig do
17
- abstract.params(
18
- index: Index,
20
+ overridable.params(
21
+ owner: T.nilable(Entry::Namespace),
22
+ node: Prism::CallNode,
23
+ file_path: String,
24
+ code_units_cache: T.any(
25
+ T.proc.params(arg0: Integer).returns(Integer),
26
+ Prism::CodeUnitsCache,
27
+ ),
28
+ ).void
29
+ end
30
+ def on_call_node_enter(owner, node, file_path, code_units_cache); end
31
+
32
+ sig do
33
+ overridable.params(
19
34
  owner: T.nilable(Entry::Namespace),
20
35
  node: Prism::CallNode,
21
36
  file_path: String,
@@ -25,6 +40,6 @@ module RubyIndexer
25
40
  ),
26
41
  ).void
27
42
  end
28
- def on_call_node(index, owner, node, file_path, code_units_cache); end
43
+ def on_call_node_leave(owner, node, file_path, code_units_cache); end
29
44
  end
30
45
  end
@@ -646,7 +646,7 @@ module RubyIndexer
646
646
  (positionals.empty? && forwarding_arguments.any?) ||
647
647
  (
648
648
  # Check if positional arguments match. This includes required, optional, rest arguments. We also need to
649
- # verify if there's a trailing forwading argument, like `def foo(a, ...); end`
649
+ # verify if there's a trailing forwarding argument, like `def foo(a, ...); end`
650
650
  positional_arguments_match?(positionals, forwarding_arguments, keyword_args, min_pos, max_pos) &&
651
651
  # If the positional arguments match, we move on to checking keyword, optional keyword and keyword rest
652
652
  # arguments. If there's a forward argument, then it will always match. If the method accepts a keyword rest
@@ -784,7 +784,7 @@ module RubyIndexer
784
784
  singleton_levels
785
785
  )
786
786
  # Find the first class entry that has a parent class. Notice that if the developer makes a mistake and inherits
787
- # from two diffent classes in different files, we simply ignore it
787
+ # from two different classes in different files, we simply ignore it
788
788
  superclass = T.cast(
789
789
  if singleton_levels > 0
790
790
  self[attached_class_name]&.find { |n| n.is_a?(Entry::Class) && n.parent_class }
@@ -974,10 +974,10 @@ module RubyIndexer
974
974
  []
975
975
  end
976
976
 
977
- # Removes redudancy from a constant reference's full name. For example, if we find a reference to `A::B::Foo` inside
978
- # of the ["A", "B"] nesting, then we should not concatenate the nesting with the name or else we'll end up with
979
- # `A::B::A::B::Foo`. This method will remove any redundant parts from the final name based on the reference and the
980
- # nesting
977
+ # Removes redundancy from a constant reference's full name. For example, if we find a reference to `A::B::Foo`
978
+ # inside of the ["A", "B"] nesting, then we should not concatenate the nesting with the name or else we'll end up
979
+ # with `A::B::A::B::Foo`. This method will remove any redundant parts from the final name based on the reference and
980
+ # the nesting
981
981
  sig { params(name: String, nesting: T::Array[String]).returns(String) }
982
982
  def build_non_redundant_full_name(name, nesting)
983
983
  # If there's no nesting, then we can just return the name as is
@@ -72,7 +72,7 @@ module RubyIndexer
72
72
  assert_entry("self::Bar", Entry::Class, "/fake/path/foo.rb:0-0:1-3")
73
73
  end
74
74
 
75
- def test_dynamically_namespaced_class_doesnt_affect_other_classes
75
+ def test_dynamically_namespaced_class_does_not_affect_other_classes
76
76
  index(<<~RUBY)
77
77
  class Foo
78
78
  class self::Bar
@@ -143,7 +143,7 @@ module RubyIndexer
143
143
  assert_entry("self::Bar", Entry::Module, "/fake/path/foo.rb:0-0:1-3")
144
144
  end
145
145
 
146
- def test_dynamically_namespaced_module_doesnt_affect_other_modules
146
+ def test_dynamically_namespaced_module_does_not_affect_other_modules
147
147
  index(<<~RUBY)
148
148
  module Foo
149
149
  class self::Bar
@@ -6,10 +6,8 @@ require_relative "test_case"
6
6
  module RubyIndexer
7
7
  class EnhancementTest < TestCase
8
8
  def test_enhancing_indexing_included_hook
9
- enhancement_class = Class.new do
10
- include Enhancement
11
-
12
- def on_call_node(index, owner, node, file_path, code_units_cache)
9
+ enhancement_class = Class.new(Enhancement) do
10
+ def on_call_node_enter(owner, node, file_path, code_units_cache)
13
11
  return unless owner
14
12
  return unless node.name == :extend
15
13
 
@@ -24,7 +22,7 @@ module RubyIndexer
24
22
  module_name = node.full_name
25
23
  next unless module_name == "ActiveSupport::Concern"
26
24
 
27
- index.register_included_hook(owner.name) do |index, base|
25
+ @index.register_included_hook(owner.name) do |index, base|
28
26
  class_methods_name = "#{owner.name}::ClassMethods"
29
27
 
30
28
  if index.indexed?(class_methods_name)
@@ -33,7 +31,7 @@ module RubyIndexer
33
31
  end
34
32
  end
35
33
 
36
- index.add(Entry::Method.new(
34
+ @index.add(Entry::Method.new(
37
35
  "new_method",
38
36
  file_path,
39
37
  location,
@@ -50,7 +48,7 @@ module RubyIndexer
50
48
  end
51
49
  end
52
50
 
53
- @index.register_enhancement(enhancement_class.new)
51
+ @index.register_enhancement(enhancement_class.new(@index))
54
52
  index(<<~RUBY)
55
53
  module ActiveSupport
56
54
  module Concern
@@ -98,10 +96,8 @@ module RubyIndexer
98
96
  end
99
97
 
100
98
  def test_enhancing_indexing_configuration_dsl
101
- enhancement_class = Class.new do
102
- include Enhancement
103
-
104
- def on_call_node(index, owner, node, file_path, code_units_cache)
99
+ enhancement_class = Class.new(Enhancement) do
100
+ def on_call_node_enter(owner, node, file_path, code_units_cache)
105
101
  return unless owner
106
102
 
107
103
  name = node.name
@@ -115,7 +111,7 @@ module RubyIndexer
115
111
 
116
112
  location = Location.from_prism_location(association_name.location, code_units_cache)
117
113
 
118
- index.add(Entry::Method.new(
114
+ @index.add(Entry::Method.new(
119
115
  T.must(association_name.value),
120
116
  file_path,
121
117
  location,
@@ -128,7 +124,7 @@ module RubyIndexer
128
124
  end
129
125
  end
130
126
 
131
- @index.register_enhancement(enhancement_class.new)
127
+ @index.register_enhancement(enhancement_class.new(@index))
132
128
  index(<<~RUBY)
133
129
  module ActiveSupport
134
130
  module Concern
@@ -160,11 +156,44 @@ module RubyIndexer
160
156
  assert_entry("posts", Entry::Method, "/fake/path/foo.rb:23-11:23-17")
161
157
  end
162
158
 
163
- def test_error_handling_in_enhancement
164
- enhancement_class = Class.new do
165
- include Enhancement
159
+ def test_error_handling_in_on_call_node_enter_enhancement
160
+ enhancement_class = Class.new(Enhancement) do
161
+ def on_call_node_enter(owner, node, file_path, code_units_cache)
162
+ raise "Error"
163
+ end
164
+
165
+ class << self
166
+ def name
167
+ "TestEnhancement"
168
+ end
169
+ end
170
+ end
171
+
172
+ @index.register_enhancement(enhancement_class.new(@index))
173
+
174
+ _stdout, stderr = capture_io do
175
+ index(<<~RUBY)
176
+ module ActiveSupport
177
+ module Concern
178
+ def self.extended(base)
179
+ base.class_eval("def new_method(a); end")
180
+ end
181
+ end
182
+ end
183
+ RUBY
184
+ end
185
+
186
+ assert_match(
187
+ %r{Indexing error in /fake/path/foo\.rb with 'TestEnhancement' on call node enter enhancement},
188
+ stderr,
189
+ )
190
+ # The module should still be indexed
191
+ assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5")
192
+ end
166
193
 
167
- def on_call_node(index, owner, node, file_path, code_units_cache)
194
+ def test_error_handling_in_on_call_node_leave_enhancement
195
+ enhancement_class = Class.new(Enhancement) do
196
+ def on_call_node_leave(owner, node, file_path, code_units_cache)
168
197
  raise "Error"
169
198
  end
170
199
 
@@ -175,7 +204,7 @@ module RubyIndexer
175
204
  end
176
205
  end
177
206
 
178
- @index.register_enhancement(enhancement_class.new)
207
+ @index.register_enhancement(enhancement_class.new(@index))
179
208
 
180
209
  _stdout, stderr = capture_io do
181
210
  index(<<~RUBY)
@@ -189,7 +218,10 @@ module RubyIndexer
189
218
  RUBY
190
219
  end
191
220
 
192
- assert_match(%r{Indexing error in /fake/path/foo\.rb with 'TestEnhancement' enhancement}, stderr)
221
+ assert_match(
222
+ %r{Indexing error in /fake/path/foo\.rb with 'TestEnhancement' on call node leave enhancement},
223
+ stderr,
224
+ )
193
225
  # The module should still be indexed
194
226
  assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5")
195
227
  end
@@ -904,7 +904,7 @@ module RubyIndexer
904
904
  assert_equal(14, entry.location.start_line)
905
905
  end
906
906
 
907
- def test_resolving_inherited_alised_namespace
907
+ def test_resolving_inherited_aliased_namespace
908
908
  index(<<~RUBY)
909
909
  module Bar
910
910
  TARGET = 123
@@ -1490,7 +1490,7 @@ module RubyIndexer
1490
1490
  assert_kind_of(Entry::UnresolvedMethodAlias, entry)
1491
1491
  end
1492
1492
 
1493
- def test_unresolable_method_aliases
1493
+ def test_unresolvable_method_aliases
1494
1494
  index(<<~RUBY)
1495
1495
  class Foo
1496
1496
  alias bar baz
@@ -209,7 +209,7 @@ module RubyIndexer
209
209
  end
210
210
  RUBY
211
211
 
212
- # If the surrounding method is beind defined on any dynamic value that isn't `self`, then we attribute the
212
+ # If the surrounding method is being defined on any dynamic value that isn't `self`, then we attribute the
213
213
  # instance variable to the wrong owner since there's no way to understand that statically
214
214
  entry = T.must(@index["@a"]&.first)
215
215
  owner = T.must(entry.owner)
@@ -123,6 +123,32 @@ module RubyIndexer
123
123
  assert_entry("baz", Entry::Method, "/fake/path/foo.rb:9-2:9-14", visibility: Entry::Visibility::PRIVATE)
124
124
  end
125
125
 
126
+ def test_visibility_tracking_with_module_function
127
+ index(<<~RUBY)
128
+ module Test
129
+ def foo; end
130
+ def bar; end
131
+ module_function :foo, "bar"
132
+ end
133
+ RUBY
134
+
135
+ ["foo", "bar"].each do |keyword|
136
+ entries = T.must(@index[keyword])
137
+ # should receive two entries because module_function creates a singleton method
138
+ # for the Test module and a private method for classes include the Test module
139
+ assert_equal(entries.size, 2)
140
+ first_entry, second_entry = *entries
141
+ # The first entry points to the location of the module_function call
142
+ assert_equal("Test", first_entry.owner.name)
143
+ assert_instance_of(Entry::Module, first_entry.owner)
144
+ assert_equal(Entry::Visibility::PRIVATE, first_entry.visibility)
145
+ # The second entry points to the public singleton method
146
+ assert_equal("Test::<Class:Test>", second_entry.owner.name)
147
+ assert_instance_of(Entry::SingletonClass, second_entry.owner)
148
+ assert_equal(Entry::Visibility::PUBLIC, second_entry.visibility)
149
+ end
150
+ end
151
+
126
152
  def test_method_with_parameters
127
153
  index(<<~RUBY)
128
154
  class Foo
@@ -100,7 +100,7 @@ module RubyIndexer
100
100
  end
101
101
 
102
102
  def test_location_and_name_location_are_the_same
103
- # NOTE: RBS does not store the name location for classes, modules or methods. This behaviour is not exactly what
103
+ # NOTE: RBS does not store the name location for classes, modules or methods. This behavior is not exactly what
104
104
  # we would like, but for now we assign the same location to both
105
105
 
106
106
  entries = @index["Array"]
@@ -46,7 +46,7 @@ module RubyLsp
46
46
  sig { returns(T::Array[T.class_of(Addon)]) }
47
47
  attr_reader :addon_classes
48
48
 
49
- # Automatically track and instantiate addon classes
49
+ # Automatically track and instantiate add-on classes
50
50
  sig { params(child_class: T.class_of(Addon)).void }
51
51
  def inherited(child_class)
52
52
  addon_classes << child_class
@@ -82,7 +82,7 @@ module RubyLsp
82
82
  e
83
83
  end
84
84
 
85
- # Instantiate all discovered addon classes
85
+ # Instantiate all discovered add-on classes
86
86
  self.addons = addon_classes.map(&:new)
87
87
  self.file_watcher_addons = addons.select { |addon| addon.respond_to?(:workspace_did_change_watched_files) }
88
88
 
@@ -194,6 +194,13 @@ module RubyLsp
194
194
  sig { abstract.returns(String) }
195
195
  def version; end
196
196
 
197
+ # Handle a response from a window/showMessageRequest request. Add-ons must include the addon_name as part of the
198
+ # original request so that the response is delegated to the correct add-on and must override this method to handle
199
+ # the response
200
+ # https://microsoft.github.io/language-server-protocol/specification#window_showMessageRequest
201
+ sig { overridable.params(title: String).void }
202
+ def handle_window_show_message_response(title); end
203
+
197
204
  # Creates a new CodeLens listener. This method is invoked on every CodeLens request
198
205
  sig do
199
206
  overridable.params(