ruby-lsp 0.22.1 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/exe/ruby-lsp +10 -9
  4. data/exe/ruby-lsp-check +5 -5
  5. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +26 -20
  6. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +88 -22
  7. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +60 -30
  8. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +73 -55
  9. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +16 -14
  10. data/lib/{core_ext → ruby_indexer/lib/ruby_indexer}/uri.rb +29 -3
  11. data/lib/ruby_indexer/ruby_indexer.rb +1 -1
  12. data/lib/ruby_indexer/test/class_variables_test.rb +140 -0
  13. data/lib/ruby_indexer/test/classes_and_modules_test.rb +11 -6
  14. data/lib/ruby_indexer/test/configuration_test.rb +116 -51
  15. data/lib/ruby_indexer/test/enhancements_test.rb +2 -2
  16. data/lib/ruby_indexer/test/index_test.rb +72 -43
  17. data/lib/ruby_indexer/test/rbs_indexer_test.rb +1 -1
  18. data/lib/ruby_indexer/test/reference_finder_test.rb +1 -1
  19. data/lib/ruby_indexer/test/test_case.rb +2 -2
  20. data/lib/ruby_indexer/test/uri_test.rb +72 -0
  21. data/lib/ruby_lsp/addon.rb +9 -0
  22. data/lib/ruby_lsp/base_server.rb +15 -6
  23. data/lib/ruby_lsp/document.rb +10 -1
  24. data/lib/ruby_lsp/internal.rb +1 -1
  25. data/lib/ruby_lsp/listeners/code_lens.rb +8 -4
  26. data/lib/ruby_lsp/listeners/completion.rb +73 -4
  27. data/lib/ruby_lsp/listeners/definition.rb +73 -17
  28. data/lib/ruby_lsp/listeners/document_symbol.rb +12 -1
  29. data/lib/ruby_lsp/listeners/folding_ranges.rb +1 -1
  30. data/lib/ruby_lsp/listeners/hover.rb +57 -0
  31. data/lib/ruby_lsp/requests/completion.rb +6 -0
  32. data/lib/ruby_lsp/requests/completion_resolve.rb +2 -1
  33. data/lib/ruby_lsp/requests/definition.rb +6 -0
  34. data/lib/ruby_lsp/requests/prepare_rename.rb +51 -0
  35. data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +1 -1
  36. data/lib/ruby_lsp/requests/rename.rb +14 -4
  37. data/lib/ruby_lsp/requests/support/common.rb +1 -5
  38. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +1 -1
  39. data/lib/ruby_lsp/requests/workspace_symbol.rb +3 -2
  40. data/lib/ruby_lsp/scripts/compose_bundle.rb +1 -1
  41. data/lib/ruby_lsp/server.rb +42 -7
  42. data/lib/ruby_lsp/setup_bundler.rb +31 -41
  43. data/lib/ruby_lsp/test_helper.rb +45 -11
  44. data/lib/ruby_lsp/type_inferrer.rb +22 -0
  45. data/lib/ruby_lsp/utils.rb +3 -0
  46. metadata +7 -8
  47. data/lib/ruby_indexer/lib/ruby_indexer/indexable_path.rb +0 -29
@@ -0,0 +1,72 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "test_helper"
5
+
6
+ module RubyIndexer
7
+ class URITest < Minitest::Test
8
+ def test_from_path_on_unix
9
+ uri = URI::Generic.from_path(path: "/some/unix/path/to/file.rb")
10
+ assert_equal("/some/unix/path/to/file.rb", uri.path)
11
+ end
12
+
13
+ def test_from_path_on_windows
14
+ uri = URI::Generic.from_path(path: "C:/some/windows/path/to/file.rb")
15
+ assert_equal("/C:/some/windows/path/to/file.rb", uri.path)
16
+ end
17
+
18
+ def test_from_path_on_windows_with_lowercase_drive
19
+ uri = URI::Generic.from_path(path: "c:/some/windows/path/to/file.rb")
20
+ assert_equal("/c:/some/windows/path/to/file.rb", uri.path)
21
+ end
22
+
23
+ def test_to_standardized_path_on_unix
24
+ uri = URI::Generic.from_path(path: "/some/unix/path/to/file.rb")
25
+ assert_equal(uri.path, uri.to_standardized_path)
26
+ end
27
+
28
+ def test_to_standardized_path_on_windows
29
+ uri = URI::Generic.from_path(path: "C:/some/windows/path/to/file.rb")
30
+ assert_equal("C:/some/windows/path/to/file.rb", uri.to_standardized_path)
31
+ end
32
+
33
+ def test_to_standardized_path_on_windows_with_lowercase_drive
34
+ uri = URI::Generic.from_path(path: "c:/some/windows/path/to/file.rb")
35
+ assert_equal("c:/some/windows/path/to/file.rb", uri.to_standardized_path)
36
+ end
37
+
38
+ def test_to_standardized_path_on_windows_with_received_uri
39
+ uri = URI("file:///c%3A/some/windows/path/to/file.rb")
40
+ assert_equal("c:/some/windows/path/to/file.rb", uri.to_standardized_path)
41
+ end
42
+
43
+ def test_plus_signs_are_properly_unescaped
44
+ path = "/opt/rubies/3.3.0/lib/ruby/3.3.0+0/pathname.rb"
45
+ uri = URI::Generic.from_path(path: path)
46
+ assert_equal(path, uri.to_standardized_path)
47
+ end
48
+
49
+ def test_from_path_with_fragment
50
+ uri = URI::Generic.from_path(path: "/some/unix/path/to/file.rb", fragment: "L1,3-2,9")
51
+ assert_equal("file:///some/unix/path/to/file.rb#L1,3-2,9", uri.to_s)
52
+ end
53
+
54
+ def test_from_path_windows_long_file_paths
55
+ uri = URI::Generic.from_path(path: "//?/C:/hostedtoolcache/windows/Ruby/3.3.1/x64/lib/ruby/3.3.0/open-uri.rb")
56
+ assert_equal("C:/hostedtoolcache/windows/Ruby/3.3.1/x64/lib/ruby/3.3.0/open-uri.rb", uri.to_standardized_path)
57
+ end
58
+
59
+ def test_from_path_computes_require_path_when_load_path_entry_is_given
60
+ uri = URI::Generic.from_path(path: "/some/unix/path/to/file.rb", load_path_entry: "/some/unix/path")
61
+ assert_equal("to/file", uri.require_path)
62
+ end
63
+
64
+ def test_allows_adding_require_path_with_load_path_entry
65
+ uri = URI::Generic.from_path(path: "/some/unix/path/to/file.rb")
66
+ assert_nil(uri.require_path)
67
+
68
+ uri.add_require_path_from_load_entry("/some/unix/path")
69
+ assert_equal("to/file", uri.require_path)
70
+ end
71
+ end
72
+ end
@@ -97,6 +97,15 @@ module RubyLsp
97
97
  errors
98
98
  end
99
99
 
100
+ # Unloads all add-ons. Only intended to be invoked once when shutting down the Ruby LSP server
101
+ sig { void }
102
+ def unload_addons
103
+ @addons.each(&:deactivate)
104
+ @addons.clear
105
+ @addon_classes.clear
106
+ @file_watcher_addons.clear
107
+ end
108
+
100
109
  # Get a reference to another add-on object by name and version. If an add-on exports an API that can be used by
101
110
  # other add-ons, this is the way to get access to that API.
102
111
  #
@@ -91,14 +91,13 @@ module RubyLsp
91
91
  # The following requests need to be executed in the main thread directly to avoid concurrency issues. Everything
92
92
  # else is pushed into the incoming queue
93
93
  case method
94
- when "initialize", "initialized", "textDocument/didOpen", "textDocument/didClose", "textDocument/didChange"
94
+ when "initialize", "initialized", "textDocument/didOpen", "textDocument/didClose", "textDocument/didChange",
95
+ "$/cancelRequest"
95
96
  process_message(message)
96
97
  when "shutdown"
97
- send_log_message("Shutting down Ruby LSP...")
98
-
99
- shutdown
100
-
101
98
  @mutex.synchronize do
99
+ send_log_message("Shutting down Ruby LSP...")
100
+ shutdown
102
101
  run_shutdown
103
102
  @writer.write(Result.new(id: message[:id], response: nil).to_hash)
104
103
  end
@@ -133,6 +132,12 @@ module RubyLsp
133
132
  @outgoing_queue.pop
134
133
  end
135
134
 
135
+ # This method is only intended to be used in tests! Pushes a message to the incoming queue directly
136
+ sig { params(message: T::Hash[Symbol, T.untyped]).void }
137
+ def push_message(message)
138
+ @incoming_queue << message
139
+ end
140
+
136
141
  sig { abstract.params(message: T::Hash[Symbol, T.untyped]).void }
137
142
  def process_message(message); end
138
143
 
@@ -154,7 +159,11 @@ module RubyLsp
154
159
  # Check if the request was cancelled before trying to process it
155
160
  @mutex.synchronize do
156
161
  if id && @cancelled_requests.include?(id)
157
- send_message(Result.new(id: id, response: nil))
162
+ send_message(Error.new(
163
+ id: id,
164
+ code: Constant::ErrorCodes::REQUEST_CANCELLED,
165
+ message: "Request #{id} was cancelled",
166
+ ))
158
167
  @cancelled_requests.delete(id)
159
168
  next
160
169
  end
@@ -15,6 +15,7 @@ module RubyLsp
15
15
  extend T::Helpers
16
16
  extend T::Generic
17
17
 
18
+ class LocationNotFoundError < StandardError; end
18
19
  ParseResultType = type_member
19
20
 
20
21
  # This maximum number of characters for providing expensive features, like semantic highlighting and diagnostics.
@@ -144,7 +145,15 @@ module RubyLsp
144
145
  def find_char_position(position)
145
146
  # Find the character index for the beginning of the requested line
146
147
  until @current_line == position[:line]
147
- @pos += 1 until LINE_BREAK == @source[@pos]
148
+ until LINE_BREAK == @source[@pos]
149
+ @pos += 1
150
+
151
+ if @pos >= @source.length
152
+ # Pack the code points back into the original string to provide context in the error message
153
+ raise LocationNotFoundError, "Requested position: #{position}\nSource:\n\n#{@source.pack("U*")}"
154
+ end
155
+ end
156
+
148
157
  @pos += 1
149
158
  @current_line += 1
150
159
  end
@@ -26,7 +26,6 @@ require "fileutils"
26
26
  require "ruby-lsp"
27
27
  require "ruby_lsp/base_server"
28
28
  require "ruby_indexer/ruby_indexer"
29
- require "core_ext/uri"
30
29
  require "ruby_lsp/utils"
31
30
  require "ruby_lsp/static_docs"
32
31
  require "ruby_lsp/scope"
@@ -78,6 +77,7 @@ require "ruby_lsp/requests/hover"
78
77
  require "ruby_lsp/requests/inlay_hints"
79
78
  require "ruby_lsp/requests/on_type_formatting"
80
79
  require "ruby_lsp/requests/prepare_type_hierarchy"
80
+ require "ruby_lsp/requests/prepare_rename"
81
81
  require "ruby_lsp/requests/range_formatting"
82
82
  require "ruby_lsp/requests/references"
83
83
  require "ruby_lsp/requests/rename"
@@ -15,7 +15,7 @@ module RubyLsp
15
15
  "bundle exec ruby"
16
16
  rescue Bundler::GemfileNotFound
17
17
  "ruby"
18
- end + " -Itest ",
18
+ end,
19
19
  String,
20
20
  )
21
21
  ACCESS_MODIFIERS = T.let([:public, :private, :protected], T::Array[Symbol])
@@ -198,7 +198,7 @@ module RubyLsp
198
198
 
199
199
  @response_builder << create_code_lens(
200
200
  node,
201
- title: "Run",
201
+ title: "Run",
202
202
  command_name: "rubyLsp.runTest",
203
203
  arguments: arguments,
204
204
  data: { type: "test", **grouping_data },
@@ -206,7 +206,7 @@ module RubyLsp
206
206
 
207
207
  @response_builder << create_code_lens(
208
208
  node,
209
- title: "Run In Terminal",
209
+ title: "Run In Terminal",
210
210
  command_name: "rubyLsp.runTestInTerminal",
211
211
  arguments: arguments,
212
212
  data: { type: "test_in_terminal", **grouping_data },
@@ -229,7 +229,11 @@ module RubyLsp
229
229
  ).returns(String)
230
230
  end
231
231
  def generate_test_command(group_stack: [], spec_name: nil, method_name: nil)
232
- command = BASE_COMMAND + T.must(@path)
232
+ path = T.must(@path)
233
+ command = BASE_COMMAND
234
+ command += " -Itest" if File.fnmatch?("**/test/**/*", path, File::FNM_PATHNAME)
235
+ command += " -Ispec" if File.fnmatch?("**/spec/**/*", path, File::FNM_PATHNAME)
236
+ command += " #{path}"
233
237
 
234
238
  case @global_state.test_library
235
239
  when "minitest"
@@ -97,6 +97,12 @@ module RubyLsp
97
97
  :on_instance_variable_operator_write_node_enter,
98
98
  :on_instance_variable_or_write_node_enter,
99
99
  :on_instance_variable_target_node_enter,
100
+ :on_class_variable_and_write_node_enter,
101
+ :on_class_variable_operator_write_node_enter,
102
+ :on_class_variable_or_write_node_enter,
103
+ :on_class_variable_read_node_enter,
104
+ :on_class_variable_target_node_enter,
105
+ :on_class_variable_write_node_enter,
100
106
  )
101
107
  end
102
108
 
@@ -110,13 +116,14 @@ module RubyLsp
110
116
  name = constant_name(node)
111
117
  return if name.nil?
112
118
 
119
+ range = range_from_location(node.location)
113
120
  candidates = @index.constant_completion_candidates(name, @node_context.nesting)
114
121
  candidates.each do |entries|
115
122
  complete_name = T.must(entries.first).name
116
123
  @response_builder << build_entry_completion(
117
124
  complete_name,
118
125
  name,
119
- range_from_location(node.location),
126
+ range,
120
127
  entries,
121
128
  top_level?(complete_name),
122
129
  )
@@ -246,6 +253,36 @@ module RubyLsp
246
253
  handle_instance_variable_completion(node.name.to_s, node.location)
247
254
  end
248
255
 
256
+ sig { params(node: Prism::ClassVariableAndWriteNode).void }
257
+ def on_class_variable_and_write_node_enter(node)
258
+ handle_class_variable_completion(node.name.to_s, node.name_loc)
259
+ end
260
+
261
+ sig { params(node: Prism::ClassVariableOperatorWriteNode).void }
262
+ def on_class_variable_operator_write_node_enter(node)
263
+ handle_class_variable_completion(node.name.to_s, node.name_loc)
264
+ end
265
+
266
+ sig { params(node: Prism::ClassVariableOrWriteNode).void }
267
+ def on_class_variable_or_write_node_enter(node)
268
+ handle_class_variable_completion(node.name.to_s, node.name_loc)
269
+ end
270
+
271
+ sig { params(node: Prism::ClassVariableTargetNode).void }
272
+ def on_class_variable_target_node_enter(node)
273
+ handle_class_variable_completion(node.name.to_s, node.location)
274
+ end
275
+
276
+ sig { params(node: Prism::ClassVariableReadNode).void }
277
+ def on_class_variable_read_node_enter(node)
278
+ handle_class_variable_completion(node.name.to_s, node.location)
279
+ end
280
+
281
+ sig { params(node: Prism::ClassVariableWriteNode).void }
282
+ def on_class_variable_write_node_enter(node)
283
+ handle_class_variable_completion(node.name.to_s, node.name_loc)
284
+ end
285
+
249
286
  private
250
287
 
251
288
  sig { params(name: String, range: Interface::Range).void }
@@ -326,6 +363,37 @@ module RubyLsp
326
363
  end
327
364
  end
328
365
 
366
+ sig { params(name: String, location: Prism::Location).void }
367
+ def handle_class_variable_completion(name, location)
368
+ type = @type_inferrer.infer_receiver_type(@node_context)
369
+ return unless type
370
+
371
+ range = range_from_location(location)
372
+
373
+ @index.class_variable_completion_candidates(name, type.name).each do |entry|
374
+ variable_name = entry.name
375
+
376
+ label_details = Interface::CompletionItemLabelDetails.new(
377
+ description: entry.file_name,
378
+ )
379
+
380
+ @response_builder << Interface::CompletionItem.new(
381
+ label: variable_name,
382
+ label_details: label_details,
383
+ text_edit: Interface::TextEdit.new(
384
+ range: range,
385
+ new_text: variable_name,
386
+ ),
387
+ kind: Constant::CompletionItemKind::FIELD,
388
+ data: {
389
+ owner_name: entry.owner&.name,
390
+ },
391
+ )
392
+ end
393
+ rescue RubyIndexer::Index::NonExistingNamespaceError
394
+ # If by any chance we haven't indexed the owner, then there's no way to find the right declaration
395
+ end
396
+
329
397
  sig { params(name: String, location: Prism::Location).void }
330
398
  def handle_instance_variable_completion(name, location)
331
399
  # Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able
@@ -335,6 +403,7 @@ module RubyLsp
335
403
  type = @type_inferrer.infer_receiver_type(@node_context)
336
404
  return unless type
337
405
 
406
+ range = range_from_location(location)
338
407
  @index.instance_variable_completion_candidates(name, type.name).each do |entry|
339
408
  variable_name = entry.name
340
409
 
@@ -346,7 +415,7 @@ module RubyLsp
346
415
  label: variable_name,
347
416
  label_details: label_details,
348
417
  text_edit: Interface::TextEdit.new(
349
- range: range_from_location(location),
418
+ range: range,
350
419
  new_text: variable_name,
351
420
  ),
352
421
  kind: Constant::CompletionItemKind::FIELD,
@@ -368,9 +437,9 @@ module RubyLsp
368
437
 
369
438
  return unless path_node_to_complete.is_a?(Prism::StringNode)
370
439
 
371
- matched_indexable_paths = @index.search_require_paths(path_node_to_complete.content)
440
+ matched_uris = @index.search_require_paths(path_node_to_complete.content)
372
441
 
373
- matched_indexable_paths.map!(&:require_path).sort!.each do |path|
442
+ matched_uris.map!(&:require_path).sort!.each do |path|
374
443
  @response_builder << build_completion(T.must(path), path_node_to_complete)
375
444
  end
376
445
  end
@@ -55,6 +55,12 @@ module RubyLsp
55
55
  :on_symbol_node_enter,
56
56
  :on_super_node_enter,
57
57
  :on_forwarding_super_node_enter,
58
+ :on_class_variable_and_write_node_enter,
59
+ :on_class_variable_operator_write_node_enter,
60
+ :on_class_variable_or_write_node_enter,
61
+ :on_class_variable_read_node_enter,
62
+ :on_class_variable_target_node_enter,
63
+ :on_class_variable_write_node_enter,
58
64
  )
59
65
  end
60
66
 
@@ -196,6 +202,36 @@ module RubyLsp
196
202
  handle_super_node_definition
197
203
  end
198
204
 
205
+ sig { params(node: Prism::ClassVariableAndWriteNode).void }
206
+ def on_class_variable_and_write_node_enter(node)
207
+ handle_class_variable_definition(node.name.to_s)
208
+ end
209
+
210
+ sig { params(node: Prism::ClassVariableOperatorWriteNode).void }
211
+ def on_class_variable_operator_write_node_enter(node)
212
+ handle_class_variable_definition(node.name.to_s)
213
+ end
214
+
215
+ sig { params(node: Prism::ClassVariableOrWriteNode).void }
216
+ def on_class_variable_or_write_node_enter(node)
217
+ handle_class_variable_definition(node.name.to_s)
218
+ end
219
+
220
+ sig { params(node: Prism::ClassVariableTargetNode).void }
221
+ def on_class_variable_target_node_enter(node)
222
+ handle_class_variable_definition(node.name.to_s)
223
+ end
224
+
225
+ sig { params(node: Prism::ClassVariableReadNode).void }
226
+ def on_class_variable_read_node_enter(node)
227
+ handle_class_variable_definition(node.name.to_s)
228
+ end
229
+
230
+ sig { params(node: Prism::ClassVariableWriteNode).void }
231
+ def on_class_variable_write_node_enter(node)
232
+ handle_class_variable_definition(node.name.to_s)
233
+ end
234
+
199
235
  private
200
236
 
201
237
  sig { void }
@@ -223,7 +259,7 @@ module RubyLsp
223
259
  location = entry.location
224
260
 
225
261
  @response_builder << Interface::Location.new(
226
- uri: URI::Generic.from_path(path: entry.file_path).to_s,
262
+ uri: entry.uri.to_s,
227
263
  range: Interface::Range.new(
228
264
  start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
229
265
  end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
@@ -232,6 +268,24 @@ module RubyLsp
232
268
  end
233
269
  end
234
270
 
271
+ sig { params(name: String).void }
272
+ def handle_class_variable_definition(name)
273
+ type = @type_inferrer.infer_receiver_type(@node_context)
274
+ return unless type
275
+
276
+ entries = @index.resolve_class_variable(name, type.name)
277
+ return unless entries
278
+
279
+ entries.each do |entry|
280
+ @response_builder << Interface::Location.new(
281
+ uri: entry.uri.to_s,
282
+ range: range_from_location(entry.location),
283
+ )
284
+ end
285
+ rescue RubyIndexer::Index::NonExistingNamespaceError
286
+ # If by any chance we haven't indexed the owner, then there's no way to find the right declaration
287
+ end
288
+
235
289
  sig { params(name: String).void }
236
290
  def handle_instance_variable_definition(name)
237
291
  # Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able
@@ -248,7 +302,7 @@ module RubyLsp
248
302
  location = entry.location
249
303
 
250
304
  @response_builder << Interface::Location.new(
251
- uri: URI::Generic.from_path(path: entry.file_path).to_s,
305
+ uri: entry.uri.to_s,
252
306
  range: Interface::Range.new(
253
307
  start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
254
308
  end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
@@ -275,11 +329,11 @@ module RubyLsp
275
329
  return unless methods
276
330
 
277
331
  methods.each do |target_method|
278
- file_path = target_method.file_path
279
- next if sorbet_level_true_or_higher?(@sorbet_level) && not_in_dependencies?(file_path)
332
+ uri = target_method.uri
333
+ next if sorbet_level_true_or_higher?(@sorbet_level) && not_in_dependencies?(T.must(uri.full_path))
280
334
 
281
335
  @response_builder << Interface::LocationLink.new(
282
- target_uri: URI::Generic.from_path(path: file_path).to_s,
336
+ target_uri: uri.to_s,
283
337
  target_range: range_from_location(target_method.location),
284
338
  target_selection_range: range_from_location(target_method.name_location),
285
339
  )
@@ -290,20 +344,22 @@ module RubyLsp
290
344
  def handle_require_definition(node, message)
291
345
  case message
292
346
  when :require
293
- entry = @index.search_require_paths(node.content).find do |indexable_path|
294
- indexable_path.require_path == node.content
347
+ entry = @index.search_require_paths(node.content).find do |uri|
348
+ uri.require_path == node.content
295
349
  end
296
350
 
297
351
  if entry
298
352
  candidate = entry.full_path
299
353
 
300
- @response_builder << Interface::Location.new(
301
- uri: URI::Generic.from_path(path: candidate).to_s,
302
- range: Interface::Range.new(
303
- start: Interface::Position.new(line: 0, character: 0),
304
- end: Interface::Position.new(line: 0, character: 0),
305
- ),
306
- )
354
+ if candidate
355
+ @response_builder << Interface::Location.new(
356
+ uri: URI::Generic.from_path(path: candidate).to_s,
357
+ range: Interface::Range.new(
358
+ start: Interface::Position.new(line: 0, character: 0),
359
+ end: Interface::Position.new(line: 0, character: 0),
360
+ ),
361
+ )
362
+ end
307
363
  end
308
364
  when :require_relative
309
365
  required_file = "#{node.content}.rb"
@@ -346,11 +402,11 @@ module RubyLsp
346
402
  # If the project has Sorbet, then we only want to handle go to definition for constants defined in gems, as an
347
403
  # additional behavior on top of jumping to RBIs. The only sigil where Sorbet cannot handle constants is typed
348
404
  # ignore
349
- file_path = entry.file_path
350
- next if @sorbet_level != RubyDocument::SorbetLevel::Ignore && not_in_dependencies?(file_path)
405
+ uri = entry.uri
406
+ next if @sorbet_level != RubyDocument::SorbetLevel::Ignore && not_in_dependencies?(T.must(uri.full_path))
351
407
 
352
408
  @response_builder << Interface::LocationLink.new(
353
- target_uri: URI::Generic.from_path(path: file_path).to_s,
409
+ target_uri: uri.to_s,
354
410
  target_range: range_from_location(entry.location),
355
411
  target_selection_range: range_from_location(entry.name_location),
356
412
  )
@@ -41,6 +41,7 @@ module RubyLsp
41
41
  :on_module_node_enter,
42
42
  :on_module_node_leave,
43
43
  :on_instance_variable_write_node_enter,
44
+ :on_instance_variable_target_node_enter,
44
45
  :on_instance_variable_operator_write_node_enter,
45
46
  :on_instance_variable_or_write_node_enter,
46
47
  :on_instance_variable_and_write_node_enter,
@@ -272,6 +273,16 @@ module RubyLsp
272
273
  )
273
274
  end
274
275
 
276
+ sig { params(node: Prism::InstanceVariableTargetNode).void }
277
+ def on_instance_variable_target_node_enter(node)
278
+ create_document_symbol(
279
+ name: node.name.to_s,
280
+ kind: Constant::SymbolKind::FIELD,
281
+ range_location: node.location,
282
+ selection_range_location: node.location,
283
+ )
284
+ end
285
+
275
286
  sig { params(node: Prism::InstanceVariableOperatorWriteNode).void }
276
287
  def on_instance_variable_operator_write_node_enter(node)
277
288
  create_document_symbol(
@@ -329,7 +340,7 @@ module RubyLsp
329
340
  ).returns(Interface::DocumentSymbol)
330
341
  end
331
342
  def create_document_symbol(name:, kind:, range_location:, selection_range_location:)
332
- name = "<blank>" if name.empty?
343
+ name = "<blank>" if name.strip.empty?
333
344
  symbol = Interface::DocumentSymbol.new(
334
345
  name: name,
335
346
  kind: kind,
@@ -195,7 +195,7 @@ module RubyLsp
195
195
  def push_comment_ranges
196
196
  # Group comments that are on consecutive lines and then push ranges for each group that has at least 2 comments
197
197
  @comments.chunk_while do |this, other|
198
- this.location.end_line + 1 == other.location.start_line
198
+ this.location.end_line + 1 == other.location.start_line && !this.trailing? && !other.trailing?
199
199
  end.each do |chunk|
200
200
  next if chunk.length == 1
201
201
 
@@ -31,6 +31,12 @@ module RubyLsp
31
31
  Prism::SuperNode,
32
32
  Prism::ForwardingSuperNode,
33
33
  Prism::YieldNode,
34
+ Prism::ClassVariableAndWriteNode,
35
+ Prism::ClassVariableOperatorWriteNode,
36
+ Prism::ClassVariableOrWriteNode,
37
+ Prism::ClassVariableReadNode,
38
+ Prism::ClassVariableTargetNode,
39
+ Prism::ClassVariableWriteNode,
34
40
  ],
35
41
  T::Array[T.class_of(Prism::Node)],
36
42
  )
@@ -85,6 +91,12 @@ module RubyLsp
85
91
  :on_string_node_enter,
86
92
  :on_interpolated_string_node_enter,
87
93
  :on_yield_node_enter,
94
+ :on_class_variable_and_write_node_enter,
95
+ :on_class_variable_operator_write_node_enter,
96
+ :on_class_variable_or_write_node_enter,
97
+ :on_class_variable_read_node_enter,
98
+ :on_class_variable_target_node_enter,
99
+ :on_class_variable_write_node_enter,
88
100
  )
89
101
  end
90
102
 
@@ -215,6 +227,36 @@ module RubyLsp
215
227
  handle_keyword_documentation(node.keyword)
216
228
  end
217
229
 
230
+ sig { params(node: Prism::ClassVariableAndWriteNode).void }
231
+ def on_class_variable_and_write_node_enter(node)
232
+ handle_class_variable_hover(node.name.to_s)
233
+ end
234
+
235
+ sig { params(node: Prism::ClassVariableOperatorWriteNode).void }
236
+ def on_class_variable_operator_write_node_enter(node)
237
+ handle_class_variable_hover(node.name.to_s)
238
+ end
239
+
240
+ sig { params(node: Prism::ClassVariableOrWriteNode).void }
241
+ def on_class_variable_or_write_node_enter(node)
242
+ handle_class_variable_hover(node.name.to_s)
243
+ end
244
+
245
+ sig { params(node: Prism::ClassVariableTargetNode).void }
246
+ def on_class_variable_target_node_enter(node)
247
+ handle_class_variable_hover(node.name.to_s)
248
+ end
249
+
250
+ sig { params(node: Prism::ClassVariableReadNode).void }
251
+ def on_class_variable_read_node_enter(node)
252
+ handle_class_variable_hover(node.name.to_s)
253
+ end
254
+
255
+ sig { params(node: Prism::ClassVariableWriteNode).void }
256
+ def on_class_variable_write_node_enter(node)
257
+ handle_class_variable_hover(node.name.to_s)
258
+ end
259
+
218
260
  private
219
261
 
220
262
  sig { params(node: T.any(Prism::InterpolatedStringNode, Prism::StringNode)).void }
@@ -317,6 +359,21 @@ module RubyLsp
317
359
  end
318
360
  end
319
361
 
362
+ sig { params(name: String).void }
363
+ def handle_class_variable_hover(name)
364
+ type = @type_inferrer.infer_receiver_type(@node_context)
365
+ return unless type
366
+
367
+ entries = @index.resolve_class_variable(name, type.name)
368
+ return unless entries
369
+
370
+ categorized_markdown_from_index_entries(name, entries).each do |category, content|
371
+ @response_builder.push(content, category: category)
372
+ end
373
+ rescue RubyIndexer::Index::NonExistingNamespaceError
374
+ # If by any chance we haven't indexed the owner, then there's no way to find the right declaration
375
+ end
376
+
320
377
  sig { params(name: String, location: Prism::Location).void }
321
378
  def generate_hover(name, location)
322
379
  entries = @index.resolve(name, @node_context.nesting)
@@ -62,6 +62,12 @@ module RubyLsp
62
62
  Prism::InstanceVariableOrWriteNode,
63
63
  Prism::InstanceVariableTargetNode,
64
64
  Prism::InstanceVariableWriteNode,
65
+ Prism::ClassVariableAndWriteNode,
66
+ Prism::ClassVariableOperatorWriteNode,
67
+ Prism::ClassVariableOrWriteNode,
68
+ Prism::ClassVariableReadNode,
69
+ Prism::ClassVariableTargetNode,
70
+ Prism::ClassVariableWriteNode,
65
71
  ],
66
72
  code_units_cache: document.code_units_cache,
67
73
  )
@@ -49,7 +49,8 @@ module RubyLsp
49
49
  if owner_name
50
50
  entries = entries.select do |entry|
51
51
  (entry.is_a?(RubyIndexer::Entry::Member) || entry.is_a?(RubyIndexer::Entry::InstanceVariable) ||
52
- entry.is_a?(RubyIndexer::Entry::MethodAlias)) && entry.owner&.name == owner_name
52
+ entry.is_a?(RubyIndexer::Entry::MethodAlias) || entry.is_a?(RubyIndexer::Entry::ClassVariable)) &&
53
+ entry.owner&.name == owner_name
53
54
  end
54
55
  end
55
56
 
@@ -56,6 +56,12 @@ module RubyLsp
56
56
  Prism::StringNode,
57
57
  Prism::SuperNode,
58
58
  Prism::ForwardingSuperNode,
59
+ Prism::ClassVariableAndWriteNode,
60
+ Prism::ClassVariableOperatorWriteNode,
61
+ Prism::ClassVariableOrWriteNode,
62
+ Prism::ClassVariableReadNode,
63
+ Prism::ClassVariableTargetNode,
64
+ Prism::ClassVariableWriteNode,
59
65
  ],
60
66
  code_units_cache: document.code_units_cache,
61
67
  )