ruby-lsp 0.20.1 → 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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +19 -4
  5. data/exe/ruby-lsp-launcher +124 -0
  6. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +6 -0
  7. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +233 -59
  8. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +34 -16
  9. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +1 -1
  10. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +15 -15
  11. data/lib/ruby_indexer/test/classes_and_modules_test.rb +4 -4
  12. data/lib/ruby_indexer/test/configuration_test.rb +10 -0
  13. data/lib/ruby_indexer/test/constant_test.rb +8 -8
  14. data/lib/ruby_indexer/test/enhancements_test.rb +169 -41
  15. data/lib/ruby_indexer/test/index_test.rb +41 -2
  16. data/lib/ruby_indexer/test/instance_variables_test.rb +1 -1
  17. data/lib/ruby_indexer/test/method_test.rb +139 -0
  18. data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
  19. data/lib/ruby_lsp/addon.rb +9 -2
  20. data/lib/ruby_lsp/base_server.rb +14 -5
  21. data/lib/ruby_lsp/client_capabilities.rb +67 -0
  22. data/lib/ruby_lsp/document.rb +1 -1
  23. data/lib/ruby_lsp/global_state.rb +33 -20
  24. data/lib/ruby_lsp/internal.rb +3 -0
  25. data/lib/ruby_lsp/listeners/completion.rb +62 -0
  26. data/lib/ruby_lsp/listeners/definition.rb +48 -13
  27. data/lib/ruby_lsp/listeners/document_highlight.rb +91 -4
  28. data/lib/ruby_lsp/listeners/document_symbol.rb +37 -4
  29. data/lib/ruby_lsp/listeners/hover.rb +52 -0
  30. data/lib/ruby_lsp/requests/code_action_resolve.rb +1 -1
  31. data/lib/ruby_lsp/requests/completion.rb +7 -1
  32. data/lib/ruby_lsp/requests/completion_resolve.rb +1 -1
  33. data/lib/ruby_lsp/requests/definition.rb +28 -11
  34. data/lib/ruby_lsp/requests/document_highlight.rb +7 -1
  35. data/lib/ruby_lsp/requests/document_symbol.rb +2 -1
  36. data/lib/ruby_lsp/requests/hover.rb +26 -6
  37. data/lib/ruby_lsp/requests/rename.rb +1 -1
  38. data/lib/ruby_lsp/requests/request.rb +1 -1
  39. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +12 -1
  40. data/lib/ruby_lsp/scripts/compose_bundle.rb +20 -0
  41. data/lib/ruby_lsp/scripts/compose_bundle_windows.rb +8 -0
  42. data/lib/ruby_lsp/server.rb +85 -55
  43. data/lib/ruby_lsp/setup_bundler.rb +154 -47
  44. data/lib/ruby_lsp/store.rb +0 -4
  45. data/lib/ruby_lsp/utils.rb +63 -0
  46. metadata +8 -3
@@ -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(
@@ -8,9 +8,11 @@ module RubyLsp
8
8
 
9
9
  abstract!
10
10
 
11
- sig { params(test_mode: T::Boolean).void }
12
- def initialize(test_mode: false)
13
- @test_mode = T.let(test_mode, T::Boolean)
11
+ sig { params(options: T.untyped).void }
12
+ def initialize(**options)
13
+ @test_mode = T.let(options[:test_mode], T.nilable(T::Boolean))
14
+ @setup_error = T.let(options[:setup_error], T.nilable(StandardError))
15
+ @install_error = T.let(options[:install_error], T.nilable(StandardError))
14
16
  @writer = T.let(Transport::Stdio::Writer.new, Transport::Stdio::Writer)
15
17
  @reader = T.let(Transport::Stdio::Reader.new, Transport::Stdio::Reader)
16
18
  @incoming_queue = T.let(Thread::Queue.new, Thread::Queue)
@@ -22,7 +24,7 @@ module RubyLsp
22
24
  @store = T.let(Store.new, Store)
23
25
  @outgoing_dispatcher = T.let(
24
26
  Thread.new do
25
- unless test_mode
27
+ unless @test_mode
26
28
  while (message = @outgoing_queue.pop)
27
29
  @mutex.synchronize { @writer.write(message.to_hash) }
28
30
  end
@@ -33,6 +35,11 @@ module RubyLsp
33
35
 
34
36
  @global_state = T.let(GlobalState.new, GlobalState)
35
37
  Thread.main.priority = 1
38
+
39
+ # We read the initialize request in `exe/ruby-lsp` to be able to determine the workspace URI where Bundler should
40
+ # be set up
41
+ initialize_request = options[:initialize_request]
42
+ process_message(initialize_request) if initialize_request
36
43
  end
37
44
 
38
45
  sig { void }
@@ -59,7 +66,9 @@ module RubyLsp
59
66
  # If the client supports request delegation and we're working with an ERB document and there was
60
67
  # something to parse, then we have to maintain the client updated about the virtual state of the host
61
68
  # language source
62
- if document.parse! && @global_state.supports_request_delegation && document.is_a?(ERBDocument)
69
+ if document.parse! && @global_state.client_capabilities.supports_request_delegation &&
70
+ document.is_a?(ERBDocument)
71
+
63
72
  send_message(
64
73
  Notification.new(
65
74
  method: "delegate/textDocument/virtualState",
@@ -0,0 +1,67 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ # This class stores all client capabilities that the Ruby LSP and its add-ons depend on to ensure that we're
6
+ # not enabling functionality unsupported by the editor connecting to the server
7
+ class ClientCapabilities
8
+ extend T::Sig
9
+
10
+ sig { returns(T::Boolean) }
11
+ attr_reader :supports_watching_files,
12
+ :supports_request_delegation,
13
+ :window_show_message_supports_extra_properties,
14
+ :supports_progress
15
+
16
+ sig { void }
17
+ def initialize
18
+ # The editor supports watching files. This requires two capabilities: dynamic registration and relative pattern
19
+ # support
20
+ @supports_watching_files = T.let(false, T::Boolean)
21
+
22
+ # The editor supports request delegation. This is an experimental capability since request delegation has not been
23
+ # standardized into the LSP spec yet
24
+ @supports_request_delegation = T.let(false, T::Boolean)
25
+
26
+ # The editor supports extra arbitrary properties for `window/showMessageRequest`. Necessary for add-ons to show
27
+ # dialogs with user interactions
28
+ @window_show_message_supports_extra_properties = T.let(false, T::Boolean)
29
+
30
+ # Which resource operations the editor supports, like renaming files
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)
35
+ end
36
+
37
+ sig { params(capabilities: T::Hash[Symbol, T.untyped]).void }
38
+ def apply_client_capabilities(capabilities)
39
+ workspace_capabilities = capabilities[:workspace] || {}
40
+
41
+ file_watching_caps = workspace_capabilities[:didChangeWatchedFiles]
42
+ if file_watching_caps&.dig(:dynamicRegistration) && file_watching_caps&.dig(:relativePatternSupport)
43
+ @supports_watching_files = true
44
+ end
45
+
46
+ @supports_request_delegation = capabilities.dig(:experimental, :requestDelegation) || false
47
+ supported_resource_operations = workspace_capabilities.dig(:workspaceEdit, :resourceOperations)
48
+ @supported_resource_operations = supported_resource_operations if supported_resource_operations
49
+
50
+ supports_additional_properties = capabilities.dig(
51
+ :window,
52
+ :showMessage,
53
+ :messageActionItem,
54
+ :additionalPropertiesSupport,
55
+ )
56
+ @window_show_message_supports_extra_properties = supports_additional_properties || false
57
+
58
+ progress = capabilities.dig(:window, :workDoneProgress)
59
+ @supports_progress = progress if progress
60
+ end
61
+
62
+ sig { returns(T::Boolean) }
63
+ def supports_rename?
64
+ @supported_resource_operations.include?("rename")
65
+ end
66
+ end
67
+ end
@@ -63,7 +63,7 @@ module RubyLsp
63
63
  sig { abstract.returns(LanguageId) }
64
64
  def language_id; end
65
65
 
66
- # TODO: remove this method once all nonpositional requests have been migrated to the listener pattern
66
+ # TODO: remove this method once all non-positional requests have been migrated to the listener pattern
67
67
  sig do
68
68
  type_parameters(:T)
69
69
  .params(
@@ -21,14 +21,14 @@ module RubyLsp
21
21
  attr_reader :encoding
22
22
 
23
23
  sig { returns(T::Boolean) }
24
- attr_reader :supports_watching_files, :experimental_features, :supports_request_delegation
25
-
26
- sig { returns(T::Array[String]) }
27
- attr_reader :supported_resource_operations
24
+ attr_reader :top_level_bundle
28
25
 
29
26
  sig { returns(TypeInferrer) }
30
27
  attr_reader :type_inferrer
31
28
 
29
+ sig { returns(ClientCapabilities) }
30
+ attr_reader :client_capabilities
31
+
32
32
  sig { void }
33
33
  def initialize
34
34
  @workspace_uri = T.let(URI::Generic.from_path(path: Dir.pwd), URI::Generic)
@@ -40,12 +40,19 @@ 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
- @supports_watching_files = T.let(false, T::Boolean)
44
- @experimental_features = T.let(false, T::Boolean)
45
43
  @type_inferrer = T.let(TypeInferrer.new(@index), TypeInferrer)
46
44
  @addon_settings = T.let({}, T::Hash[String, T.untyped])
47
- @supports_request_delegation = T.let(false, T::Boolean)
48
- @supported_resource_operations = T.let([], T::Array[String])
45
+ @top_level_bundle = T.let(
46
+ begin
47
+ Bundler.with_original_env { Bundler.default_gemfile }
48
+ true
49
+ rescue Bundler::GemfileNotFound, Bundler::GitError
50
+ false
51
+ end,
52
+ T::Boolean,
53
+ )
54
+ @client_capabilities = T.let(ClientCapabilities.new, ClientCapabilities)
55
+ @enabled_feature_flags = T.let({}, T::Hash[Symbol, T::Boolean])
49
56
  end
50
57
 
51
58
  sig { params(addon_name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
@@ -123,12 +130,7 @@ module RubyLsp
123
130
  end
124
131
  @index.configuration.encoding = @encoding
125
132
 
126
- file_watching_caps = options.dig(:capabilities, :workspace, :didChangeWatchedFiles)
127
- if file_watching_caps&.dig(:dynamicRegistration) && file_watching_caps&.dig(:relativePatternSupport)
128
- @supports_watching_files = true
129
- end
130
-
131
- @experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled) || false
133
+ @client_capabilities.apply_client_capabilities(options[:capabilities]) if options[:capabilities]
132
134
 
133
135
  addon_settings = options.dig(:initializationOptions, :addonSettings)
134
136
  if addon_settings
@@ -136,13 +138,17 @@ module RubyLsp
136
138
  @addon_settings.merge!(addon_settings)
137
139
  end
138
140
 
139
- @supports_request_delegation = options.dig(:capabilities, :experimental, :requestDelegation) || false
140
- supported_resource_operations = options.dig(:capabilities, :workspace, :workspaceEdit, :resourceOperations)
141
- @supported_resource_operations = supported_resource_operations if supported_resource_operations
141
+ enabled_flags = options.dig(:initializationOptions, :enabledFeatureFlags)
142
+ @enabled_feature_flags = enabled_flags if enabled_flags
142
143
 
143
144
  notifications
144
145
  end
145
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
+
146
152
  sig { returns(String) }
147
153
  def workspace_path
148
154
  T.must(@workspace_uri.to_standardized_path)
@@ -160,6 +166,11 @@ module RubyLsp
160
166
  end
161
167
  end
162
168
 
169
+ sig { returns(T::Boolean) }
170
+ def supports_watching_files
171
+ @client_capabilities.supports_watching_files
172
+ end
173
+
163
174
  private
164
175
 
165
176
  sig { params(direct_dependencies: T::Array[String], all_dependencies: T::Array[String]).returns(String) }
@@ -231,14 +242,16 @@ module RubyLsp
231
242
  sig { returns(T::Array[String]) }
232
243
  def gather_direct_dependencies
233
244
  Bundler.with_original_env { Bundler.default_gemfile }
234
- Bundler.locked_gems.dependencies.keys + gemspec_dependencies
245
+
246
+ dependencies = Bundler.locked_gems&.dependencies&.keys || []
247
+ dependencies + gemspec_dependencies
235
248
  rescue Bundler::GemfileNotFound
236
249
  []
237
250
  end
238
251
 
239
252
  sig { returns(T::Array[String]) }
240
253
  def gemspec_dependencies
241
- Bundler.locked_gems.sources
254
+ (Bundler.locked_gems&.sources || [])
242
255
  .grep(Bundler::Source::Gemspec)
243
256
  .flat_map { _1.gemspec&.dependencies&.map(&:name) }
244
257
  end
@@ -246,7 +259,7 @@ module RubyLsp
246
259
  sig { returns(T::Array[String]) }
247
260
  def gather_direct_and_indirect_dependencies
248
261
  Bundler.with_original_env { Bundler.default_gemfile }
249
- Bundler.locked_gems.specs.map(&:name)
262
+ Bundler.locked_gems&.specs&.map(&:name) || []
250
263
  rescue Bundler::GemfileNotFound
251
264
  []
252
265
  end
@@ -12,6 +12,7 @@ require "sorbet-runtime"
12
12
  require "bundler"
13
13
  Bundler.ui.level = :silent
14
14
 
15
+ require "json"
15
16
  require "uri"
16
17
  require "cgi"
17
18
  require "set"
@@ -20,6 +21,7 @@ require "prism"
20
21
  require "prism/visitor"
21
22
  require "language_server-protocol"
22
23
  require "rbs"
24
+ require "fileutils"
23
25
 
24
26
  require "ruby-lsp"
25
27
  require "ruby_lsp/base_server"
@@ -28,6 +30,7 @@ require "core_ext/uri"
28
30
  require "ruby_lsp/utils"
29
31
  require "ruby_lsp/static_docs"
30
32
  require "ruby_lsp/scope"
33
+ require "ruby_lsp/client_capabilities"
31
34
  require "ruby_lsp/global_state"
32
35
  require "ruby_lsp/server"
33
36
  require "ruby_lsp/type_inferrer"
@@ -85,6 +85,12 @@ module RubyLsp
85
85
  :on_constant_path_node_enter,
86
86
  :on_constant_read_node_enter,
87
87
  :on_call_node_enter,
88
+ :on_global_variable_and_write_node_enter,
89
+ :on_global_variable_operator_write_node_enter,
90
+ :on_global_variable_or_write_node_enter,
91
+ :on_global_variable_read_node_enter,
92
+ :on_global_variable_target_node_enter,
93
+ :on_global_variable_write_node_enter,
88
94
  :on_instance_variable_read_node_enter,
89
95
  :on_instance_variable_write_node_enter,
90
96
  :on_instance_variable_and_write_node_enter,
@@ -180,6 +186,36 @@ module RubyLsp
180
186
  end
181
187
  end
182
188
 
189
+ sig { params(node: Prism::GlobalVariableAndWriteNode).void }
190
+ def on_global_variable_and_write_node_enter(node)
191
+ handle_global_variable_completion(node.name.to_s, node.name_loc)
192
+ end
193
+
194
+ sig { params(node: Prism::GlobalVariableOperatorWriteNode).void }
195
+ def on_global_variable_operator_write_node_enter(node)
196
+ handle_global_variable_completion(node.name.to_s, node.name_loc)
197
+ end
198
+
199
+ sig { params(node: Prism::GlobalVariableOrWriteNode).void }
200
+ def on_global_variable_or_write_node_enter(node)
201
+ handle_global_variable_completion(node.name.to_s, node.name_loc)
202
+ end
203
+
204
+ sig { params(node: Prism::GlobalVariableReadNode).void }
205
+ def on_global_variable_read_node_enter(node)
206
+ handle_global_variable_completion(node.name.to_s, node.location)
207
+ end
208
+
209
+ sig { params(node: Prism::GlobalVariableTargetNode).void }
210
+ def on_global_variable_target_node_enter(node)
211
+ handle_global_variable_completion(node.name.to_s, node.location)
212
+ end
213
+
214
+ sig { params(node: Prism::GlobalVariableWriteNode).void }
215
+ def on_global_variable_write_node_enter(node)
216
+ handle_global_variable_completion(node.name.to_s, node.name_loc)
217
+ end
218
+
183
219
  sig { params(node: Prism::InstanceVariableReadNode).void }
184
220
  def on_instance_variable_read_node_enter(node)
185
221
  handle_instance_variable_completion(node.name.to_s, node.location)
@@ -267,6 +303,29 @@ module RubyLsp
267
303
  end
268
304
  end
269
305
 
306
+ sig { params(name: String, location: Prism::Location).void }
307
+ def handle_global_variable_completion(name, location)
308
+ candidates = @index.prefix_search(name)
309
+
310
+ return if candidates.none?
311
+
312
+ range = range_from_location(location)
313
+
314
+ candidates.flatten.uniq(&:name).each do |entry|
315
+ entry_name = entry.name
316
+
317
+ @response_builder << Interface::CompletionItem.new(
318
+ label: entry_name,
319
+ filter_text: entry_name,
320
+ label_details: Interface::CompletionItemLabelDetails.new(
321
+ description: entry.file_name,
322
+ ),
323
+ text_edit: Interface::TextEdit.new(range: range, new_text: entry_name),
324
+ kind: Constant::CompletionItemKind::VARIABLE,
325
+ )
326
+ end
327
+ end
328
+
270
329
  sig { params(name: String, location: Prism::Location).void }
271
330
  def handle_instance_variable_completion(name, location)
272
331
  # Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able
@@ -381,8 +440,11 @@ module RubyLsp
381
440
  return unless range
382
441
 
383
442
  guessed_type = type.is_a?(TypeInferrer::GuessedType) && type.name
443
+ external_references = @node_context.fully_qualified_name != type.name
384
444
 
385
445
  @index.method_completion_candidates(method_name, type.name).each do |entry|
446
+ next if entry.visibility != RubyIndexer::Entry::Visibility::PUBLIC && external_references
447
+
386
448
  entry_name = entry.name
387
449
  owner_name = entry.owner&.name
388
450
 
@@ -39,7 +39,12 @@ module RubyLsp
39
39
  :on_block_argument_node_enter,
40
40
  :on_constant_read_node_enter,
41
41
  :on_constant_path_node_enter,
42
+ :on_global_variable_and_write_node_enter,
43
+ :on_global_variable_operator_write_node_enter,
44
+ :on_global_variable_or_write_node_enter,
42
45
  :on_global_variable_read_node_enter,
46
+ :on_global_variable_target_node_enter,
47
+ :on_global_variable_write_node_enter,
43
48
  :on_instance_variable_read_node_enter,
44
49
  :on_instance_variable_write_node_enter,
45
50
  :on_instance_variable_and_write_node_enter,
@@ -121,23 +126,34 @@ module RubyLsp
121
126
  find_in_index(name)
122
127
  end
123
128
 
129
+ sig { params(node: Prism::GlobalVariableAndWriteNode).void }
130
+ def on_global_variable_and_write_node_enter(node)
131
+ handle_global_variable_definition(node.name.to_s)
132
+ end
133
+
134
+ sig { params(node: Prism::GlobalVariableOperatorWriteNode).void }
135
+ def on_global_variable_operator_write_node_enter(node)
136
+ handle_global_variable_definition(node.name.to_s)
137
+ end
138
+
139
+ sig { params(node: Prism::GlobalVariableOrWriteNode).void }
140
+ def on_global_variable_or_write_node_enter(node)
141
+ handle_global_variable_definition(node.name.to_s)
142
+ end
143
+
124
144
  sig { params(node: Prism::GlobalVariableReadNode).void }
125
145
  def on_global_variable_read_node_enter(node)
126
- entries = @index[node.name.to_s]
127
-
128
- return unless entries
146
+ handle_global_variable_definition(node.name.to_s)
147
+ end
129
148
 
130
- entries.each do |entry|
131
- location = entry.location
149
+ sig { params(node: Prism::GlobalVariableTargetNode).void }
150
+ def on_global_variable_target_node_enter(node)
151
+ handle_global_variable_definition(node.name.to_s)
152
+ end
132
153
 
133
- @response_builder << Interface::Location.new(
134
- uri: URI::Generic.from_path(path: entry.file_path).to_s,
135
- range: Interface::Range.new(
136
- start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
137
- end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
138
- ),
139
- )
140
- end
154
+ sig { params(node: Prism::GlobalVariableWriteNode).void }
155
+ def on_global_variable_write_node_enter(node)
156
+ handle_global_variable_definition(node.name.to_s)
141
157
  end
142
158
 
143
159
  sig { params(node: Prism::InstanceVariableReadNode).void }
@@ -197,6 +213,25 @@ module RubyLsp
197
213
  )
198
214
  end
199
215
 
216
+ sig { params(name: String).void }
217
+ def handle_global_variable_definition(name)
218
+ entries = @index[name]
219
+
220
+ return unless entries
221
+
222
+ entries.each do |entry|
223
+ location = entry.location
224
+
225
+ @response_builder << Interface::Location.new(
226
+ uri: URI::Generic.from_path(path: entry.file_path).to_s,
227
+ range: Interface::Range.new(
228
+ start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
229
+ end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
230
+ ),
231
+ )
232
+ end
233
+ end
234
+
200
235
  sig { params(name: String).void }
201
236
  def handle_instance_variable_definition(name)
202
237
  # Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able
@@ -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