ruby-lsp 0.23.23 → 0.26.0

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 (64) 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 +3 -15
  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 +27 -7
  13. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +12 -8
  14. data/lib/ruby_indexer/test/configuration_test.rb +1 -2
  15. data/lib/ruby_indexer/test/index_test.rb +36 -0
  16. data/lib/ruby_indexer/test/instance_variables_test.rb +24 -0
  17. data/lib/ruby_indexer/test/method_test.rb +17 -0
  18. data/lib/ruby_indexer/test/rbs_indexer_test.rb +2 -2
  19. data/lib/ruby_indexer/test/reference_finder_test.rb +79 -14
  20. data/lib/ruby_lsp/addon.rb +44 -15
  21. data/lib/ruby_lsp/base_server.rb +34 -26
  22. data/lib/ruby_lsp/document.rb +162 -52
  23. data/lib/ruby_lsp/erb_document.rb +8 -3
  24. data/lib/ruby_lsp/global_state.rb +21 -0
  25. data/lib/ruby_lsp/internal.rb +0 -2
  26. data/lib/ruby_lsp/listeners/completion.rb +14 -3
  27. data/lib/ruby_lsp/listeners/hover.rb +7 -0
  28. data/lib/ruby_lsp/listeners/inlay_hints.rb +5 -3
  29. data/lib/ruby_lsp/listeners/spec_style.rb +7 -8
  30. data/lib/ruby_lsp/listeners/test_discovery.rb +18 -15
  31. data/lib/ruby_lsp/listeners/test_style.rb +14 -13
  32. data/lib/ruby_lsp/requests/code_action_resolve.rb +3 -3
  33. data/lib/ruby_lsp/requests/code_lens.rb +9 -3
  34. data/lib/ruby_lsp/requests/completion.rb +1 -1
  35. data/lib/ruby_lsp/requests/definition.rb +1 -1
  36. data/lib/ruby_lsp/requests/discover_tests.rb +2 -2
  37. data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
  38. data/lib/ruby_lsp/requests/hover.rb +1 -1
  39. data/lib/ruby_lsp/requests/inlay_hints.rb +3 -3
  40. data/lib/ruby_lsp/requests/on_type_formatting.rb +1 -1
  41. data/lib/ruby_lsp/requests/prepare_rename.rb +1 -1
  42. data/lib/ruby_lsp/requests/references.rb +10 -6
  43. data/lib/ruby_lsp/requests/rename.rb +8 -6
  44. data/lib/ruby_lsp/requests/request.rb +6 -7
  45. data/lib/ruby_lsp/requests/selection_ranges.rb +1 -1
  46. data/lib/ruby_lsp/requests/show_syntax_tree.rb +1 -1
  47. data/lib/ruby_lsp/requests/signature_help.rb +1 -1
  48. data/lib/ruby_lsp/requests/support/common.rb +1 -3
  49. data/lib/ruby_lsp/requests/support/formatter.rb +16 -15
  50. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +2 -2
  51. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +13 -3
  52. data/lib/ruby_lsp/response_builders/response_builder.rb +6 -8
  53. data/lib/ruby_lsp/ruby_document.rb +10 -5
  54. data/lib/ruby_lsp/server.rb +89 -108
  55. data/lib/ruby_lsp/setup_bundler.rb +59 -25
  56. data/lib/ruby_lsp/static_docs.rb +1 -0
  57. data/lib/ruby_lsp/store.rb +0 -10
  58. data/lib/ruby_lsp/test_helper.rb +1 -4
  59. data/lib/ruby_lsp/test_reporters/lsp_reporter.rb +13 -8
  60. data/lib/ruby_lsp/test_reporters/minitest_reporter.rb +17 -4
  61. data/lib/ruby_lsp/utils.rb +47 -11
  62. data/static_docs/break.md +103 -0
  63. metadata +2 -16
  64. data/lib/ruby_lsp/load_sorbet.rb +0 -62
@@ -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,
@@ -56,6 +56,17 @@ module RubyLsp
56
56
  @enabled_feature_flags = {} #: Hash[Symbol, bool]
57
57
  @mutex = Mutex.new #: Mutex
58
58
  @telemetry_machine_id = nil #: String?
59
+ @feature_configuration = {
60
+ inlayHint: RequestConfig.new({
61
+ enableAll: false,
62
+ implicitRescue: false,
63
+ implicitHashValue: false,
64
+ }),
65
+ codeLens: RequestConfig.new({
66
+ enableAll: false,
67
+ enableTestCodeLens: true,
68
+ }),
69
+ } #: Hash[Symbol, RequestConfig]
59
70
  end
60
71
 
61
72
  #: [T] { -> T } -> T
@@ -175,9 +186,19 @@ module RubyLsp
175
186
  @enabled_feature_flags = enabled_flags if enabled_flags
176
187
 
177
188
  @telemetry_machine_id = options.dig(:initializationOptions, :telemetryMachineId)
189
+
190
+ options.dig(:initializationOptions, :featuresConfiguration)&.each do |feature_name, config|
191
+ @feature_configuration[feature_name]&.merge!(config)
192
+ end
193
+
178
194
  notifications
179
195
  end
180
196
 
197
+ #: (Symbol) -> RequestConfig?
198
+ def feature_configuration(feature_name)
199
+ @feature_configuration[feature_name]
200
+ end
201
+
181
202
  #: (Symbol flag) -> bool?
182
203
  def enabled_feature?(flag)
183
204
  @enabled_feature_flags[:all] || @enabled_feature_flags[flag]
@@ -6,8 +6,6 @@
6
6
  yarp_require_paths = Gem.loaded_specs["yarp"]&.full_require_paths
7
7
  $LOAD_PATH.delete_if { |path| yarp_require_paths.include?(path) } if yarp_require_paths
8
8
 
9
- require "sorbet-runtime"
10
-
11
9
  # Set Bundler's UI level to silent as soon as possible to prevent any prints to STDOUT
12
10
  require "bundler"
13
11
  Bundler.ui.level = :silent
@@ -445,11 +445,14 @@ module RubyLsp
445
445
  return unless arguments_node
446
446
 
447
447
  path_node_to_complete = arguments_node.arguments.first
448
-
449
448
  return unless path_node_to_complete.is_a?(Prism::StringNode)
450
449
 
451
- origin_dir = Pathname.new(@uri.to_standardized_path).dirname
450
+ # If the file is unsaved (e.g.: untitled:Untitled-1), we can't provide relative completion as we don't know
451
+ # where the user intends to save it
452
+ full_path = @uri.to_standardized_path
453
+ return unless full_path
452
454
 
455
+ origin_dir = Pathname.new(full_path).dirname
453
456
  content = path_node_to_complete.content
454
457
  # if the path is not a directory, glob all possible next characters
455
458
  # for example ../somethi| (where | is the cursor position)
@@ -516,6 +519,14 @@ module RubyLsp
516
519
 
517
520
  entry_name = entry.name
518
521
  owner_name = entry.owner&.name
522
+ new_text = entry_name
523
+
524
+ if entry_name.end_with?("=")
525
+ method_name = entry_name.delete_suffix("=")
526
+
527
+ # For writer methods, format as assignment and prefix "self." when no receiver is specified
528
+ new_text = node.receiver.nil? ? "self.#{method_name} = " : "#{method_name} = "
529
+ end
519
530
 
520
531
  label_details = Interface::CompletionItemLabelDetails.new(
521
532
  description: entry.file_name,
@@ -525,7 +536,7 @@ module RubyLsp
525
536
  label: entry_name,
526
537
  filter_text: entry_name,
527
538
  label_details: label_details,
528
- text_edit: Interface::TextEdit.new(range: range, new_text: entry_name),
539
+ text_edit: Interface::TextEdit.new(range: range, new_text: new_text),
529
540
  kind: Constant::CompletionItemKind::METHOD,
530
541
  data: {
531
542
  owner_name: owner_name,
@@ -7,6 +7,7 @@ module RubyLsp
7
7
  include Requests::Support::Common
8
8
 
9
9
  ALLOWED_TARGETS = [
10
+ Prism::BreakNode,
10
11
  Prism::CallNode,
11
12
  Prism::ConstantReadNode,
12
13
  Prism::ConstantWriteNode,
@@ -54,6 +55,7 @@ module RubyLsp
54
55
 
55
56
  dispatcher.register(
56
57
  self,
58
+ :on_break_node_enter,
57
59
  :on_constant_read_node_enter,
58
60
  :on_constant_write_node_enter,
59
61
  :on_constant_path_node_enter,
@@ -84,6 +86,11 @@ module RubyLsp
84
86
  )
85
87
  end
86
88
 
89
+ #: (Prism::BreakNode node) -> void
90
+ def on_break_node_enter(node)
91
+ handle_keyword_documentation(node.keyword)
92
+ end
93
+
87
94
  #: (Prism::StringNode node) -> void
88
95
  def on_string_node_enter(node)
89
96
  if @path && File.basename(@path) == GEMFILE_NAME
@@ -8,10 +8,12 @@ module RubyLsp
8
8
 
9
9
  RESCUE_STRING_LENGTH = "rescue".length #: Integer
10
10
 
11
- #: (ResponseBuilders::CollectionResponseBuilder[Interface::InlayHint] response_builder, RequestConfig hints_configuration, Prism::Dispatcher dispatcher) -> void
12
- def initialize(response_builder, hints_configuration, dispatcher)
11
+ #: (GlobalState, ResponseBuilders::CollectionResponseBuilder[Interface::InlayHint], Prism::Dispatcher) -> void
12
+ def initialize(global_state, response_builder, dispatcher)
13
13
  @response_builder = response_builder
14
- @hints_configuration = hints_configuration
14
+ @hints_configuration = ( # rubocop:disable Style/RedundantParentheses
15
+ global_state.feature_configuration(:inlayHint) #: as !nil
16
+ ) #: RequestConfig
15
17
 
16
18
  dispatcher.register(self, :on_rescue_node_enter, :on_implicit_node_enter)
17
19
  end