ruby-lsp 0.16.7 → 0.17.3

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/VERSION +1 -1
  4. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +195 -18
  5. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +129 -12
  6. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +333 -44
  7. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +99 -0
  8. data/lib/ruby_indexer/ruby_indexer.rb +1 -0
  9. data/lib/ruby_indexer/test/classes_and_modules_test.rb +58 -11
  10. data/lib/ruby_indexer/test/configuration_test.rb +5 -6
  11. data/lib/ruby_indexer/test/constant_test.rb +9 -9
  12. data/lib/ruby_indexer/test/index_test.rb +867 -7
  13. data/lib/ruby_indexer/test/instance_variables_test.rb +131 -0
  14. data/lib/ruby_indexer/test/method_test.rb +57 -0
  15. data/lib/ruby_indexer/test/rbs_indexer_test.rb +42 -0
  16. data/lib/ruby_indexer/test/test_case.rb +10 -1
  17. data/lib/ruby_lsp/addon.rb +8 -8
  18. data/lib/ruby_lsp/document.rb +26 -3
  19. data/lib/ruby_lsp/global_state.rb +37 -16
  20. data/lib/ruby_lsp/internal.rb +2 -0
  21. data/lib/ruby_lsp/listeners/code_lens.rb +2 -2
  22. data/lib/ruby_lsp/listeners/completion.rb +74 -17
  23. data/lib/ruby_lsp/listeners/definition.rb +82 -24
  24. data/lib/ruby_lsp/listeners/hover.rb +62 -6
  25. data/lib/ruby_lsp/listeners/signature_help.rb +4 -4
  26. data/lib/ruby_lsp/node_context.rb +39 -0
  27. data/lib/ruby_lsp/requests/code_action_resolve.rb +73 -2
  28. data/lib/ruby_lsp/requests/code_actions.rb +16 -15
  29. data/lib/ruby_lsp/requests/completion.rb +21 -13
  30. data/lib/ruby_lsp/requests/completion_resolve.rb +9 -0
  31. data/lib/ruby_lsp/requests/definition.rb +25 -5
  32. data/lib/ruby_lsp/requests/document_highlight.rb +2 -2
  33. data/lib/ruby_lsp/requests/hover.rb +5 -6
  34. data/lib/ruby_lsp/requests/on_type_formatting.rb +8 -4
  35. data/lib/ruby_lsp/requests/signature_help.rb +3 -3
  36. data/lib/ruby_lsp/requests/support/common.rb +4 -3
  37. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +23 -6
  38. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +5 -1
  39. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +4 -0
  40. data/lib/ruby_lsp/requests/workspace_symbol.rb +7 -4
  41. data/lib/ruby_lsp/server.rb +22 -5
  42. data/lib/ruby_lsp/test_helper.rb +1 -0
  43. metadata +29 -5
@@ -0,0 +1,131 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "test_case"
5
+
6
+ module RubyIndexer
7
+ class InstanceVariableTest < TestCase
8
+ def test_instance_variable_write
9
+ index(<<~RUBY)
10
+ module Foo
11
+ class Bar
12
+ def initialize
13
+ # Hello
14
+ @a = 1
15
+ end
16
+ end
17
+ end
18
+ RUBY
19
+
20
+ assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8")
21
+
22
+ entry = T.must(@index["@a"]&.first)
23
+ assert_equal("Foo::Bar", T.must(entry.owner).name)
24
+ end
25
+
26
+ def test_instance_variable_and_write
27
+ index(<<~RUBY)
28
+ module Foo
29
+ class Bar
30
+ def initialize
31
+ # Hello
32
+ @a &&= value
33
+ end
34
+ end
35
+ end
36
+ RUBY
37
+
38
+ assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8")
39
+
40
+ entry = T.must(@index["@a"]&.first)
41
+ assert_equal("Foo::Bar", T.must(entry.owner).name)
42
+ end
43
+
44
+ def test_instance_variable_operator_write
45
+ index(<<~RUBY)
46
+ module Foo
47
+ class Bar
48
+ def initialize
49
+ # Hello
50
+ @a += value
51
+ end
52
+ end
53
+ end
54
+ RUBY
55
+
56
+ assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8")
57
+
58
+ entry = T.must(@index["@a"]&.first)
59
+ assert_equal("Foo::Bar", T.must(entry.owner).name)
60
+ end
61
+
62
+ def test_instance_variable_or_write
63
+ index(<<~RUBY)
64
+ module Foo
65
+ class Bar
66
+ def initialize
67
+ # Hello
68
+ @a ||= value
69
+ end
70
+ end
71
+ end
72
+ RUBY
73
+
74
+ assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8")
75
+
76
+ entry = T.must(@index["@a"]&.first)
77
+ assert_equal("Foo::Bar", T.must(entry.owner).name)
78
+ end
79
+
80
+ def test_instance_variable_target
81
+ index(<<~RUBY)
82
+ module Foo
83
+ class Bar
84
+ def initialize
85
+ # Hello
86
+ @a, @b = [1, 2]
87
+ end
88
+ end
89
+ end
90
+ RUBY
91
+
92
+ assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8")
93
+ assert_entry("@b", Entry::InstanceVariable, "/fake/path/foo.rb:4-10:4-12")
94
+
95
+ entry = T.must(@index["@a"]&.first)
96
+ assert_equal("Foo::Bar", T.must(entry.owner).name)
97
+
98
+ entry = T.must(@index["@b"]&.first)
99
+ assert_equal("Foo::Bar", T.must(entry.owner).name)
100
+ end
101
+
102
+ def test_empty_name_instance_variables
103
+ index(<<~RUBY)
104
+ module Foo
105
+ class Bar
106
+ def initialize
107
+ @ = 123
108
+ end
109
+ end
110
+ end
111
+ RUBY
112
+
113
+ refute_entry("@")
114
+ end
115
+
116
+ def test_class_instance_variables
117
+ index(<<~RUBY)
118
+ module Foo
119
+ class Bar
120
+ @a = 123
121
+ end
122
+ end
123
+ RUBY
124
+
125
+ assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:2-4:2-6")
126
+
127
+ entry = T.must(@index["@a"]&.first)
128
+ assert_equal("Foo::Bar", T.must(entry.owner).name)
129
+ end
130
+ end
131
+ end
@@ -71,6 +71,43 @@ module RubyIndexer
71
71
  assert_equal("Bar", second_entry.owner.name)
72
72
  end
73
73
 
74
+ def test_visibility_tracking
75
+ index(<<~RUBY)
76
+ private def foo
77
+ end
78
+
79
+ def bar; end
80
+
81
+ protected
82
+
83
+ def baz; end
84
+ RUBY
85
+
86
+ assert_entry("foo", Entry::InstanceMethod, "/fake/path/foo.rb:0-8:1-3", visibility: Entry::Visibility::PRIVATE)
87
+ assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:3-0:3-12", visibility: Entry::Visibility::PUBLIC)
88
+ assert_entry("baz", Entry::InstanceMethod, "/fake/path/foo.rb:7-0:7-12", visibility: Entry::Visibility::PROTECTED)
89
+ end
90
+
91
+ def test_visibility_tracking_with_nested_class_or_modules
92
+ index(<<~RUBY)
93
+ class Foo
94
+ private
95
+
96
+ def foo; end
97
+
98
+ class Bar
99
+ def bar; end
100
+ end
101
+
102
+ def baz; end
103
+ end
104
+ RUBY
105
+
106
+ assert_entry("foo", Entry::InstanceMethod, "/fake/path/foo.rb:3-2:3-14", visibility: Entry::Visibility::PRIVATE)
107
+ assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:6-4:6-16", visibility: Entry::Visibility::PUBLIC)
108
+ assert_entry("baz", Entry::InstanceMethod, "/fake/path/foo.rb:9-2:9-14", visibility: Entry::Visibility::PRIVATE)
109
+ end
110
+
74
111
  def test_method_with_parameters
75
112
  index(<<~RUBY)
76
113
  class Foo
@@ -341,5 +378,25 @@ module RubyIndexer
341
378
  entry = T.must(@index["third"]&.first)
342
379
  assert_equal("Foo", T.must(entry.owner).name)
343
380
  end
381
+
382
+ def test_keeps_track_of_aliases
383
+ index(<<~RUBY)
384
+ class Foo
385
+ alias whatever to_s
386
+ alias_method :foo, :to_a
387
+ alias_method "bar", "to_a"
388
+
389
+ # These two are not indexed because they are dynamic or incomplete
390
+ alias_method baz, :to_a
391
+ alias_method :baz
392
+ end
393
+ RUBY
394
+
395
+ assert_entry("whatever", Entry::UnresolvedMethodAlias, "/fake/path/foo.rb:1-8:1-16")
396
+ assert_entry("foo", Entry::UnresolvedMethodAlias, "/fake/path/foo.rb:2-15:2-19")
397
+ assert_entry("bar", Entry::UnresolvedMethodAlias, "/fake/path/foo.rb:3-15:3-20")
398
+ # Foo plus 3 valid aliases
399
+ assert_equal(4, @index.instance_variable_get(:@entries).length - @default_indexed_entries.length)
400
+ end
344
401
  end
345
402
  end
@@ -0,0 +1,42 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "test_case"
5
+
6
+ module RubyIndexer
7
+ class RBSIndexerTest < TestCase
8
+ def test_index_core_classes
9
+ entries = @index["Array"]
10
+ refute_nil(entries)
11
+ assert_equal(1, entries.length)
12
+ entry = entries.first
13
+ assert_match(%r{/gems/rbs-.*/core/array.rbs}, entry.file_path)
14
+ assert_equal("array.rbs", entry.file_name)
15
+ assert_equal("Object", entry.parent_class)
16
+ assert_equal(1, entry.mixin_operations.length)
17
+ enumerable_include = entry.mixin_operations.first
18
+ assert_equal("Enumerable", enumerable_include.module_name)
19
+
20
+ # Using fixed positions would be fragile, so let's just check some basics.
21
+ assert_operator(entry.location.start_line, :>, 0)
22
+ assert_operator(entry.location.end_line, :>, entry.location.start_line)
23
+ assert_equal(0, entry.location.start_column)
24
+ assert_operator(entry.location.end_column, :>, 0)
25
+ end
26
+
27
+ def test_index_core_modules
28
+ entries = @index["Kernel"]
29
+ refute_nil(entries)
30
+ assert_equal(1, entries.length)
31
+ entry = entries.first
32
+ assert_match(%r{/gems/rbs-.*/core/kernel.rbs}, entry.file_path)
33
+ assert_equal("kernel.rbs", entry.file_name)
34
+
35
+ # Using fixed positions would be fragile, so let's just check some basics.
36
+ assert_operator(entry.location.start_line, :>, 0)
37
+ assert_operator(entry.location.end_line, :>, entry.location.start_line)
38
+ assert_equal(0, entry.location.start_column)
39
+ assert_operator(entry.location.end_column, :>, 0)
40
+ end
41
+ end
42
+ end
@@ -7,6 +7,8 @@ module RubyIndexer
7
7
  class TestCase < Minitest::Test
8
8
  def setup
9
9
  @index = Index.new
10
+ RBSIndexer.new(@index).index_ruby_core
11
+ @default_indexed_entries = @index.instance_variable_get(:@entries).dup
10
12
  end
11
13
 
12
14
  private
@@ -15,8 +17,9 @@ module RubyIndexer
15
17
  @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), source)
16
18
  end
17
19
 
18
- def assert_entry(expected_name, type, expected_location)
20
+ def assert_entry(expected_name, type, expected_location, visibility: nil)
19
21
  entries = @index[expected_name]
22
+ refute_nil(entries, "Expected #{expected_name} to be indexed")
20
23
  refute_empty(entries, "Expected #{expected_name} to be indexed")
21
24
 
22
25
  entry = entries.first
@@ -28,6 +31,8 @@ module RubyIndexer
28
31
  ":#{location.end_line - 1}-#{location.end_column}"
29
32
 
30
33
  assert_equal(expected_location, location_string)
34
+
35
+ assert_equal(visibility, entry.visibility) if visibility
31
36
  end
32
37
 
33
38
  def refute_entry(expected_name)
@@ -39,6 +44,10 @@ module RubyIndexer
39
44
  assert_empty(@index.instance_variable_get(:@entries), "Expected nothing to be indexed")
40
45
  end
41
46
 
47
+ def assert_no_indexed_entries
48
+ assert_equal(@default_indexed_entries, @index.instance_variable_get(:@entries))
49
+ end
50
+
42
51
  def assert_no_entry(entry)
43
52
  refute(@index.instance_variable_get(:@entries).key?(entry), "Expected '#{entry}' to not be indexed")
44
53
  end
@@ -99,8 +99,8 @@ module RubyLsp
99
99
  end
100
100
 
101
101
  sig { returns(String) }
102
- def backtraces
103
- @errors.filter_map(&:backtrace).join("\n\n")
102
+ def errors_details
103
+ @errors.map(&:full_message).join("\n\n")
104
104
  end
105
105
 
106
106
  # Each addon should implement `MyAddon#activate` and use to perform any sort of initialization, such as
@@ -131,11 +131,11 @@ module RubyLsp
131
131
  sig do
132
132
  overridable.params(
133
133
  response_builder: ResponseBuilders::Hover,
134
- nesting: T::Array[String],
134
+ node_context: NodeContext,
135
135
  dispatcher: Prism::Dispatcher,
136
136
  ).void
137
137
  end
138
- def create_hover_listener(response_builder, nesting, dispatcher); end
138
+ def create_hover_listener(response_builder, node_context, dispatcher); end
139
139
 
140
140
  # Creates a new DocumentSymbol listener. This method is invoked on every DocumentSymbol request
141
141
  sig do
@@ -159,21 +159,21 @@ module RubyLsp
159
159
  overridable.params(
160
160
  response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
161
161
  uri: URI::Generic,
162
- nesting: T::Array[String],
162
+ node_context: NodeContext,
163
163
  dispatcher: Prism::Dispatcher,
164
164
  ).void
165
165
  end
166
- def create_definition_listener(response_builder, uri, nesting, dispatcher); end
166
+ def create_definition_listener(response_builder, uri, node_context, dispatcher); end
167
167
 
168
168
  # Creates a new Completion listener. This method is invoked on every Completion request
169
169
  sig do
170
170
  overridable.params(
171
171
  response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
172
- nesting: T::Array[String],
172
+ node_context: NodeContext,
173
173
  dispatcher: Prism::Dispatcher,
174
174
  uri: URI::Generic,
175
175
  ).void
176
176
  end
177
- def create_completion_listener(response_builder, nesting, dispatcher, uri); end
177
+ def create_completion_listener(response_builder, node_context, dispatcher, uri); end
178
178
  end
179
179
  end
@@ -110,7 +110,7 @@ module RubyLsp
110
110
  params(
111
111
  position: T::Hash[Symbol, T.untyped],
112
112
  node_types: T::Array[T.class_of(Prism::Node)],
113
- ).returns([T.nilable(Prism::Node), T.nilable(Prism::Node), T::Array[String]])
113
+ ).returns(NodeContext)
114
114
  end
115
115
  def locate_node(position, node_types: [])
116
116
  locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types)
@@ -121,13 +121,14 @@ module RubyLsp
121
121
  node: Prism::Node,
122
122
  char_position: Integer,
123
123
  node_types: T::Array[T.class_of(Prism::Node)],
124
- ).returns([T.nilable(Prism::Node), T.nilable(Prism::Node), T::Array[String]])
124
+ ).returns(NodeContext)
125
125
  end
126
126
  def locate(node, char_position, node_types: [])
127
127
  queue = T.let(node.child_nodes.compact, T::Array[T.nilable(Prism::Node)])
128
128
  closest = node
129
129
  parent = T.let(nil, T.nilable(Prism::Node))
130
130
  nesting = T.let([], T::Array[T.any(Prism::ClassNode, Prism::ModuleNode)])
131
+ call_node = T.let(nil, T.nilable(Prism::CallNode))
131
132
 
132
133
  until queue.empty?
133
134
  candidate = queue.shift
@@ -159,6 +160,15 @@ module RubyLsp
159
160
  nesting << candidate
160
161
  end
161
162
 
163
+ if candidate.is_a?(Prism::CallNode)
164
+ arg_loc = candidate.arguments&.location
165
+ blk_loc = candidate.block&.location
166
+ if (arg_loc && (arg_loc.start_offset...arg_loc.end_offset).cover?(char_position)) ||
167
+ (blk_loc && (blk_loc.start_offset...blk_loc.end_offset).cover?(char_position))
168
+ call_node = candidate
169
+ end
170
+ end
171
+
162
172
  # If there are node types to filter by, and the current node is not one of those types, then skip it
163
173
  next if node_types.any? && node_types.none? { |type| candidate.class == type }
164
174
 
@@ -170,7 +180,20 @@ module RubyLsp
170
180
  end
171
181
  end
172
182
 
173
- [closest, parent, nesting.map { |n| n.constant_path.location.slice }]
183
+ # When targeting the constant part of a class/module definition, we do not want the nesting to be duplicated. That
184
+ # is, when targeting Bar in the following example:
185
+ #
186
+ # ```ruby
187
+ # class Foo::Bar; end
188
+ # ```
189
+ # The correct target is `Foo::Bar` with an empty nesting. `Foo::Bar` should not appear in the nesting stack, even
190
+ # though the class/module node does indeed enclose the target, because it would lead to incorrect behavior
191
+ if closest.is_a?(Prism::ConstantReadNode) || closest.is_a?(Prism::ConstantPathNode)
192
+ last_level = nesting.last
193
+ nesting.pop if last_level && last_level.constant_path == closest
194
+ end
195
+
196
+ NodeContext.new(closest, parent, nesting.map { |n| n.constant_path.location.slice }, call_node)
174
197
  end
175
198
 
176
199
  sig { returns(T::Boolean) }
@@ -54,18 +54,19 @@ module RubyLsp
54
54
 
55
55
  sig { params(options: T::Hash[Symbol, T.untyped]).void }
56
56
  def apply_options(options)
57
- dependencies = gather_dependencies
57
+ direct_dependencies = gather_direct_dependencies
58
+ all_dependencies = gather_direct_and_indirect_dependencies
58
59
  workspace_uri = options.dig(:workspaceFolders, 0, :uri)
59
60
  @workspace_uri = URI(workspace_uri) if workspace_uri
60
61
 
61
62
  specified_formatter = options.dig(:initializationOptions, :formatter)
62
63
  @formatter = specified_formatter if specified_formatter
63
- @formatter = detect_formatter(dependencies) if @formatter == "auto"
64
+ @formatter = detect_formatter(direct_dependencies, all_dependencies) if @formatter == "auto"
64
65
 
65
66
  specified_linters = options.dig(:initializationOptions, :linters)
66
- @linters = specified_linters || detect_linters(dependencies)
67
- @test_library = detect_test_library(dependencies)
68
- @typechecker = detect_typechecker(dependencies)
67
+ @linters = specified_linters || detect_linters(direct_dependencies)
68
+ @test_library = detect_test_library(direct_dependencies)
69
+ @typechecker = detect_typechecker(direct_dependencies)
69
70
 
70
71
  encodings = options.dig(:capabilities, :general, :positionEncodings)
71
72
  @encoding = if !encodings || encodings.empty?
@@ -103,16 +104,18 @@ module RubyLsp
103
104
 
104
105
  private
105
106
 
106
- sig { params(dependencies: T::Array[String]).returns(String) }
107
- def detect_formatter(dependencies)
107
+ sig { params(direct_dependencies: T::Array[String], all_dependencies: T::Array[String]).returns(String) }
108
+ def detect_formatter(direct_dependencies, all_dependencies)
108
109
  # NOTE: Intentionally no $ at end, since we want to match rubocop-shopify, etc.
109
- if dependencies.any?(/^rubocop/)
110
- "rubocop"
111
- elsif dependencies.any?(/^syntax_tree$/)
112
- "syntax_tree"
113
- else
114
- "none"
115
- end
110
+ return "rubocop" if direct_dependencies.any?(/^rubocop/)
111
+
112
+ syntax_tree_is_direct_dependency = direct_dependencies.include?("syntax_tree")
113
+ return "syntax_tree" if syntax_tree_is_direct_dependency
114
+
115
+ rubocop_is_transitive_dependency = all_dependencies.include?("rubocop")
116
+ return "rubocop" if dot_rubocop_yml_present && rubocop_is_transitive_dependency
117
+
118
+ "none"
116
119
  end
117
120
 
118
121
  # Try to detect if there are linters in the project's dependencies. For auto-detection, we always only consider a
@@ -132,7 +135,7 @@ module RubyLsp
132
135
  # by ruby-lsp-rails. A Rails app doesn't need to depend on the rails gem itself, individual components like
133
136
  # activestorage may be added to the gemfile so that other components aren't downloaded. Check for the presence
134
137
  # of bin/rails to support these cases.
135
- elsif File.exist?(File.join(workspace_path, "bin/rails"))
138
+ elsif bin_rails_present
136
139
  "rails"
137
140
  # NOTE: Intentionally ends with $ to avoid mis-matching minitest-reporters, etc. in a Rails app.
138
141
  elsif dependencies.any?(/^minitest$/)
@@ -162,8 +165,18 @@ module RubyLsp
162
165
  false
163
166
  end
164
167
 
168
+ sig { returns(T::Boolean) }
169
+ def bin_rails_present
170
+ File.exist?(File.join(workspace_path, "bin/rails"))
171
+ end
172
+
173
+ sig { returns(T::Boolean) }
174
+ def dot_rubocop_yml_present
175
+ File.exist?(File.join(workspace_path, ".rubocop.yml"))
176
+ end
177
+
165
178
  sig { returns(T::Array[String]) }
166
- def gather_dependencies
179
+ def gather_direct_dependencies
167
180
  Bundler.with_original_env { Bundler.default_gemfile }
168
181
  Bundler.locked_gems.dependencies.keys + gemspec_dependencies
169
182
  rescue Bundler::GemfileNotFound
@@ -176,5 +189,13 @@ module RubyLsp
176
189
  .grep(Bundler::Source::Gemspec)
177
190
  .flat_map { _1.gemspec&.dependencies&.map(&:name) }
178
191
  end
192
+
193
+ sig { returns(T::Array[String]) }
194
+ def gather_direct_and_indirect_dependencies
195
+ Bundler.with_original_env { Bundler.default_gemfile }
196
+ Bundler.locked_gems.specs.map(&:name)
197
+ rescue Bundler::GemfileNotFound
198
+ []
199
+ end
179
200
  end
180
201
  end
@@ -18,6 +18,7 @@ require "set"
18
18
  require "prism"
19
19
  require "prism/visitor"
20
20
  require "language_server-protocol"
21
+ require "rbs"
21
22
 
22
23
  require "ruby-lsp"
23
24
  require "ruby_lsp/base_server"
@@ -29,6 +30,7 @@ require "ruby_lsp/global_state"
29
30
  require "ruby_lsp/server"
30
31
  require "ruby_lsp/requests"
31
32
  require "ruby_lsp/response_builders"
33
+ require "ruby_lsp/node_context"
32
34
  require "ruby_lsp/document"
33
35
  require "ruby_lsp/ruby_document"
34
36
  require "ruby_lsp/store"
@@ -236,7 +236,7 @@ module RubyLsp
236
236
  # so there must be something to the left of the available path.
237
237
  group_stack = T.must(group_stack[last_dynamic_reference_index + 1..])
238
238
  if method_name
239
- " --name " + "/::#{Shellwords.escape(group_stack.join("::") + "#" + method_name)}$/"
239
+ " --name " + "/::#{Shellwords.escape(group_stack.join("::")) + "#" + Shellwords.escape(method_name)}$/"
240
240
  else
241
241
  # When clicking on a CodeLens for `Test`, `(#|::)` will match all tests
242
242
  # that are registered on the class itself (matches after `#`) and all tests
@@ -245,7 +245,7 @@ module RubyLsp
245
245
  end
246
246
  elsif method_name
247
247
  # We know the entire path, do an exact match
248
- " --name " + Shellwords.escape(group_stack.join("::") + "#" + method_name)
248
+ " --name " + Shellwords.escape(group_stack.join("::")) + "#" + Shellwords.escape(method_name)
249
249
  elsif spec_name
250
250
  " --name " + "/#{Shellwords.escape(spec_name)}/"
251
251
  else