ruby-lsp 0.23.23 → 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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/exe/ruby-lsp +10 -4
- data/exe/ruby-lsp-check +0 -4
- data/exe/ruby-lsp-launcher +25 -11
- data/exe/ruby-lsp-test-exec +3 -15
- data/lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb +0 -1
- data/lib/rubocop/cop/ruby_lsp/use_register_with_handler_method.rb +0 -1
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +7 -1
- data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +1 -4
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +10 -19
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +29 -7
- data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +12 -8
- data/lib/ruby_indexer/test/configuration_test.rb +1 -2
- data/lib/ruby_indexer/test/index_test.rb +39 -0
- data/lib/ruby_indexer/test/instance_variables_test.rb +24 -0
- data/lib/ruby_indexer/test/method_test.rb +17 -0
- data/lib/ruby_indexer/test/rbs_indexer_test.rb +2 -2
- data/lib/ruby_indexer/test/reference_finder_test.rb +79 -14
- data/lib/ruby_lsp/addon.rb +44 -15
- data/lib/ruby_lsp/base_server.rb +34 -26
- data/lib/ruby_lsp/document.rb +162 -52
- data/lib/ruby_lsp/erb_document.rb +8 -3
- data/lib/ruby_lsp/global_state.rb +21 -0
- data/lib/ruby_lsp/internal.rb +0 -2
- data/lib/ruby_lsp/listeners/completion.rb +14 -3
- data/lib/ruby_lsp/listeners/hover.rb +7 -0
- data/lib/ruby_lsp/listeners/inlay_hints.rb +5 -3
- data/lib/ruby_lsp/listeners/spec_style.rb +7 -8
- data/lib/ruby_lsp/listeners/test_discovery.rb +18 -15
- data/lib/ruby_lsp/listeners/test_style.rb +14 -13
- data/lib/ruby_lsp/requests/code_action_resolve.rb +3 -3
- data/lib/ruby_lsp/requests/code_lens.rb +9 -3
- data/lib/ruby_lsp/requests/completion.rb +1 -1
- data/lib/ruby_lsp/requests/definition.rb +1 -1
- data/lib/ruby_lsp/requests/discover_tests.rb +2 -2
- data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
- data/lib/ruby_lsp/requests/hover.rb +1 -1
- data/lib/ruby_lsp/requests/inlay_hints.rb +3 -3
- data/lib/ruby_lsp/requests/on_type_formatting.rb +1 -1
- data/lib/ruby_lsp/requests/prepare_rename.rb +1 -1
- data/lib/ruby_lsp/requests/references.rb +10 -6
- data/lib/ruby_lsp/requests/rename.rb +8 -6
- data/lib/ruby_lsp/requests/request.rb +6 -7
- data/lib/ruby_lsp/requests/selection_ranges.rb +1 -1
- data/lib/ruby_lsp/requests/show_syntax_tree.rb +1 -1
- data/lib/ruby_lsp/requests/signature_help.rb +1 -1
- data/lib/ruby_lsp/requests/support/common.rb +1 -3
- data/lib/ruby_lsp/requests/support/formatter.rb +16 -15
- data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +2 -2
- data/lib/ruby_lsp/requests/support/rubocop_runner.rb +13 -3
- data/lib/ruby_lsp/response_builders/response_builder.rb +6 -8
- data/lib/ruby_lsp/ruby_document.rb +10 -5
- data/lib/ruby_lsp/server.rb +93 -108
- data/lib/ruby_lsp/setup_bundler.rb +59 -25
- data/lib/ruby_lsp/static_docs.rb +1 -0
- data/lib/ruby_lsp/store.rb +0 -10
- data/lib/ruby_lsp/test_helper.rb +1 -4
- data/lib/ruby_lsp/test_reporters/lsp_reporter.rb +13 -8
- data/lib/ruby_lsp/test_reporters/minitest_reporter.rb +17 -4
- data/lib/ruby_lsp/utils.rb +47 -11
- data/static_docs/break.md +103 -0
- metadata +2 -16
- 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
|
220
|
-
refs = find_instance_variable_references("@
|
219
|
+
def test_finds_instance_variable_references
|
220
|
+
refs = find_instance_variable_references("@name", ["Foo"], <<~RUBY)
|
221
221
|
class Foo
|
222
|
-
def
|
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(
|
242
|
+
assert_equal(3, refs.size)
|
228
243
|
|
229
|
-
assert_equal("@
|
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
|
256
|
-
refs = find_instance_variable_references("@name", <<~RUBY)
|
257
|
-
|
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(
|
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(
|
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
|
|
data/lib/ruby_lsp/addon.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
182
|
-
|
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
|
204
|
+
# Each add-on must implement `MyAddon#deactivate` and use to perform any clean up, like shutting down a
|
185
205
|
# child process
|
186
|
-
|
187
|
-
|
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
|
-
|
191
|
-
|
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
|
-
|
196
|
-
|
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
|
data/lib/ruby_lsp/base_server.rb
CHANGED
@@ -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.
|
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", "
|
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
|
-
|
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
|
-
|
122
|
-
#: -> untyped
|
123
|
-
def pop_response
|
124
|
-
@outgoing_queue.pop
|
125
|
-
end
|
139
|
+
private
|
126
140
|
|
127
|
-
#
|
128
|
-
#:
|
129
|
-
def
|
130
|
-
|
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))
|
data/lib/ruby_lsp/document.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
67
|
-
|
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
|
-
|
126
|
-
|
123
|
+
# @abstract
|
124
|
+
#: -> bool
|
125
|
+
def parse!
|
126
|
+
raise AbstractMethodInvokedError
|
127
|
+
end
|
127
128
|
|
128
|
-
|
129
|
-
|
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
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
-
|
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
|
-
#:
|
180
|
-
def initialize
|
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
|
-
|
193
|
-
|
278
|
+
codepoint = @codepoints[@pos] #: Integer?
|
279
|
+
raise InvalidLocationError unless codepoint
|
194
280
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
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
|
-
|
293
|
+
line_characters = 0
|
294
|
+
line_code_units = 0
|
208
295
|
|
209
|
-
|
210
|
-
|
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
|
-
|
309
|
+
@pos + line_characters
|
214
310
|
end
|
311
|
+
end
|
215
312
|
|
216
|
-
|
217
|
-
|
218
|
-
#: (
|
219
|
-
def
|
220
|
-
|
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
|
-
|
223
|
-
|
224
|
-
|
329
|
+
until LINE_BREAK == @codepoints[@pos]
|
330
|
+
@pos += 1
|
331
|
+
codepoint = @codepoints[@pos] #: Integer?
|
332
|
+
raise InvalidLocationError unless codepoint
|
333
|
+
end
|
225
334
|
|
226
|
-
|
335
|
+
@pos += 1
|
336
|
+
@current_line += 1
|
227
337
|
end
|
228
338
|
|
229
|
-
|
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::
|
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.
|
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
|
-
|
61
|
+
ast,
|
57
62
|
char_position,
|
58
63
|
code_units_cache: @code_units_cache,
|
59
64
|
node_types: node_types,
|