ruby-lsp 0.21.0 → 0.22.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.
@@ -5,24 +5,28 @@ require_relative "test_case"
5
5
 
6
6
  module RubyIndexer
7
7
  class EnhancementTest < TestCase
8
+ def teardown
9
+ super
10
+ Enhancement.clear
11
+ end
12
+
8
13
  def test_enhancing_indexing_included_hook
9
- enhancement_class = Class.new(Enhancement) do
10
- def on_call_node_enter(owner, node, file_path, code_units_cache)
14
+ Class.new(Enhancement) do
15
+ def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
16
+ owner = @listener.current_owner
11
17
  return unless owner
12
- return unless node.name == :extend
18
+ return unless call_node.name == :extend
13
19
 
14
- arguments = node.arguments&.arguments
20
+ arguments = call_node.arguments&.arguments
15
21
  return unless arguments
16
22
 
17
- location = Location.from_prism_location(node.location, code_units_cache)
18
-
19
23
  arguments.each do |node|
20
24
  next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
21
25
 
22
26
  module_name = node.full_name
23
27
  next unless module_name == "ActiveSupport::Concern"
24
28
 
25
- @index.register_included_hook(owner.name) do |index, base|
29
+ @listener.register_included_hook do |index, base|
26
30
  class_methods_name = "#{owner.name}::ClassMethods"
27
31
 
28
32
  if index.indexed?(class_methods_name)
@@ -31,16 +35,11 @@ module RubyIndexer
31
35
  end
32
36
  end
33
37
 
34
- @index.add(Entry::Method.new(
38
+ @listener.add_method(
35
39
  "new_method",
36
- file_path,
37
- location,
38
- location,
39
- nil,
40
+ call_node.location,
40
41
  [Entry::Signature.new([Entry::RequiredParameter.new(name: :a)])],
41
- Entry::Visibility::PUBLIC,
42
- owner,
43
- ))
42
+ )
44
43
  rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
45
44
  Prism::ConstantPathNode::MissingNodesInConstantPathError
46
45
  # Do nothing
@@ -48,7 +47,6 @@ module RubyIndexer
48
47
  end
49
48
  end
50
49
 
51
- @index.register_enhancement(enhancement_class.new(@index))
52
50
  index(<<~RUBY)
53
51
  module ActiveSupport
54
52
  module Concern
@@ -96,9 +94,9 @@ module RubyIndexer
96
94
  end
97
95
 
98
96
  def test_enhancing_indexing_configuration_dsl
99
- enhancement_class = Class.new(Enhancement) do
100
- def on_call_node_enter(owner, node, file_path, code_units_cache)
101
- return unless owner
97
+ Class.new(Enhancement) do
98
+ def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
99
+ return unless @listener.current_owner
102
100
 
103
101
  name = node.name
104
102
  return unless name == :has_many
@@ -109,22 +107,14 @@ module RubyIndexer
109
107
  association_name = arguments.first
110
108
  return unless association_name.is_a?(Prism::SymbolNode)
111
109
 
112
- location = Location.from_prism_location(association_name.location, code_units_cache)
113
-
114
- @index.add(Entry::Method.new(
110
+ @listener.add_method(
115
111
  T.must(association_name.value),
116
- file_path,
117
- location,
118
- location,
119
- nil,
112
+ association_name.location,
120
113
  [],
121
- Entry::Visibility::PUBLIC,
122
- owner,
123
- ))
114
+ )
124
115
  end
125
116
  end
126
117
 
127
- @index.register_enhancement(enhancement_class.new(@index))
128
118
  index(<<~RUBY)
129
119
  module ActiveSupport
130
120
  module Concern
@@ -157,8 +147,8 @@ module RubyIndexer
157
147
  end
158
148
 
159
149
  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)
150
+ Class.new(Enhancement) do
151
+ def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
162
152
  raise "Error"
163
153
  end
164
154
 
@@ -169,8 +159,6 @@ module RubyIndexer
169
159
  end
170
160
  end
171
161
 
172
- @index.register_enhancement(enhancement_class.new(@index))
173
-
174
162
  _stdout, stderr = capture_io do
175
163
  index(<<~RUBY)
176
164
  module ActiveSupport
@@ -192,8 +180,8 @@ module RubyIndexer
192
180
  end
193
181
 
194
182
  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)
183
+ Class.new(Enhancement) do
184
+ def on_call_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
197
185
  raise "Error"
198
186
  end
199
187
 
@@ -204,8 +192,6 @@ module RubyIndexer
204
192
  end
205
193
  end
206
194
 
207
- @index.register_enhancement(enhancement_class.new(@index))
208
-
209
195
  _stdout, stderr = capture_io do
210
196
  index(<<~RUBY)
211
197
  module ActiveSupport
@@ -225,5 +211,115 @@ module RubyIndexer
225
211
  # The module should still be indexed
226
212
  assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5")
227
213
  end
214
+
215
+ def test_advancing_namespace_stack_from_enhancement
216
+ Class.new(Enhancement) do
217
+ def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
218
+ owner = @listener.current_owner
219
+ return unless owner
220
+
221
+ case call_node.name
222
+ when :class_methods
223
+ @listener.add_module("ClassMethods", call_node.location, call_node.location)
224
+ when :extend
225
+ arguments = call_node.arguments&.arguments
226
+ return unless arguments
227
+
228
+ arguments.each do |node|
229
+ next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
230
+
231
+ module_name = node.full_name
232
+ next unless module_name == "ActiveSupport::Concern"
233
+
234
+ @listener.register_included_hook do |index, base|
235
+ class_methods_name = "#{owner.name}::ClassMethods"
236
+
237
+ if index.indexed?(class_methods_name)
238
+ singleton = index.existing_or_new_singleton_class(base.name)
239
+ singleton.mixin_operations << Entry::Include.new(class_methods_name)
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ def on_call_node_leave(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
247
+ return unless call_node.name == :class_methods
248
+
249
+ @listener.pop_namespace_stack
250
+ end
251
+ end
252
+
253
+ index(<<~RUBY)
254
+ module ActiveSupport
255
+ module Concern
256
+ end
257
+ end
258
+
259
+ module MyConcern
260
+ extend ActiveSupport::Concern
261
+
262
+ class_methods do
263
+ def foo; end
264
+ end
265
+ end
266
+
267
+ class User
268
+ include MyConcern
269
+ end
270
+ RUBY
271
+
272
+ assert_equal(
273
+ [
274
+ "User::<Class:User>",
275
+ "MyConcern::ClassMethods",
276
+ "Object::<Class:Object>",
277
+ "BasicObject::<Class:BasicObject>",
278
+ "Class",
279
+ "Module",
280
+ "Object",
281
+ "Kernel",
282
+ "BasicObject",
283
+ ],
284
+ @index.linearized_ancestors_of("User::<Class:User>"),
285
+ )
286
+
287
+ refute_nil(@index.resolve_method("foo", "User::<Class:User>"))
288
+ end
289
+
290
+ def test_creating_anonymous_classes_from_enhancement
291
+ Class.new(Enhancement) do
292
+ def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
293
+ case call_node.name
294
+ when :context
295
+ arguments = call_node.arguments&.arguments
296
+ first_argument = arguments&.first
297
+ return unless first_argument.is_a?(Prism::StringNode)
298
+
299
+ @listener.add_class(
300
+ "<RSpec:#{first_argument.content}>",
301
+ call_node.location,
302
+ first_argument.location,
303
+ )
304
+ when :subject
305
+ @listener.add_method("subject", call_node.location, [])
306
+ end
307
+ end
308
+
309
+ def on_call_node_leave(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
310
+ return unless call_node.name == :context
311
+
312
+ @listener.pop_namespace_stack
313
+ end
314
+ end
315
+
316
+ index(<<~RUBY)
317
+ context "does something" do
318
+ subject { call_whatever }
319
+ end
320
+ RUBY
321
+
322
+ refute_nil(@index.resolve_method("subject", "<RSpec:does something>"))
323
+ end
228
324
  end
229
325
  end
@@ -1672,6 +1672,38 @@ module RubyIndexer
1672
1672
  )
1673
1673
  end
1674
1674
 
1675
+ def test_extend_self
1676
+ @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY)
1677
+ module Foo
1678
+ def bar
1679
+ end
1680
+
1681
+ extend self
1682
+
1683
+ def baz
1684
+ end
1685
+ end
1686
+ RUBY
1687
+
1688
+ ["bar", "baz"].product(["Foo", "Foo::<Class:Foo>"]).each do |method, receiver|
1689
+ entry = @index.resolve_method(method, receiver)&.first
1690
+ refute_nil(entry)
1691
+ assert_equal(method, T.must(entry).name)
1692
+ end
1693
+
1694
+ assert_equal(
1695
+ [
1696
+ "Foo::<Class:Foo>",
1697
+ "Foo",
1698
+ "Module",
1699
+ "Object",
1700
+ "Kernel",
1701
+ "BasicObject",
1702
+ ],
1703
+ @index.linearized_ancestors_of("Foo::<Class:Foo>"),
1704
+ )
1705
+ end
1706
+
1675
1707
  def test_linearizing_singleton_ancestors
1676
1708
  @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY)
1677
1709
  module First
@@ -2023,5 +2055,12 @@ module RubyIndexer
2023
2055
  ),
2024
2056
  )
2025
2057
  end
2058
+
2059
+ def test_prevents_multiple_calls_to_index_all
2060
+ # For this test class, `index_all` is already called once in `setup`.
2061
+ assert_raises(Index::IndexNotEmptyError) do
2062
+ @index.index_all
2063
+ end
2064
+ end
2026
2065
  end
2027
2066
  end
@@ -141,7 +141,7 @@ module RubyIndexer
141
141
  # The first entry points to the location of the module_function call
142
142
  assert_equal("Test", first_entry.owner.name)
143
143
  assert_instance_of(Entry::Module, first_entry.owner)
144
- assert_equal(Entry::Visibility::PRIVATE, first_entry.visibility)
144
+ assert_predicate(first_entry, :private?)
145
145
  # The second entry points to the public singleton method
146
146
  assert_equal("Test::<Class:Test>", second_entry.owner.name)
147
147
  assert_instance_of(Entry::SingletonClass, second_entry.owner)
@@ -149,6 +149,119 @@ module RubyIndexer
149
149
  end
150
150
  end
151
151
 
152
+ def test_private_class_method_visibility_tracking_string_symbol_arguments
153
+ index(<<~RUBY)
154
+ class Test
155
+ def self.foo
156
+ end
157
+
158
+ def self.bar
159
+ end
160
+
161
+ private_class_method("foo", :bar)
162
+
163
+ def self.baz
164
+ end
165
+ end
166
+ RUBY
167
+
168
+ ["foo", "bar"].each do |keyword|
169
+ entries = T.must(@index[keyword])
170
+ assert_equal(1, entries.size)
171
+ entry = entries.first
172
+ assert_predicate(entry, :private?)
173
+ end
174
+
175
+ entries = T.must(@index["baz"])
176
+ assert_equal(1, entries.size)
177
+ entry = entries.first
178
+ assert_predicate(entry, :public?)
179
+ end
180
+
181
+ def test_private_class_method_visibility_tracking_array_argument
182
+ index(<<~RUBY)
183
+ class Test
184
+ def self.foo
185
+ end
186
+
187
+ def self.bar
188
+ end
189
+
190
+ private_class_method(["foo", :bar])
191
+
192
+ def self.baz
193
+ end
194
+ end
195
+ RUBY
196
+
197
+ ["foo", "bar"].each do |keyword|
198
+ entries = T.must(@index[keyword])
199
+ assert_equal(1, entries.size)
200
+ entry = entries.first
201
+ assert_predicate(entry, :private?)
202
+ end
203
+
204
+ entries = T.must(@index["baz"])
205
+ assert_equal(1, entries.size)
206
+ entry = entries.first
207
+ assert_predicate(entry, :public?)
208
+ end
209
+
210
+ def test_private_class_method_visibility_tracking_method_argument
211
+ index(<<~RUBY)
212
+ class Test
213
+ private_class_method def self.foo
214
+ end
215
+
216
+ def self.bar
217
+ end
218
+ end
219
+ RUBY
220
+
221
+ entries = T.must(@index["foo"])
222
+ assert_equal(1, entries.size)
223
+ entry = entries.first
224
+ assert_predicate(entry, :private?)
225
+
226
+ entries = T.must(@index["bar"])
227
+ assert_equal(1, entries.size)
228
+ entry = entries.first
229
+ assert_predicate(entry, :public?)
230
+ end
231
+
232
+ def test_comments_documentation
233
+ index(<<~RUBY)
234
+ # Documentation for Foo
235
+
236
+ class Foo
237
+ # ####################
238
+ # Documentation for bar
239
+ # ####################
240
+ #
241
+ def bar
242
+ end
243
+
244
+ # test
245
+
246
+ # Documentation for baz
247
+ def baz; end
248
+ def ban; end
249
+ end
250
+ RUBY
251
+
252
+ foo_comment = @index["Foo"].first.comments
253
+ assert_equal("Documentation for Foo", foo_comment)
254
+
255
+ bar_comment = @index["bar"].first.comments
256
+ assert_equal("####################\nDocumentation for bar\n####################\n", bar_comment)
257
+
258
+ baz_comment = @index["baz"].first.comments
259
+ assert_equal("Documentation for baz", baz_comment)
260
+
261
+ ban_comment = @index["ban"].first.comments
262
+ assert_empty(ban_comment)
263
+ end
264
+
152
265
  def test_method_with_parameters
153
266
  index(<<~RUBY)
154
267
  class Foo
@@ -10,7 +10,8 @@ module RubyLsp
10
10
  sig { returns(T::Boolean) }
11
11
  attr_reader :supports_watching_files,
12
12
  :supports_request_delegation,
13
- :window_show_message_supports_extra_properties
13
+ :window_show_message_supports_extra_properties,
14
+ :supports_progress
14
15
 
15
16
  sig { void }
16
17
  def initialize
@@ -28,6 +29,9 @@ module RubyLsp
28
29
 
29
30
  # Which resource operations the editor supports, like renaming files
30
31
  @supported_resource_operations = T.let([], T::Array[String])
32
+
33
+ # The editor supports displaying progress requests
34
+ @supports_progress = T.let(false, T::Boolean)
31
35
  end
32
36
 
33
37
  sig { params(capabilities: T::Hash[Symbol, T.untyped]).void }
@@ -50,6 +54,9 @@ module RubyLsp
50
54
  :additionalPropertiesSupport,
51
55
  )
52
56
  @window_show_message_supports_extra_properties = supports_additional_properties || false
57
+
58
+ progress = capabilities.dig(:window, :workDoneProgress)
59
+ @supports_progress = progress if progress
53
60
  end
54
61
 
55
62
  sig { returns(T::Boolean) }
@@ -21,7 +21,7 @@ module RubyLsp
21
21
  attr_reader :encoding
22
22
 
23
23
  sig { returns(T::Boolean) }
24
- attr_reader :experimental_features, :top_level_bundle
24
+ attr_reader :top_level_bundle
25
25
 
26
26
  sig { returns(TypeInferrer) }
27
27
  attr_reader :type_inferrer
@@ -40,7 +40,6 @@ module RubyLsp
40
40
  @has_type_checker = T.let(true, T::Boolean)
41
41
  @index = T.let(RubyIndexer::Index.new, RubyIndexer::Index)
42
42
  @supported_formatters = T.let({}, T::Hash[String, Requests::Support::Formatter])
43
- @experimental_features = T.let(false, T::Boolean)
44
43
  @type_inferrer = T.let(TypeInferrer.new(@index), TypeInferrer)
45
44
  @addon_settings = T.let({}, T::Hash[String, T.untyped])
46
45
  @top_level_bundle = T.let(
@@ -53,6 +52,7 @@ module RubyLsp
53
52
  T::Boolean,
54
53
  )
55
54
  @client_capabilities = T.let(ClientCapabilities.new, ClientCapabilities)
55
+ @enabled_feature_flags = T.let({}, T::Hash[Symbol, T::Boolean])
56
56
  end
57
57
 
58
58
  sig { params(addon_name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
@@ -130,7 +130,6 @@ module RubyLsp
130
130
  end
131
131
  @index.configuration.encoding = @encoding
132
132
 
133
- @experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled) || false
134
133
  @client_capabilities.apply_client_capabilities(options[:capabilities]) if options[:capabilities]
135
134
 
136
135
  addon_settings = options.dig(:initializationOptions, :addonSettings)
@@ -139,9 +138,17 @@ module RubyLsp
139
138
  @addon_settings.merge!(addon_settings)
140
139
  end
141
140
 
141
+ enabled_flags = options.dig(:initializationOptions, :enabledFeatureFlags)
142
+ @enabled_feature_flags = enabled_flags if enabled_flags
143
+
142
144
  notifications
143
145
  end
144
146
 
147
+ sig { params(flag: Symbol).returns(T.nilable(T::Boolean)) }
148
+ def enabled_feature?(flag)
149
+ @enabled_feature_flags[:all] || @enabled_feature_flags[flag]
150
+ end
151
+
145
152
  sig { returns(String) }
146
153
  def workspace_path
147
154
  T.must(@workspace_uri.to_standardized_path)
@@ -159,6 +166,11 @@ module RubyLsp
159
166
  end
160
167
  end
161
168
 
169
+ sig { returns(T::Boolean) }
170
+ def supports_watching_files
171
+ @client_capabilities.supports_watching_files
172
+ end
173
+
162
174
  private
163
175
 
164
176
  sig { params(direct_dependencies: T::Array[String], all_dependencies: T::Array[String]).returns(String) }
@@ -21,6 +21,7 @@ require "prism"
21
21
  require "prism/visitor"
22
22
  require "language_server-protocol"
23
23
  require "rbs"
24
+ require "fileutils"
24
25
 
25
26
  require "ruby-lsp"
26
27
  require "ruby_lsp/base_server"
@@ -92,14 +92,15 @@ module RubyLsp
92
92
  target: T.nilable(Prism::Node),
93
93
  parent: T.nilable(Prism::Node),
94
94
  dispatcher: Prism::Dispatcher,
95
+ position: T::Hash[Symbol, T.untyped],
95
96
  ).void
96
97
  end
97
- def initialize(response_builder, target, parent, dispatcher)
98
+ def initialize(response_builder, target, parent, dispatcher, position)
98
99
  @response_builder = response_builder
99
100
 
100
101
  return unless target && parent
101
102
 
102
- highlight_target =
103
+ highlight_target, highlight_target_value =
103
104
  case target
104
105
  when Prism::GlobalVariableReadNode, Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableOperatorWriteNode,
105
106
  Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableTargetNode, Prism::GlobalVariableWriteNode,
@@ -116,13 +117,17 @@ module RubyLsp
116
117
  Prism::CallNode, Prism::BlockParameterNode, Prism::RequiredKeywordParameterNode,
117
118
  Prism::RequiredKeywordParameterNode, Prism::KeywordRestParameterNode, Prism::OptionalParameterNode,
118
119
  Prism::RequiredParameterNode, Prism::RestParameterNode
120
+ [target, node_value(target)]
121
+ when Prism::ModuleNode, Prism::ClassNode, Prism::SingletonClassNode, Prism::DefNode, Prism::CaseNode,
122
+ Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::IfNode, Prism::UnlessNode
119
123
  target
120
124
  end
121
125
 
122
126
  @target = T.let(highlight_target, T.nilable(Prism::Node))
123
- @target_value = T.let(node_value(highlight_target), T.nilable(String))
127
+ @target_value = T.let(highlight_target_value, T.nilable(String))
128
+ @target_position = position
124
129
 
125
- if @target && @target_value
130
+ if @target
126
131
  dispatcher.register(
127
132
  self,
128
133
  :on_call_node_enter,
@@ -172,6 +177,13 @@ module RubyLsp
172
177
  :on_global_variable_or_write_node_enter,
173
178
  :on_global_variable_and_write_node_enter,
174
179
  :on_global_variable_operator_write_node_enter,
180
+ :on_singleton_class_node_enter,
181
+ :on_case_node_enter,
182
+ :on_while_node_enter,
183
+ :on_until_node_enter,
184
+ :on_for_node_enter,
185
+ :on_if_node_enter,
186
+ :on_unless_node_enter,
175
187
  )
176
188
  end
177
189
  end
@@ -189,6 +201,8 @@ module RubyLsp
189
201
 
190
202
  sig { params(node: Prism::DefNode).void }
191
203
  def on_def_node_enter(node)
204
+ add_matching_end_highlights(node.def_keyword_loc, node.end_keyword_loc) if @target.is_a?(Prism::DefNode)
205
+
192
206
  return unless matches?(node, [Prism::CallNode, Prism::DefNode])
193
207
 
194
208
  add_highlight(Constant::DocumentHighlightKind::WRITE, node.name_loc)
@@ -252,6 +266,8 @@ module RubyLsp
252
266
 
253
267
  sig { params(node: Prism::ClassNode).void }
254
268
  def on_class_node_enter(node)
269
+ add_matching_end_highlights(node.class_keyword_loc, node.end_keyword_loc) if @target.is_a?(Prism::ClassNode)
270
+
255
271
  return unless matches?(node, CONSTANT_NODES + CONSTANT_PATH_NODES + [Prism::ClassNode])
256
272
 
257
273
  add_highlight(Constant::DocumentHighlightKind::WRITE, node.constant_path.location)
@@ -259,6 +275,8 @@ module RubyLsp
259
275
 
260
276
  sig { params(node: Prism::ModuleNode).void }
261
277
  def on_module_node_enter(node)
278
+ add_matching_end_highlights(node.module_keyword_loc, node.end_keyword_loc) if @target.is_a?(Prism::ModuleNode)
279
+
262
280
  return unless matches?(node, CONSTANT_NODES + CONSTANT_PATH_NODES + [Prism::ModuleNode])
263
281
 
264
282
  add_highlight(Constant::DocumentHighlightKind::WRITE, node.constant_path.location)
@@ -511,6 +529,55 @@ module RubyLsp
511
529
  add_highlight(Constant::DocumentHighlightKind::WRITE, node.name_loc)
512
530
  end
513
531
 
532
+ sig { params(node: Prism::SingletonClassNode).void }
533
+ def on_singleton_class_node_enter(node)
534
+ return unless @target.is_a?(Prism::SingletonClassNode)
535
+
536
+ add_matching_end_highlights(node.class_keyword_loc, node.end_keyword_loc)
537
+ end
538
+
539
+ sig { params(node: Prism::CaseNode).void }
540
+ def on_case_node_enter(node)
541
+ return unless @target.is_a?(Prism::CaseNode)
542
+
543
+ add_matching_end_highlights(node.case_keyword_loc, node.end_keyword_loc)
544
+ end
545
+
546
+ sig { params(node: Prism::WhileNode).void }
547
+ def on_while_node_enter(node)
548
+ return unless @target.is_a?(Prism::WhileNode)
549
+
550
+ add_matching_end_highlights(node.keyword_loc, node.closing_loc)
551
+ end
552
+
553
+ sig { params(node: Prism::UntilNode).void }
554
+ def on_until_node_enter(node)
555
+ return unless @target.is_a?(Prism::UntilNode)
556
+
557
+ add_matching_end_highlights(node.keyword_loc, node.closing_loc)
558
+ end
559
+
560
+ sig { params(node: Prism::ForNode).void }
561
+ def on_for_node_enter(node)
562
+ return unless @target.is_a?(Prism::ForNode)
563
+
564
+ add_matching_end_highlights(node.for_keyword_loc, node.end_keyword_loc)
565
+ end
566
+
567
+ sig { params(node: Prism::IfNode).void }
568
+ def on_if_node_enter(node)
569
+ return unless @target.is_a?(Prism::IfNode)
570
+
571
+ add_matching_end_highlights(node.if_keyword_loc, node.end_keyword_loc)
572
+ end
573
+
574
+ sig { params(node: Prism::UnlessNode).void }
575
+ def on_unless_node_enter(node)
576
+ return unless @target.is_a?(Prism::UnlessNode)
577
+
578
+ add_matching_end_highlights(node.keyword_loc, node.end_keyword_loc)
579
+ end
580
+
514
581
  private
515
582
 
516
583
  sig { params(node: Prism::Node, classes: T::Array[T.class_of(Prism::Node)]).returns(T.nilable(T::Boolean)) }
@@ -550,6 +617,26 @@ module RubyLsp
550
617
  node.constant_path.slice
551
618
  end
552
619
  end
620
+
621
+ sig { params(keyword_loc: T.nilable(Prism::Location), end_loc: T.nilable(Prism::Location)).void }
622
+ def add_matching_end_highlights(keyword_loc, end_loc)
623
+ return unless keyword_loc && end_loc && end_loc.length.positive?
624
+ return unless covers_target_position?(keyword_loc) || covers_target_position?(end_loc)
625
+
626
+ add_highlight(Constant::DocumentHighlightKind::TEXT, keyword_loc)
627
+ add_highlight(Constant::DocumentHighlightKind::TEXT, end_loc)
628
+ end
629
+
630
+ sig { params(location: Prism::Location).returns(T::Boolean) }
631
+ def covers_target_position?(location)
632
+ start_line = location.start_line - 1
633
+ end_line = location.end_line - 1
634
+ start_covered = start_line < @target_position[:line] ||
635
+ (start_line == @target_position[:line] && location.start_column <= @target_position[:character])
636
+ end_covered = end_line > @target_position[:line] ||
637
+ (end_line == @target_position[:line] && location.end_column >= @target_position[:character])
638
+ start_covered && end_covered
639
+ end
553
640
  end
554
641
  end
555
642
  end