ruby-lsp 0.23.15 → 0.26.9

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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/exe/ruby-lsp +17 -14
  4. data/exe/ruby-lsp-check +0 -4
  5. data/exe/ruby-lsp-launcher +41 -14
  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/configuration.rb +4 -3
  10. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +42 -20
  11. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +1 -7
  12. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +49 -62
  13. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +84 -74
  14. data/lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb +6 -9
  15. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +9 -14
  16. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +12 -8
  17. data/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb +4 -4
  18. data/lib/ruby_lsp/addon.rb +44 -15
  19. data/lib/ruby_lsp/base_server.rb +56 -37
  20. data/lib/ruby_lsp/client_capabilities.rb +6 -1
  21. data/lib/ruby_lsp/document.rb +174 -62
  22. data/lib/ruby_lsp/erb_document.rb +10 -8
  23. data/lib/ruby_lsp/global_state.rb +86 -33
  24. data/lib/ruby_lsp/internal.rb +6 -3
  25. data/lib/ruby_lsp/listeners/completion.rb +22 -11
  26. data/lib/ruby_lsp/listeners/definition.rb +41 -21
  27. data/lib/ruby_lsp/listeners/document_highlight.rb +26 -1
  28. data/lib/ruby_lsp/listeners/document_link.rb +64 -28
  29. data/lib/ruby_lsp/listeners/hover.rb +27 -16
  30. data/lib/ruby_lsp/listeners/inlay_hints.rb +5 -3
  31. data/lib/ruby_lsp/listeners/semantic_highlighting.rb +2 -2
  32. data/lib/ruby_lsp/listeners/signature_help.rb +2 -2
  33. data/lib/ruby_lsp/listeners/spec_style.rb +155 -79
  34. data/lib/ruby_lsp/listeners/test_discovery.rb +39 -21
  35. data/lib/ruby_lsp/listeners/test_style.rb +75 -35
  36. data/lib/ruby_lsp/rbs_document.rb +3 -6
  37. data/lib/ruby_lsp/requests/code_action_resolve.rb +83 -58
  38. data/lib/ruby_lsp/requests/code_actions.rb +20 -5
  39. data/lib/ruby_lsp/requests/code_lens.rb +27 -6
  40. data/lib/ruby_lsp/requests/completion.rb +3 -3
  41. data/lib/ruby_lsp/requests/completion_resolve.rb +8 -6
  42. data/lib/ruby_lsp/requests/definition.rb +4 -7
  43. data/lib/ruby_lsp/requests/discover_tests.rb +2 -2
  44. data/lib/ruby_lsp/requests/document_highlight.rb +2 -2
  45. data/lib/ruby_lsp/requests/document_link.rb +1 -1
  46. data/lib/ruby_lsp/requests/folding_ranges.rb +1 -1
  47. data/lib/ruby_lsp/requests/go_to_relevant_file.rb +64 -12
  48. data/lib/ruby_lsp/requests/hover.rb +3 -6
  49. data/lib/ruby_lsp/requests/inlay_hints.rb +4 -4
  50. data/lib/ruby_lsp/requests/on_type_formatting.rb +1 -1
  51. data/lib/ruby_lsp/requests/prepare_rename.rb +1 -1
  52. data/lib/ruby_lsp/requests/references.rb +10 -21
  53. data/lib/ruby_lsp/requests/rename.rb +9 -10
  54. data/lib/ruby_lsp/requests/request.rb +8 -8
  55. data/lib/ruby_lsp/requests/selection_ranges.rb +2 -2
  56. data/lib/ruby_lsp/requests/semantic_highlighting.rb +1 -1
  57. data/lib/ruby_lsp/requests/show_syntax_tree.rb +2 -2
  58. data/lib/ruby_lsp/requests/signature_help.rb +2 -2
  59. data/lib/ruby_lsp/requests/support/annotation.rb +1 -1
  60. data/lib/ruby_lsp/requests/support/common.rb +9 -12
  61. data/lib/ruby_lsp/requests/support/formatter.rb +16 -15
  62. data/lib/ruby_lsp/requests/support/package_url.rb +414 -0
  63. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +7 -1
  64. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +2 -2
  65. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +13 -3
  66. data/lib/ruby_lsp/requests/support/source_uri.rb +7 -4
  67. data/lib/ruby_lsp/requests/support/test_item.rb +7 -1
  68. data/lib/ruby_lsp/requests/workspace_symbol.rb +20 -12
  69. data/lib/ruby_lsp/response_builders/collection_response_builder.rb +1 -4
  70. data/lib/ruby_lsp/response_builders/document_symbol.rb +2 -3
  71. data/lib/ruby_lsp/response_builders/hover.rb +1 -4
  72. data/lib/ruby_lsp/response_builders/response_builder.rb +6 -7
  73. data/lib/ruby_lsp/response_builders/semantic_highlighting.rb +4 -5
  74. data/lib/ruby_lsp/response_builders/signature_help.rb +1 -2
  75. data/lib/ruby_lsp/response_builders/test_collection.rb +29 -3
  76. data/lib/ruby_lsp/ruby_document.rb +14 -42
  77. data/lib/ruby_lsp/scripts/compose_bundle.rb +3 -3
  78. data/lib/ruby_lsp/scripts/compose_bundle_windows.rb +3 -1
  79. data/lib/ruby_lsp/server.rb +173 -130
  80. data/lib/ruby_lsp/setup_bundler.rb +114 -47
  81. data/lib/ruby_lsp/static_docs.rb +1 -0
  82. data/lib/ruby_lsp/store.rb +6 -16
  83. data/lib/ruby_lsp/test_helper.rb +1 -4
  84. data/lib/ruby_lsp/test_reporters/lsp_reporter.rb +121 -17
  85. data/lib/ruby_lsp/test_reporters/minitest_reporter.rb +65 -25
  86. data/lib/ruby_lsp/test_reporters/test_unit_reporter.rb +16 -18
  87. data/lib/ruby_lsp/utils.rb +102 -13
  88. data/static_docs/break.md +103 -0
  89. metadata +8 -33
  90. data/lib/ruby_indexer/test/class_variables_test.rb +0 -140
  91. data/lib/ruby_indexer/test/classes_and_modules_test.rb +0 -770
  92. data/lib/ruby_indexer/test/configuration_test.rb +0 -280
  93. data/lib/ruby_indexer/test/constant_test.rb +0 -402
  94. data/lib/ruby_indexer/test/enhancements_test.rb +0 -325
  95. data/lib/ruby_indexer/test/global_variable_test.rb +0 -49
  96. data/lib/ruby_indexer/test/index_test.rb +0 -2190
  97. data/lib/ruby_indexer/test/instance_variables_test.rb +0 -240
  98. data/lib/ruby_indexer/test/method_test.rb +0 -973
  99. data/lib/ruby_indexer/test/prefix_tree_test.rb +0 -150
  100. data/lib/ruby_indexer/test/rbs_indexer_test.rb +0 -380
  101. data/lib/ruby_indexer/test/reference_finder_test.rb +0 -330
  102. data/lib/ruby_indexer/test/test_case.rb +0 -51
  103. data/lib/ruby_indexer/test/uri_test.rb +0 -85
  104. data/lib/ruby_lsp/load_sorbet.rb +0 -62
@@ -2,29 +2,16 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module RubyLsp
5
+ # @abstract
6
+ #: [ParseResultType]
5
7
  class Document
6
- class LanguageId < T::Enum
7
- enums do
8
- Ruby = new("ruby")
9
- ERB = new("erb")
10
- RBS = new("rbs")
11
- end
12
- end
13
-
14
- extend T::Sig
15
- extend T::Helpers
16
- extend T::Generic
17
-
18
- class LocationNotFoundError < StandardError; end
19
- ParseResultType = type_member
8
+ class InvalidLocationError < StandardError; end
20
9
 
21
10
  # This maximum number of characters for providing expensive features, like semantic highlighting and diagnostics.
22
11
  # This is the same number used by the TypeScript extension in VS Code
23
12
  MAXIMUM_CHARACTERS_FOR_EXPENSIVE_FEATURES = 100_000
24
13
  EMPTY_CACHE = Object.new.freeze #: Object
25
14
 
26
- abstract!
27
-
28
15
  #: ParseResultType
29
16
  attr_reader :parse_result
30
17
 
@@ -56,8 +43,13 @@ module RubyLsp
56
43
  @encoding = global_state.encoding #: Encoding
57
44
  @uri = uri #: URI::Generic
58
45
  @needs_parsing = true #: bool
59
- @parse_result = T.unsafe(nil) #: ParseResultType
60
46
  @last_edit = nil #: Edit?
47
+
48
+ # Workaround to be able to type parse_result properly. It is immediately set when invoking parse!
49
+ @parse_result = ( # rubocop:disable Style/RedundantParentheses
50
+ nil #: as untyped
51
+ ) #: ParseResultType
52
+
61
53
  parse!
62
54
  end
63
55
 
@@ -66,8 +58,11 @@ module RubyLsp
66
58
  self.class == other.class && uri == other.uri && @source == other.source
67
59
  end
68
60
 
69
- sig { abstract.returns(LanguageId) }
70
- def language_id; end
61
+ # @abstract
62
+ #: -> Symbol
63
+ def language_id
64
+ raise AbstractMethodInvokedError
65
+ end
71
66
 
72
67
  #: [T] (String request_name) { (Document[ParseResultType] document) -> T } -> T
73
68
  def cache_fetch(request_name, &block)
@@ -89,6 +84,11 @@ module RubyLsp
89
84
  @cache[request_name]
90
85
  end
91
86
 
87
+ #: (String request_name) -> void
88
+ def clear_cache(request_name)
89
+ @cache[request_name] = EMPTY_CACHE
90
+ end
91
+
92
92
  #: (Array[Hash[Symbol, untyped]] edits, version: Integer) -> void
93
93
  def push_edits(edits, version:)
94
94
  edits.each do |edit|
@@ -120,11 +120,17 @@ module RubyLsp
120
120
  end
121
121
 
122
122
  # Returns `true` if the document was parsed and `false` if nothing needed parsing
123
- sig { abstract.returns(T::Boolean) }
124
- def parse!; end
123
+ # @abstract
124
+ #: -> bool
125
+ def parse!
126
+ raise AbstractMethodInvokedError
127
+ end
125
128
 
126
- sig { abstract.returns(T::Boolean) }
127
- def syntax_error?; end
129
+ # @abstract
130
+ #: -> bool
131
+ def syntax_error?
132
+ raise AbstractMethodInvokedError
133
+ end
128
134
 
129
135
  #: -> bool
130
136
  def past_expensive_limit?
@@ -133,27 +139,28 @@ module RubyLsp
133
139
 
134
140
  #: (Hash[Symbol, untyped] start_pos, ?Hash[Symbol, untyped]? end_pos) -> [Integer, Integer?]
135
141
  def find_index_by_position(start_pos, end_pos = nil)
136
- @global_state.synchronize do
137
- scanner = create_scanner
138
- start_index = scanner.find_char_position(start_pos)
139
- end_index = scanner.find_char_position(end_pos) if end_pos
140
- [start_index, end_index]
141
- 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]
142
146
  end
143
147
 
144
148
  private
145
149
 
146
150
  #: -> Scanner
147
151
  def create_scanner
148
- 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
149
160
  end
150
161
 
162
+ # @abstract
151
163
  class Edit
152
- extend T::Sig
153
- extend T::Helpers
154
-
155
- abstract!
156
-
157
164
  #: Hash[Symbol, untyped]
158
165
  attr_reader :range
159
166
 
@@ -167,33 +174,114 @@ module RubyLsp
167
174
  class Replace < Edit; end
168
175
  class Delete < Edit; end
169
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
170
182
  class Scanner
171
- extend T::Sig
172
-
173
183
  LINE_BREAK = 0x0A #: Integer
174
184
  # After character 0xFFFF, UTF-16 considers characters to have length 2 and we have to account for that
175
185
  SURROGATE_PAIR_START = 0xFFFF #: Integer
176
186
 
177
- #: (String source, Encoding encoding) -> void
178
- def initialize(source, encoding)
187
+ #: -> void
188
+ def initialize
179
189
  @current_line = 0 #: Integer
180
190
  @pos = 0 #: Integer
181
- @source = source.codepoints #: Array[Integer]
182
- @encoding = encoding
183
191
  end
184
192
 
185
- # 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
186
274
  #: (Hash[Symbol, untyped] position) -> Integer
187
275
  def find_char_position(position)
188
276
  # Find the character index for the beginning of the requested line
189
277
  until @current_line == position[:line]
190
- until LINE_BREAK == @source[@pos]
191
- @pos += 1
278
+ codepoint = @codepoints[@pos] #: Integer?
279
+ raise InvalidLocationError unless codepoint
192
280
 
193
- if @pos >= @source.length
194
- # Pack the code points back into the original string to provide context in the error message
195
- raise LocationNotFoundError, "Requested position: #{position}\nSource:\n\n#{@source.pack("U*")}"
196
- end
281
+ until LINE_BREAK == @codepoints[@pos]
282
+ @pos += 1
283
+ codepoint = @codepoints[@pos] #: Integer?
284
+ raise InvalidLocationError unless codepoint
197
285
  end
198
286
 
199
287
  @pos += 1
@@ -202,29 +290,53 @@ module RubyLsp
202
290
 
203
291
  # The final position is the beginning of the line plus the requested column. If the encoding is UTF-16, we also
204
292
  # need to adjust for surrogate pairs
205
- requested_position = @pos + position[:character]
293
+ line_characters = 0
294
+ line_code_units = 0
295
+
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
206
305
 
207
- if @encoding == Encoding::UTF_16LE
208
- requested_position -= utf_16_character_position_correction(@pos, requested_position)
306
+ line_characters += 1
209
307
  end
210
308
 
211
- requested_position
309
+ @pos + line_characters
310
+ end
311
+ end
312
+
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]
212
319
  end
213
320
 
214
- # Subtract 1 for each character after 0xFFFF in the current line from the column position, so that we hit the
215
- # right character in the UTF-8 representation
216
- #: (Integer current_position, Integer requested_position) -> Integer
217
- def utf_16_character_position_correction(current_position, requested_position)
218
- utf16_unicode_correction = 0
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
219
328
 
220
- until current_position == requested_position
221
- codepoint = @source[current_position]
222
- 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
223
334
 
224
- current_position += 1
335
+ @pos += 1
336
+ @current_line += 1
225
337
  end
226
338
 
227
- utf16_unicode_correction
339
+ @pos + position[:character]
228
340
  end
229
341
  end
230
342
  end
@@ -2,11 +2,8 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module RubyLsp
5
+ #: [ParseResultType = Prism::ParseLexResult]
5
6
  class ERBDocument < Document
6
- extend T::Generic
7
-
8
- ParseResultType = type_member { { fixed: Prism::ParseResult } }
9
-
10
7
  #: String
11
8
  attr_reader :host_language_source
12
9
 
@@ -34,11 +31,16 @@ module RubyLsp
34
31
  @host_language_source = scanner.host_language
35
32
  # Use partial script to avoid syntax errors in ERB files where keywords may be used without the full context in
36
33
  # which they will be evaluated
37
- @parse_result = Prism.parse(scanner.ruby, partial_script: true)
34
+ @parse_result = Prism.parse_lex(scanner.ruby, partial_script: true)
38
35
  @code_units_cache = @parse_result.code_units_cache(@encoding)
39
36
  true
40
37
  end
41
38
 
39
+ #: -> Prism::ProgramNode
40
+ def ast
41
+ @parse_result.value.first
42
+ end
43
+
42
44
  # @override
43
45
  #: -> bool
44
46
  def syntax_error?
@@ -46,9 +48,9 @@ module RubyLsp
46
48
  end
47
49
 
48
50
  # @override
49
- #: -> LanguageId
51
+ #: -> Symbol
50
52
  def language_id
51
- LanguageId::ERB
53
+ :erb
52
54
  end
53
55
 
54
56
  #: (Hash[Symbol, untyped] position, ?node_types: Array[singleton(Prism::Node)]) -> NodeContext
@@ -56,7 +58,7 @@ module RubyLsp
56
58
  char_position, _ = find_index_by_position(position)
57
59
 
58
60
  RubyDocument.locate(
59
- @parse_result.value,
61
+ ast,
60
62
  char_position,
61
63
  code_units_cache: @code_units_cache,
62
64
  node_types: node_types,
@@ -2,6 +2,21 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module RubyLsp
5
+ # Holds the detected value and the reason for detection
6
+ class DetectionResult
7
+ #: String
8
+ attr_reader :value
9
+
10
+ #: String
11
+ attr_reader :reason
12
+
13
+ #: (String value, String reason) -> void
14
+ def initialize(value, reason)
15
+ @value = value
16
+ @reason = reason
17
+ end
18
+ end
19
+
5
20
  class GlobalState
6
21
  #: String
7
22
  attr_reader :test_library
@@ -56,6 +71,17 @@ module RubyLsp
56
71
  @enabled_feature_flags = {} #: Hash[Symbol, bool]
57
72
  @mutex = Mutex.new #: Mutex
58
73
  @telemetry_machine_id = nil #: String?
74
+ @feature_configuration = {
75
+ inlayHint: RequestConfig.new({
76
+ enableAll: false,
77
+ implicitRescue: false,
78
+ implicitHashValue: false,
79
+ }),
80
+ codeLens: RequestConfig.new({
81
+ enableAll: false,
82
+ enableTestCodeLens: true,
83
+ }),
84
+ } #: Hash[Symbol, RequestConfig]
59
85
  end
60
86
 
61
87
  #: [T] { -> T } -> T
@@ -111,8 +137,11 @@ module RubyLsp
111
137
  end
112
138
 
113
139
  if @formatter == "auto"
114
- @formatter = detect_formatter(direct_dependencies, all_dependencies)
115
- notifications << Notification.window_log_message("Auto detected formatter: #{@formatter}")
140
+ formatter_result = detect_formatter(direct_dependencies, all_dependencies)
141
+ @formatter = formatter_result.value
142
+ notifications << Notification.window_log_message(
143
+ "Auto detected formatter: #{@formatter} (#{formatter_result.reason})",
144
+ )
116
145
  end
117
146
 
118
147
  specified_linters = options.dig(:initializationOptions, :linters)
@@ -133,21 +162,28 @@ module RubyLsp
133
162
  specified_linters << "rubocop_internal"
134
163
  end
135
164
 
136
- @linters = specified_linters || detect_linters(direct_dependencies, all_dependencies)
137
-
138
- notifications << if specified_linters
139
- Notification.window_log_message("Using linters specified by user: #{@linters.join(", ")}")
165
+ if specified_linters
166
+ @linters = specified_linters
167
+ notifications << Notification.window_log_message("Using linters specified by user: #{@linters.join(", ")}")
140
168
  else
141
- Notification.window_log_message("Auto detected linters: #{@linters.join(", ")}")
169
+ linter_results = detect_linters(direct_dependencies, all_dependencies)
170
+ @linters = linter_results.map(&:value)
171
+ linter_messages = linter_results.map { |r| "#{r.value} (#{r.reason})" }
172
+ notifications << Notification.window_log_message("Auto detected linters: #{linter_messages.join(", ")}")
142
173
  end
143
174
 
144
- @test_library = detect_test_library(direct_dependencies)
145
- notifications << Notification.window_log_message("Detected test library: #{@test_library}")
175
+ test_library_result = detect_test_library(direct_dependencies)
176
+ @test_library = test_library_result.value
177
+ notifications << Notification.window_log_message(
178
+ "Detected test library: #{@test_library} (#{test_library_result.reason})",
179
+ )
146
180
 
147
- @has_type_checker = detect_typechecker(all_dependencies)
148
- if @has_type_checker
181
+ typechecker_result = detect_typechecker(all_dependencies)
182
+ @has_type_checker = !typechecker_result.nil?
183
+ if typechecker_result
149
184
  notifications << Notification.window_log_message(
150
- "Ruby LSP detected this is a Sorbet project and will defer to the Sorbet LSP for some functionality",
185
+ "Ruby LSP detected this is a Sorbet project (#{typechecker_result.reason}) and will defer to the " \
186
+ "Sorbet LSP for some functionality",
151
187
  )
152
188
  end
153
189
 
@@ -175,9 +211,19 @@ module RubyLsp
175
211
  @enabled_feature_flags = enabled_flags if enabled_flags
176
212
 
177
213
  @telemetry_machine_id = options.dig(:initializationOptions, :telemetryMachineId)
214
+
215
+ options.dig(:initializationOptions, :featuresConfiguration)&.each do |feature_name, config|
216
+ @feature_configuration[feature_name]&.merge!(config)
217
+ end
218
+
178
219
  notifications
179
220
  end
180
221
 
222
+ #: (Symbol) -> RequestConfig?
223
+ def feature_configuration(feature_name)
224
+ @feature_configuration[feature_name]
225
+ end
226
+
181
227
  #: (Symbol flag) -> bool?
182
228
  def enabled_feature?(flag)
183
229
  @enabled_feature_flags[:all] || @enabled_feature_flags[flag]
@@ -207,60 +253,67 @@ module RubyLsp
207
253
 
208
254
  private
209
255
 
210
- #: (Array[String] direct_dependencies, Array[String] all_dependencies) -> String
256
+ #: (Array[String] direct_dependencies, Array[String] all_dependencies) -> DetectionResult
211
257
  def detect_formatter(direct_dependencies, all_dependencies)
212
258
  # NOTE: Intentionally no $ at end, since we want to match rubocop-shopify, etc.
213
- return "rubocop_internal" if direct_dependencies.any?(/^rubocop/)
259
+ if direct_dependencies.any?(/^rubocop/)
260
+ return DetectionResult.new("rubocop_internal", "direct dependency matching /^rubocop/")
261
+ end
214
262
 
215
- syntax_tree_is_direct_dependency = direct_dependencies.include?("syntax_tree")
216
- return "syntax_tree" if syntax_tree_is_direct_dependency
263
+ if direct_dependencies.include?("syntax_tree")
264
+ return DetectionResult.new("syntax_tree", "direct dependency")
265
+ end
217
266
 
218
- rubocop_is_transitive_dependency = all_dependencies.include?("rubocop")
219
- return "rubocop_internal" if dot_rubocop_yml_present && rubocop_is_transitive_dependency
267
+ if all_dependencies.include?("rubocop") && dot_rubocop_yml_present
268
+ return DetectionResult.new("rubocop_internal", "transitive dependency with .rubocop.yml present")
269
+ end
220
270
 
221
- "none"
271
+ DetectionResult.new("none", "no formatter detected")
222
272
  end
223
273
 
224
274
  # Try to detect if there are linters in the project's dependencies. For auto-detection, we always only consider a
225
275
  # single linter. To have multiple linters running, the user must configure them manually
226
- #: (Array[String] dependencies, Array[String] all_dependencies) -> Array[String]
276
+ #: (Array[String] dependencies, Array[String] all_dependencies) -> Array[DetectionResult]
227
277
  def detect_linters(dependencies, all_dependencies)
228
- linters = []
278
+ linters = [] #: Array[DetectionResult]
229
279
 
230
- if dependencies.any?(/^rubocop/) || (all_dependencies.include?("rubocop") && dot_rubocop_yml_present)
231
- linters << "rubocop_internal"
280
+ if dependencies.any?(/^rubocop/)
281
+ linters << DetectionResult.new("rubocop_internal", "direct dependency matching /^rubocop/")
282
+ elsif all_dependencies.include?("rubocop") && dot_rubocop_yml_present
283
+ linters << DetectionResult.new("rubocop_internal", "transitive dependency with .rubocop.yml present")
232
284
  end
233
285
 
234
286
  linters
235
287
  end
236
288
 
237
- #: (Array[String] dependencies) -> String
289
+ #: (Array[String] dependencies) -> DetectionResult
238
290
  def detect_test_library(dependencies)
239
291
  if dependencies.any?(/^rspec/)
240
- "rspec"
292
+ DetectionResult.new("rspec", "direct dependency matching /^rspec/")
241
293
  # A Rails app may have a dependency on minitest, but we would instead want to use the Rails test runner provided
242
294
  # by ruby-lsp-rails. A Rails app doesn't need to depend on the rails gem itself, individual components like
243
295
  # activestorage may be added to the gemfile so that other components aren't downloaded. Check for the presence
244
296
  # of bin/rails to support these cases.
245
297
  elsif bin_rails_present
246
- "rails"
298
+ DetectionResult.new("rails", "bin/rails present")
247
299
  # NOTE: Intentionally ends with $ to avoid mis-matching minitest-reporters, etc. in a Rails app.
248
300
  elsif dependencies.any?(/^minitest$/)
249
- "minitest"
301
+ DetectionResult.new("minitest", "direct dependency matching /^minitest$/")
250
302
  elsif dependencies.any?(/^test-unit/)
251
- "test-unit"
303
+ DetectionResult.new("test-unit", "direct dependency matching /^test-unit/")
252
304
  else
253
- "unknown"
305
+ DetectionResult.new("unknown", "no test library detected")
254
306
  end
255
307
  end
256
308
 
257
- #: (Array[String] dependencies) -> bool
309
+ #: (Array[String] dependencies) -> DetectionResult?
258
310
  def detect_typechecker(dependencies)
259
- return false if ENV["RUBY_LSP_BYPASS_TYPECHECKER"]
311
+ return if ENV["RUBY_LSP_BYPASS_TYPECHECKER"]
312
+ return if dependencies.none?(/^sorbet-static/)
260
313
 
261
- dependencies.any?(/^sorbet-static/)
314
+ DetectionResult.new("sorbet", "sorbet-static in dependencies")
262
315
  rescue Bundler::GemfileNotFound
263
- false
316
+ nil
264
317
  end
265
318
 
266
319
  #: -> bool
@@ -6,15 +6,18 @@
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
14
12
 
15
13
  require "json"
16
14
  require "uri"
17
- require "cgi"
15
+ require "cgi/escape"
16
+ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.5")
17
+ # Just requiring `cgi/escape` leaves CGI.unescape broken on older rubies
18
+ # Some background on why this is necessary: https://bugs.ruby-lang.org/issues/21258
19
+ require "cgi/util"
20
+ end
18
21
  require "set"
19
22
  require "strscan"
20
23
  require "prism"