ruby-lsp 0.16.7 → 0.17.3

Sign up to get free protection for your applications and to get access to all the features.
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