ruby-lsp 0.23.20 → 0.26.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/exe/ruby-lsp +10 -4
  4. data/exe/ruby-lsp-check +0 -4
  5. data/exe/ruby-lsp-launcher +25 -11
  6. data/exe/ruby-lsp-test-exec +6 -0
  7. data/lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb +0 -1
  8. data/lib/rubocop/cop/ruby_lsp/use_register_with_handler_method.rb +0 -1
  9. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +7 -1
  10. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +1 -4
  11. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +10 -19
  12. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +29 -7
  13. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +2 -2
  14. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +12 -8
  15. data/lib/ruby_indexer/test/configuration_test.rb +1 -2
  16. data/lib/ruby_indexer/test/index_test.rb +39 -0
  17. data/lib/ruby_indexer/test/instance_variables_test.rb +24 -0
  18. data/lib/ruby_indexer/test/method_test.rb +17 -0
  19. data/lib/ruby_indexer/test/rbs_indexer_test.rb +2 -2
  20. data/lib/ruby_indexer/test/reference_finder_test.rb +79 -14
  21. data/lib/ruby_lsp/addon.rb +44 -15
  22. data/lib/ruby_lsp/base_server.rb +34 -26
  23. data/lib/ruby_lsp/document.rb +162 -52
  24. data/lib/ruby_lsp/erb_document.rb +8 -3
  25. data/lib/ruby_lsp/global_state.rb +21 -0
  26. data/lib/ruby_lsp/internal.rb +0 -2
  27. data/lib/ruby_lsp/listeners/completion.rb +14 -3
  28. data/lib/ruby_lsp/listeners/hover.rb +7 -0
  29. data/lib/ruby_lsp/listeners/inlay_hints.rb +5 -3
  30. data/lib/ruby_lsp/listeners/spec_style.rb +126 -67
  31. data/lib/ruby_lsp/listeners/test_discovery.rb +18 -15
  32. data/lib/ruby_lsp/listeners/test_style.rb +56 -23
  33. data/lib/ruby_lsp/requests/code_action_resolve.rb +3 -3
  34. data/lib/ruby_lsp/requests/code_lens.rb +14 -5
  35. data/lib/ruby_lsp/requests/completion.rb +1 -1
  36. data/lib/ruby_lsp/requests/definition.rb +1 -1
  37. data/lib/ruby_lsp/requests/discover_tests.rb +2 -2
  38. data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
  39. data/lib/ruby_lsp/requests/hover.rb +1 -1
  40. data/lib/ruby_lsp/requests/inlay_hints.rb +3 -3
  41. data/lib/ruby_lsp/requests/on_type_formatting.rb +1 -1
  42. data/lib/ruby_lsp/requests/prepare_rename.rb +1 -1
  43. data/lib/ruby_lsp/requests/references.rb +10 -6
  44. data/lib/ruby_lsp/requests/rename.rb +8 -6
  45. data/lib/ruby_lsp/requests/request.rb +6 -7
  46. data/lib/ruby_lsp/requests/selection_ranges.rb +1 -1
  47. data/lib/ruby_lsp/requests/show_syntax_tree.rb +1 -1
  48. data/lib/ruby_lsp/requests/signature_help.rb +1 -1
  49. data/lib/ruby_lsp/requests/support/common.rb +1 -3
  50. data/lib/ruby_lsp/requests/support/formatter.rb +16 -15
  51. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +2 -2
  52. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +13 -3
  53. data/lib/ruby_lsp/response_builders/response_builder.rb +6 -8
  54. data/lib/ruby_lsp/ruby_document.rb +10 -5
  55. data/lib/ruby_lsp/server.rb +95 -110
  56. data/lib/ruby_lsp/setup_bundler.rb +59 -25
  57. data/lib/ruby_lsp/static_docs.rb +1 -0
  58. data/lib/ruby_lsp/store.rb +0 -10
  59. data/lib/ruby_lsp/test_helper.rb +1 -4
  60. data/lib/ruby_lsp/test_reporters/lsp_reporter.rb +18 -7
  61. data/lib/ruby_lsp/test_reporters/minitest_reporter.rb +54 -7
  62. data/lib/ruby_lsp/test_reporters/test_unit_reporter.rb +0 -1
  63. data/lib/ruby_lsp/utils.rb +47 -11
  64. data/static_docs/break.md +103 -0
  65. metadata +7 -19
  66. data/lib/ruby_lsp/load_sorbet.rb +0 -62
@@ -216,22 +216,43 @@ module RubyIndexer
216
216
  assert_equal(11, refs[2].location.start_line)
217
217
  end
218
218
 
219
- def test_finds_instance_variable_read_references
220
- refs = find_instance_variable_references("@foo", <<~RUBY)
219
+ def test_finds_instance_variable_references
220
+ refs = find_instance_variable_references("@name", ["Foo"], <<~RUBY)
221
221
  class Foo
222
- def foo
223
- @foo
222
+ def initialize
223
+ @name = "foo"
224
+ end
225
+ def name
226
+ @name
227
+ end
228
+ def name_capital
229
+ @name[0]
230
+ end
231
+ end
232
+
233
+ class Bar
234
+ def initialize
235
+ @name = "foo"
236
+ end
237
+ def name
238
+ @name
224
239
  end
225
240
  end
226
241
  RUBY
227
- assert_equal(1, refs.size)
242
+ assert_equal(3, refs.size)
228
243
 
229
- assert_equal("@foo", refs[0].name)
244
+ assert_equal("@name", refs[0].name)
230
245
  assert_equal(3, refs[0].location.start_line)
246
+
247
+ assert_equal("@name", refs[1].name)
248
+ assert_equal(6, refs[1].location.start_line)
249
+
250
+ assert_equal("@name", refs[2].name)
251
+ assert_equal(9, refs[2].location.start_line)
231
252
  end
232
253
 
233
254
  def test_finds_instance_variable_write_references
234
- refs = find_instance_variable_references("@foo", <<~RUBY)
255
+ refs = find_instance_variable_references("@foo", ["Foo"], <<~RUBY)
235
256
  class Foo
236
257
  def write
237
258
  @foo = 1
@@ -252,26 +273,70 @@ module RubyIndexer
252
273
  assert_equal(7, refs[4].location.start_line)
253
274
  end
254
275
 
255
- def test_finds_instance_variable_references_ignore_context
256
- refs = find_instance_variable_references("@name", <<~RUBY)
257
- class Foo
276
+ def test_finds_instance_variable_references_in_owner_ancestors
277
+ refs = find_instance_variable_references("@name", ["Foo", "Base", "Top", "Parent"], <<~RUBY)
278
+ module Base
279
+ def change_name(name)
280
+ @name = name
281
+ end
258
282
  def name
283
+ @name
284
+ end
285
+
286
+ module ::Top
287
+ def name
288
+ @name
289
+ end
290
+ end
291
+ end
292
+
293
+ class Parent
294
+ def initialize
295
+ @name = "parent"
296
+ end
297
+ def name_capital
298
+ @name[0]
299
+ end
300
+ end
301
+
302
+ class Foo < Parent
303
+ include Base
304
+ def initialize
259
305
  @name = "foo"
260
306
  end
307
+ def name
308
+ @name
309
+ end
261
310
  end
311
+
262
312
  class Bar
263
313
  def name
264
314
  @name = "bar"
265
315
  end
266
316
  end
267
317
  RUBY
268
- assert_equal(2, refs.size)
318
+ assert_equal(7, refs.size)
269
319
 
270
320
  assert_equal("@name", refs[0].name)
271
321
  assert_equal(3, refs[0].location.start_line)
272
322
 
273
323
  assert_equal("@name", refs[1].name)
274
- assert_equal(8, refs[1].location.start_line)
324
+ assert_equal(6, refs[1].location.start_line)
325
+
326
+ assert_equal("@name", refs[2].name)
327
+ assert_equal(11, refs[2].location.start_line)
328
+
329
+ assert_equal("@name", refs[3].name)
330
+ assert_equal(18, refs[3].location.start_line)
331
+
332
+ assert_equal("@name", refs[4].name)
333
+ assert_equal(21, refs[4].location.start_line)
334
+
335
+ assert_equal("@name", refs[5].name)
336
+ assert_equal(28, refs[5].location.start_line)
337
+
338
+ assert_equal("@name", refs[6].name)
339
+ assert_equal(31, refs[6].location.start_line)
275
340
  end
276
341
 
277
342
  def test_accounts_for_reopened_classes
@@ -310,8 +375,8 @@ module RubyIndexer
310
375
  find_references(target, source)
311
376
  end
312
377
 
313
- def find_instance_variable_references(instance_variable_name, source)
314
- target = ReferenceFinder::InstanceVariableTarget.new(instance_variable_name)
378
+ def find_instance_variable_references(instance_variable_name, owner_ancestors, source)
379
+ target = ReferenceFinder::InstanceVariableTarget.new(instance_variable_name, owner_ancestors)
315
380
  find_references(target, source)
316
381
  end
317
382
 
@@ -19,12 +19,8 @@ module RubyLsp
19
19
  # end
20
20
  # end
21
21
  # ```
22
+ # @abstract
22
23
  class Addon
23
- extend T::Sig
24
- extend T::Helpers
25
-
26
- abstract!
27
-
28
24
  @addons = [] #: Array[Addon]
29
25
  @addon_classes = [] #: Array[singleton(Addon)]
30
26
  # Add-on instances that have declared a handler to accept file watcher events
@@ -60,7 +56,28 @@ module RubyLsp
60
56
  addon_files = Gem.find_files("ruby_lsp/**/addon.rb")
61
57
 
62
58
  if include_project_addons
63
- addon_files.concat(Dir.glob(File.join(global_state.workspace_path, "**", "ruby_lsp/**/addon.rb")))
59
+ project_addons = Dir.glob("#{global_state.workspace_path}/**/ruby_lsp/**/addon.rb")
60
+ bundle_path = Bundler.bundle_path.to_s
61
+ gems_dir = Bundler.bundle_path.join("gems")
62
+
63
+ # Create an array of rejection glob patterns to ignore add-ons already discovered through Gem.find_files if
64
+ # they are also copied inside the workspace for whatever reason. We received reports of projects having gems
65
+ # installed in vendor/bundle despite BUNDLE_PATH pointing elsewhere. Without this mechanism, we will
66
+ # double-require the same add-on, potentially for different versions of the same gem, which leads to incorrect
67
+ # behavior
68
+ reject_glob_patterns = addon_files.map do |path|
69
+ relative_gem_path = Pathname.new(path).relative_path_from(gems_dir)
70
+ first_part, *parts = relative_gem_path.to_s.split(File::SEPARATOR)
71
+ first_part&.gsub!(/-([0-9.]+)$/, "*")
72
+ "**/#{first_part}/#{parts.join("/")}"
73
+ end
74
+
75
+ project_addons.reject! do |path|
76
+ path.start_with?(bundle_path) ||
77
+ reject_glob_patterns.any? { |pattern| File.fnmatch?(pattern, path, File::Constants::FNM_PATHNAME) }
78
+ end
79
+
80
+ addon_files.concat(project_addons)
64
81
  end
65
82
 
66
83
  errors = addon_files.filter_map do |addon_path|
@@ -178,22 +195,34 @@ module RubyLsp
178
195
 
179
196
  # Each add-on should implement `MyAddon#activate` and use to perform any sort of initialization, such as
180
197
  # reading information into memory or even spawning a separate process
181
- sig { abstract.params(global_state: GlobalState, outgoing_queue: Thread::Queue).void }
182
- def activate(global_state, outgoing_queue); end
198
+ # @abstract
199
+ #: (GlobalState, Thread::Queue) -> void
200
+ def activate(global_state, outgoing_queue)
201
+ raise AbstractMethodInvokedError
202
+ end
183
203
 
184
- # Each add-on should implement `MyAddon#deactivate` and use to perform any clean up, like shutting down a
204
+ # Each add-on must implement `MyAddon#deactivate` and use to perform any clean up, like shutting down a
185
205
  # child process
186
- sig { abstract.void }
187
- def deactivate; end
206
+ # @abstract
207
+ #: -> void
208
+ def deactivate
209
+ raise AbstractMethodInvokedError
210
+ end
188
211
 
189
212
  # Add-ons should override the `name` method to return the add-on name
190
- sig { abstract.returns(String) }
191
- def name; end
213
+ # @abstract
214
+ #: -> String
215
+ def name
216
+ raise AbstractMethodInvokedError
217
+ end
192
218
 
193
219
  # Add-ons should override the `version` method to return a semantic version string representing the add-on's
194
220
  # version. This is used for compatibility checks
195
- sig { abstract.returns(String) }
196
- def version; end
221
+ # @abstract
222
+ #: -> String
223
+ def version
224
+ raise AbstractMethodInvokedError
225
+ end
197
226
 
198
227
  # Handle a response from a window/showMessageRequest request. Add-ons must include the addon_name as part of the
199
228
  # original request so that the response is delegated to the correct add-on and must override this method to handle
@@ -2,19 +2,15 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module RubyLsp
5
+ # @abstract
5
6
  class BaseServer
6
- extend T::Sig
7
- extend T::Helpers
8
-
9
- abstract!
10
-
11
7
  #: (**untyped options) -> void
12
8
  def initialize(**options)
9
+ @reader = MessageReader.new(options[:reader] || $stdin) #: MessageReader
10
+ @writer = MessageWriter.new(options[:writer] || $stdout) #: MessageWriter
13
11
  @test_mode = options[:test_mode] #: bool?
14
12
  @setup_error = options[:setup_error] #: StandardError?
15
13
  @install_error = options[:install_error] #: StandardError?
16
- @writer = Transport::Stdio::Writer.new #: Transport::Stdio::Writer
17
- @reader = Transport::Stdio::Reader.new #: Transport::Stdio::Reader
18
14
  @incoming_queue = Thread::Queue.new #: Thread::Queue
19
15
  @outgoing_queue = Thread::Queue.new #: Thread::Queue
20
16
  @cancelled_requests = [] #: Array[Integer]
@@ -40,7 +36,7 @@ module RubyLsp
40
36
 
41
37
  #: -> void
42
38
  def start
43
- @reader.read do |message|
39
+ @reader.each_message do |message|
44
40
  method = message[:method]
45
41
 
46
42
  # We must parse the document under a mutex lock or else we might switch threads and accept text edits in the
@@ -87,8 +83,7 @@ module RubyLsp
87
83
  # The following requests need to be executed in the main thread directly to avoid concurrency issues. Everything
88
84
  # else is pushed into the incoming queue
89
85
  case method
90
- when "initialize", "initialized", "textDocument/didOpen", "textDocument/didClose", "textDocument/didChange",
91
- "rubyLsp/diagnoseState"
86
+ when "initialize", "initialized", "rubyLsp/diagnoseState"
92
87
  process_message(message)
93
88
  when "shutdown"
94
89
  @global_state.synchronize do
@@ -98,13 +93,36 @@ module RubyLsp
98
93
  @writer.write(Result.new(id: message[:id], response: nil).to_hash)
99
94
  end
100
95
  when "exit"
101
- @global_state.synchronize { exit(@incoming_queue.closed? ? 0 : 1) }
96
+ exit(@incoming_queue.closed? ? 0 : 1)
102
97
  else
103
98
  @incoming_queue << message
104
99
  end
105
100
  end
106
101
  end
107
102
 
103
+ # This method is only intended to be used in tests! Pops the latest response that would be sent to the client
104
+ #: -> untyped
105
+ def pop_response
106
+ @outgoing_queue.pop
107
+ end
108
+
109
+ # This method is only intended to be used in tests! Pushes a message to the incoming queue directly
110
+ #: (Hash[Symbol, untyped] message) -> void
111
+ def push_message(message)
112
+ @incoming_queue << message
113
+ end
114
+
115
+ # @abstract
116
+ #: (Hash[Symbol, untyped] message) -> void
117
+ def process_message(message)
118
+ raise AbstractMethodInvokedError
119
+ end
120
+
121
+ #: -> bool?
122
+ def test_mode?
123
+ @test_mode
124
+ end
125
+
108
126
  #: -> void
109
127
  def run_shutdown
110
128
  @incoming_queue.clear
@@ -118,24 +136,14 @@ module RubyLsp
118
136
  @store.clear
119
137
  end
120
138
 
121
- # This method is only intended to be used in tests! Pops the latest response that would be sent to the client
122
- #: -> untyped
123
- def pop_response
124
- @outgoing_queue.pop
125
- end
139
+ private
126
140
 
127
- # This method is only intended to be used in tests! Pushes a message to the incoming queue directly
128
- #: (Hash[Symbol, untyped] message) -> void
129
- def push_message(message)
130
- @incoming_queue << message
141
+ # @abstract
142
+ #: -> void
143
+ def shutdown
144
+ raise AbstractMethodInvokedError
131
145
  end
132
146
 
133
- sig { abstract.params(message: T::Hash[Symbol, T.untyped]).void }
134
- def process_message(message); end
135
-
136
- sig { abstract.void }
137
- def shutdown; end
138
-
139
147
  #: (Integer id, String message, ?type: Integer) -> void
140
148
  def fail_request_and_notify(id, message, type: Constant::MessageType::INFO)
141
149
  send_message(Error.new(id: id, code: Constant::ErrorCodes::REQUEST_FAILED, message: message))
@@ -2,21 +2,16 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module RubyLsp
5
+ # @abstract
5
6
  #: [ParseResultType]
6
7
  class Document
7
- extend T::Sig
8
- extend T::Helpers
9
- extend T::Generic
10
-
11
- class LocationNotFoundError < StandardError; end
8
+ class InvalidLocationError < StandardError; end
12
9
 
13
10
  # This maximum number of characters for providing expensive features, like semantic highlighting and diagnostics.
14
11
  # This is the same number used by the TypeScript extension in VS Code
15
12
  MAXIMUM_CHARACTERS_FOR_EXPENSIVE_FEATURES = 100_000
16
13
  EMPTY_CACHE = Object.new.freeze #: Object
17
14
 
18
- abstract!
19
-
20
15
  #: ParseResultType
21
16
  attr_reader :parse_result
22
17
 
@@ -63,8 +58,11 @@ module RubyLsp
63
58
  self.class == other.class && uri == other.uri && @source == other.source
64
59
  end
65
60
 
66
- sig { abstract.returns(Symbol) }
67
- def language_id; end
61
+ # @abstract
62
+ #: -> Symbol
63
+ def language_id
64
+ raise AbstractMethodInvokedError
65
+ end
68
66
 
69
67
  #: [T] (String request_name) { (Document[ParseResultType] document) -> T } -> T
70
68
  def cache_fetch(request_name, &block)
@@ -122,11 +120,17 @@ module RubyLsp
122
120
  end
123
121
 
124
122
  # Returns `true` if the document was parsed and `false` if nothing needed parsing
125
- sig { abstract.returns(T::Boolean) }
126
- def parse!; end
123
+ # @abstract
124
+ #: -> bool
125
+ def parse!
126
+ raise AbstractMethodInvokedError
127
+ end
127
128
 
128
- sig { abstract.returns(T::Boolean) }
129
- def syntax_error?; end
129
+ # @abstract
130
+ #: -> bool
131
+ def syntax_error?
132
+ raise AbstractMethodInvokedError
133
+ end
130
134
 
131
135
  #: -> bool
132
136
  def past_expensive_limit?
@@ -135,27 +139,28 @@ module RubyLsp
135
139
 
136
140
  #: (Hash[Symbol, untyped] start_pos, ?Hash[Symbol, untyped]? end_pos) -> [Integer, Integer?]
137
141
  def find_index_by_position(start_pos, end_pos = nil)
138
- @global_state.synchronize do
139
- scanner = create_scanner
140
- start_index = scanner.find_char_position(start_pos)
141
- end_index = scanner.find_char_position(end_pos) if end_pos
142
- [start_index, end_index]
143
- end
142
+ scanner = create_scanner
143
+ start_index = scanner.find_char_position(start_pos)
144
+ end_index = scanner.find_char_position(end_pos) if end_pos
145
+ [start_index, end_index]
144
146
  end
145
147
 
146
148
  private
147
149
 
148
150
  #: -> Scanner
149
151
  def create_scanner
150
- Scanner.new(@source, @encoding)
152
+ case @encoding
153
+ when Encoding::UTF_8
154
+ Utf8Scanner.new(@source)
155
+ when Encoding::UTF_16LE
156
+ Utf16Scanner.new(@source)
157
+ else
158
+ Utf32Scanner.new(@source)
159
+ end
151
160
  end
152
161
 
162
+ # @abstract
153
163
  class Edit
154
- extend T::Sig
155
- extend T::Helpers
156
-
157
- abstract!
158
-
159
164
  #: Hash[Symbol, untyped]
160
165
  attr_reader :range
161
166
 
@@ -169,33 +174,114 @@ module RubyLsp
169
174
  class Replace < Edit; end
170
175
  class Delete < Edit; end
171
176
 
177
+ # Parent class for all position scanners. Scanners are used to translate a position given by the editor into a
178
+ # string index that we can use to find the right place in the document source. The logic for finding the correct
179
+ # index depends on the encoding negotiated with the editor, so we have different subclasses for each encoding.
180
+ # See https://microsoft.github.io/language-server-protocol/specification/#positionEncodingKind for more information
181
+ # @abstract
172
182
  class Scanner
173
- extend T::Sig
174
-
175
183
  LINE_BREAK = 0x0A #: Integer
176
184
  # After character 0xFFFF, UTF-16 considers characters to have length 2 and we have to account for that
177
185
  SURROGATE_PAIR_START = 0xFFFF #: Integer
178
186
 
179
- #: (String source, Encoding encoding) -> void
180
- def initialize(source, encoding)
187
+ #: -> void
188
+ def initialize
181
189
  @current_line = 0 #: Integer
182
190
  @pos = 0 #: Integer
183
- @source = source.codepoints #: Array[Integer]
184
- @encoding = encoding
185
191
  end
186
192
 
187
- # Finds the character index inside the source string for a given line and column
193
+ # Finds the character index inside the source string for a given line and column. This method always returns the
194
+ # character index regardless of whether we are searching positions based on bytes, code units, or codepoints.
195
+ # @abstract
196
+ #: (Hash[Symbol, untyped] position) -> Integer
197
+ def find_char_position(position)
198
+ raise AbstractMethodInvokedError
199
+ end
200
+ end
201
+
202
+ # For the UTF-8 encoding, positions correspond to bytes
203
+ class Utf8Scanner < Scanner
204
+ #: (String source) -> void
205
+ def initialize(source)
206
+ super()
207
+ @bytes = source.bytes #: Array[Integer]
208
+ @character_length = 0 #: Integer
209
+ end
210
+
211
+ # @override
212
+ #: (Hash[Symbol, untyped] position) -> Integer
213
+ def find_char_position(position)
214
+ # Each group of bytes is a character. We advance based on the number of bytes to count how many full characters
215
+ # we have in the requested offset
216
+ until @current_line == position[:line]
217
+ byte = @bytes[@pos] #: Integer?
218
+ raise InvalidLocationError unless byte
219
+
220
+ until LINE_BREAK == byte
221
+ @pos += character_byte_length(byte)
222
+ @character_length += 1
223
+ byte = @bytes[@pos]
224
+ raise InvalidLocationError unless byte
225
+ end
226
+
227
+ @pos += 1
228
+ @character_length += 1
229
+ @current_line += 1
230
+ end
231
+
232
+ # @character_length has the number of characters until the beginning of the line. We don't accumulate on it for
233
+ # the character part because locating the same position twice must return the same value
234
+ line_byte_offset = 0
235
+ line_characters = 0
236
+
237
+ while line_byte_offset < position[:character]
238
+ byte = @bytes[@pos + line_byte_offset] #: Integer?
239
+ raise InvalidLocationError unless byte
240
+
241
+ line_byte_offset += character_byte_length(byte)
242
+ line_characters += 1
243
+ end
244
+
245
+ @character_length + line_characters
246
+ end
247
+
248
+ private
249
+
250
+ #: (Integer) -> Integer
251
+ def character_byte_length(byte)
252
+ if byte < 0x80 # 1-byte character
253
+ 1
254
+ elsif byte < 0xE0 # 2-byte character
255
+ 2
256
+ elsif byte < 0xF0 # 3-byte character
257
+ 3
258
+ else # 4-byte character
259
+ 4
260
+ end
261
+ end
262
+ end
263
+
264
+ # For the UTF-16 encoding, positions correspond to UTF-16 code units, which count characters beyond the surrogate
265
+ # pair as length 2
266
+ class Utf16Scanner < Scanner
267
+ #: (String) -> void
268
+ def initialize(source)
269
+ super()
270
+ @codepoints = source.codepoints #: Array[Integer]
271
+ end
272
+
273
+ # @override
188
274
  #: (Hash[Symbol, untyped] position) -> Integer
189
275
  def find_char_position(position)
190
276
  # Find the character index for the beginning of the requested line
191
277
  until @current_line == position[:line]
192
- until LINE_BREAK == @source[@pos]
193
- @pos += 1
278
+ codepoint = @codepoints[@pos] #: Integer?
279
+ raise InvalidLocationError unless codepoint
194
280
 
195
- if @pos >= @source.length
196
- # Pack the code points back into the original string to provide context in the error message
197
- raise LocationNotFoundError, "Requested position: #{position}\nSource:\n\n#{@source.pack("U*")}"
198
- end
281
+ until LINE_BREAK == @codepoints[@pos]
282
+ @pos += 1
283
+ codepoint = @codepoints[@pos] #: Integer?
284
+ raise InvalidLocationError unless codepoint
199
285
  end
200
286
 
201
287
  @pos += 1
@@ -204,29 +290,53 @@ module RubyLsp
204
290
 
205
291
  # The final position is the beginning of the line plus the requested column. If the encoding is UTF-16, we also
206
292
  # need to adjust for surrogate pairs
207
- requested_position = @pos + position[:character]
293
+ line_characters = 0
294
+ line_code_units = 0
208
295
 
209
- if @encoding == Encoding::UTF_16LE
210
- requested_position -= utf_16_character_position_correction(@pos, requested_position)
296
+ while line_code_units < position[:character]
297
+ code_point = @codepoints[@pos + line_characters]
298
+ raise InvalidLocationError unless code_point
299
+
300
+ line_code_units += if code_point > SURROGATE_PAIR_START
301
+ 2 # Surrogate pair, so we skip the next code unit
302
+ else
303
+ 1 # Single code unit character
304
+ end
305
+
306
+ line_characters += 1
211
307
  end
212
308
 
213
- requested_position
309
+ @pos + line_characters
214
310
  end
311
+ end
215
312
 
216
- # Subtract 1 for each character after 0xFFFF in the current line from the column position, so that we hit the
217
- # right character in the UTF-8 representation
218
- #: (Integer current_position, Integer requested_position) -> Integer
219
- def utf_16_character_position_correction(current_position, requested_position)
220
- utf16_unicode_correction = 0
313
+ # For the UTF-32 encoding, positions correspond directly to codepoints
314
+ class Utf32Scanner < Scanner
315
+ #: (String) -> void
316
+ def initialize(source)
317
+ super()
318
+ @codepoints = source.codepoints #: Array[Integer]
319
+ end
320
+
321
+ # @override
322
+ #: (Hash[Symbol, untyped] position) -> Integer
323
+ def find_char_position(position)
324
+ # Find the character index for the beginning of the requested line
325
+ until @current_line == position[:line]
326
+ codepoint = @codepoints[@pos] #: Integer?
327
+ raise InvalidLocationError unless codepoint
221
328
 
222
- until current_position == requested_position
223
- codepoint = @source[current_position]
224
- utf16_unicode_correction += 1 if codepoint && codepoint > SURROGATE_PAIR_START
329
+ until LINE_BREAK == @codepoints[@pos]
330
+ @pos += 1
331
+ codepoint = @codepoints[@pos] #: Integer?
332
+ raise InvalidLocationError unless codepoint
333
+ end
225
334
 
226
- current_position += 1
335
+ @pos += 1
336
+ @current_line += 1
227
337
  end
228
338
 
229
- utf16_unicode_correction
339
+ @pos + position[:character]
230
340
  end
231
341
  end
232
342
  end
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module RubyLsp
5
- #: [ParseResultType = Prism::ParseResult]
5
+ #: [ParseResultType = Prism::ParseLexResult]
6
6
  class ERBDocument < Document
7
7
  #: String
8
8
  attr_reader :host_language_source
@@ -31,11 +31,16 @@ module RubyLsp
31
31
  @host_language_source = scanner.host_language
32
32
  # Use partial script to avoid syntax errors in ERB files where keywords may be used without the full context in
33
33
  # which they will be evaluated
34
- @parse_result = Prism.parse(scanner.ruby, partial_script: true)
34
+ @parse_result = Prism.parse_lex(scanner.ruby, partial_script: true)
35
35
  @code_units_cache = @parse_result.code_units_cache(@encoding)
36
36
  true
37
37
  end
38
38
 
39
+ #: -> Prism::ProgramNode
40
+ def ast
41
+ @parse_result.value.first
42
+ end
43
+
39
44
  # @override
40
45
  #: -> bool
41
46
  def syntax_error?
@@ -53,7 +58,7 @@ module RubyLsp
53
58
  char_position, _ = find_index_by_position(position)
54
59
 
55
60
  RubyDocument.locate(
56
- @parse_result.value,
61
+ ast,
57
62
  char_position,
58
63
  code_units_cache: @code_units_cache,
59
64
  node_types: node_types,