ruby-lsp 0.17.11 → 0.17.13

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.
@@ -57,21 +57,48 @@ module RubyLsp
57
57
  @linters.filter_map { |name| @supported_formatters[name] }
58
58
  end
59
59
 
60
- sig { params(options: T::Hash[Symbol, T.untyped]).void }
60
+ # Applies the options provided by the editor and returns an array of notifications to send back to the client
61
+ sig { params(options: T::Hash[Symbol, T.untyped]).returns(T::Array[Notification]) }
61
62
  def apply_options(options)
63
+ notifications = []
62
64
  direct_dependencies = gather_direct_dependencies
63
65
  all_dependencies = gather_direct_and_indirect_dependencies
64
66
  workspace_uri = options.dig(:workspaceFolders, 0, :uri)
65
67
  @workspace_uri = URI(workspace_uri) if workspace_uri
66
68
 
67
69
  specified_formatter = options.dig(:initializationOptions, :formatter)
68
- @formatter = specified_formatter if specified_formatter
69
- @formatter = detect_formatter(direct_dependencies, all_dependencies) if @formatter == "auto"
70
+
71
+ if specified_formatter
72
+ @formatter = specified_formatter
73
+
74
+ if specified_formatter != "auto"
75
+ notifications << Notification.window_log_message("Using formatter specified by user: #{@formatter}")
76
+ end
77
+ end
78
+
79
+ if @formatter == "auto"
80
+ @formatter = detect_formatter(direct_dependencies, all_dependencies)
81
+ notifications << Notification.window_log_message("Auto detected formatter: #{@formatter}")
82
+ end
70
83
 
71
84
  specified_linters = options.dig(:initializationOptions, :linters)
72
85
  @linters = specified_linters || detect_linters(direct_dependencies, all_dependencies)
86
+
87
+ notifications << if specified_linters
88
+ Notification.window_log_message("Using linters specified by user: #{@linters.join(", ")}")
89
+ else
90
+ Notification.window_log_message("Auto detected linters: #{@linters.join(", ")}")
91
+ end
92
+
73
93
  @test_library = detect_test_library(direct_dependencies)
94
+ notifications << Notification.window_log_message("Detected test library: #{@test_library}")
95
+
74
96
  @has_type_checker = detect_typechecker(direct_dependencies)
97
+ if @has_type_checker
98
+ notifications << Notification.window_log_message(
99
+ "Ruby LSP detected this is a Sorbet project and will defer to the Sorbet LSP for some functionality",
100
+ )
101
+ end
75
102
 
76
103
  encodings = options.dig(:capabilities, :general, :positionEncodings)
77
104
  @encoding = if !encodings || encodings.empty?
@@ -91,6 +118,8 @@ module RubyLsp
91
118
 
92
119
  @experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled) || false
93
120
  @type_inferrer.experimental_features = @experimental_features
121
+
122
+ notifications
94
123
  end
95
124
 
96
125
  sig { returns(String) }
@@ -163,16 +192,7 @@ module RubyLsp
163
192
  def detect_typechecker(dependencies)
164
193
  return false if ENV["RUBY_LSP_BYPASS_TYPECHECKER"]
165
194
 
166
- # We can't read the env from within `Bundle.with_original_env` so we need to set it here.
167
- ruby_lsp_env_is_test = (ENV["RUBY_LSP_ENV"] == "test")
168
- Bundler.with_original_env do
169
- sorbet_static_detected = dependencies.any?(/^sorbet-static/)
170
- # Don't show message while running tests, since it's noisy
171
- if sorbet_static_detected && !ruby_lsp_env_is_test
172
- $stderr.puts("Ruby LSP detected this is a Sorbet project so will defer to Sorbet LSP for some functionality")
173
- end
174
- sorbet_static_detected
175
- end
195
+ dependencies.any?(/^sorbet-static/)
176
196
  rescue Bundler::GemfileNotFound
177
197
  false
178
198
  end
@@ -56,7 +56,7 @@ module RubyLsp
56
56
  response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
57
57
  global_state: GlobalState,
58
58
  node_context: NodeContext,
59
- sorbet_level: Document::SorbetLevel,
59
+ sorbet_level: RubyDocument::SorbetLevel,
60
60
  dispatcher: Prism::Dispatcher,
61
61
  uri: URI::Generic,
62
62
  trigger_character: T.nilable(String),
@@ -99,7 +99,7 @@ module RubyLsp
99
99
  def on_constant_read_node_enter(node)
100
100
  # The only scenario where Sorbet doesn't provide constant completion is on ignored files. Even if the file has
101
101
  # no sigil, Sorbet will still provide completion for constants
102
- return if @sorbet_level != Document::SorbetLevel::Ignore
102
+ return if @sorbet_level != RubyDocument::SorbetLevel::Ignore
103
103
 
104
104
  name = constant_name(node)
105
105
  return if name.nil?
@@ -122,7 +122,7 @@ module RubyLsp
122
122
  def on_constant_path_node_enter(node)
123
123
  # The only scenario where Sorbet doesn't provide constant completion is on ignored files. Even if the file has
124
124
  # no sigil, Sorbet will still provide completion for constants
125
- return if @sorbet_level != Document::SorbetLevel::Ignore
125
+ return if @sorbet_level != RubyDocument::SorbetLevel::Ignore
126
126
 
127
127
  name = constant_name(node)
128
128
  return if name.nil?
@@ -134,7 +134,7 @@ module RubyLsp
134
134
  def on_call_node_enter(node)
135
135
  # The only scenario where Sorbet doesn't provide constant completion is on ignored files. Even if the file has
136
136
  # no sigil, Sorbet will still provide completion for constants
137
- if @sorbet_level == Document::SorbetLevel::Ignore
137
+ if @sorbet_level == RubyDocument::SorbetLevel::Ignore
138
138
  receiver = node.receiver
139
139
 
140
140
  # When writing `Foo::`, the AST assigns a method call node (because you can use that syntax to invoke
@@ -257,7 +257,7 @@ module RubyLsp
257
257
  def handle_instance_variable_completion(name, location)
258
258
  # Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able
259
259
  # to provide all features for them
260
- return if @sorbet_level == Document::SorbetLevel::Strict
260
+ return if @sorbet_level == RubyDocument::SorbetLevel::Strict
261
261
 
262
262
  type = @type_inferrer.infer_receiver_type(@node_context)
263
263
  return unless type
@@ -20,7 +20,7 @@ module RubyLsp
20
20
  uri: URI::Generic,
21
21
  node_context: NodeContext,
22
22
  dispatcher: Prism::Dispatcher,
23
- sorbet_level: Document::SorbetLevel,
23
+ sorbet_level: RubyDocument::SorbetLevel,
24
24
  ).void
25
25
  end
26
26
  def initialize(response_builder, global_state, language_id, uri, node_context, dispatcher, sorbet_level) # rubocop:disable Metrics/ParameterLists
@@ -181,7 +181,7 @@ module RubyLsp
181
181
  def handle_instance_variable_definition(name)
182
182
  # Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able
183
183
  # to provide all features for them
184
- return if @sorbet_level == Document::SorbetLevel::Strict
184
+ return if @sorbet_level == RubyDocument::SorbetLevel::Strict
185
185
 
186
186
  type = @type_inferrer.infer_receiver_type(@node_context)
187
187
  return unless type
@@ -289,7 +289,7 @@ module RubyLsp
289
289
  # additional behavior on top of jumping to RBIs. The only sigil where Sorbet cannot handle constants is typed
290
290
  # ignore
291
291
  file_path = entry.file_path
292
- next if @sorbet_level != Document::SorbetLevel::Ignore && not_in_dependencies?(file_path)
292
+ next if @sorbet_level != RubyDocument::SorbetLevel::Ignore && not_in_dependencies?(file_path)
293
293
 
294
294
  @response_builder << Interface::LocationLink.new(
295
295
  target_uri: URI::Generic.from_path(path: file_path).to_s,
@@ -42,7 +42,7 @@ module RubyLsp
42
42
  uri: URI::Generic,
43
43
  node_context: NodeContext,
44
44
  dispatcher: Prism::Dispatcher,
45
- sorbet_level: Document::SorbetLevel,
45
+ sorbet_level: RubyDocument::SorbetLevel,
46
46
  ).void
47
47
  end
48
48
  def initialize(response_builder, global_state, uri, node_context, dispatcher, sorbet_level) # rubocop:disable Metrics/ParameterLists
@@ -73,7 +73,7 @@ module RubyLsp
73
73
 
74
74
  sig { params(node: Prism::ConstantReadNode).void }
75
75
  def on_constant_read_node_enter(node)
76
- return if @sorbet_level != Document::SorbetLevel::Ignore
76
+ return if @sorbet_level != RubyDocument::SorbetLevel::Ignore
77
77
 
78
78
  name = constant_name(node)
79
79
  return if name.nil?
@@ -83,14 +83,14 @@ module RubyLsp
83
83
 
84
84
  sig { params(node: Prism::ConstantWriteNode).void }
85
85
  def on_constant_write_node_enter(node)
86
- return if @sorbet_level != Document::SorbetLevel::Ignore
86
+ return if @sorbet_level != RubyDocument::SorbetLevel::Ignore
87
87
 
88
88
  generate_hover(node.name.to_s, node.name_loc)
89
89
  end
90
90
 
91
91
  sig { params(node: Prism::ConstantPathNode).void }
92
92
  def on_constant_path_node_enter(node)
93
- return if @sorbet_level != Document::SorbetLevel::Ignore
93
+ return if @sorbet_level != RubyDocument::SorbetLevel::Ignore
94
94
 
95
95
  name = constant_name(node)
96
96
  return if name.nil?
@@ -174,7 +174,10 @@ module RubyLsp
174
174
  methods = @index.resolve_method(message, type.name, inherited_only: inherited_only)
175
175
  return unless methods
176
176
 
177
- title = "#{message}#{T.must(methods.first).decorated_parameters}"
177
+ first_method = T.must(methods.first)
178
+
179
+ title = "#{message}#{first_method.decorated_parameters}"
180
+ title << first_method.formatted_signatures
178
181
 
179
182
  if type.is_a?(TypeInferrer::GuessedType)
180
183
  title << "\n\nGuessed receiver: #{type.name}"
@@ -190,7 +193,7 @@ module RubyLsp
190
193
  def handle_instance_variable_hover(name)
191
194
  # Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able
192
195
  # to provide all features for them
193
- return if @sorbet_level == Document::SorbetLevel::Strict
196
+ return if @sorbet_level == RubyDocument::SorbetLevel::Strict
194
197
 
195
198
  type = @type_inferrer.infer_receiver_type(@node_context)
196
199
  return unless type
@@ -13,7 +13,7 @@ module RubyLsp
13
13
  global_state: GlobalState,
14
14
  node_context: NodeContext,
15
15
  dispatcher: Prism::Dispatcher,
16
- sorbet_level: Document::SorbetLevel,
16
+ sorbet_level: RubyDocument::SorbetLevel,
17
17
  ).void
18
18
  end
19
19
  def initialize(response_builder, global_state, node_context, dispatcher, sorbet_level)
@@ -23,6 +23,8 @@ module RubyLsp
23
23
  #
24
24
  class CodeActionResolve < Request
25
25
  extend T::Sig
26
+ include Support::Common
27
+
26
28
  NEW_VARIABLE_NAME = "new_variable"
27
29
  NEW_METHOD_NAME = "new_method"
28
30
 
@@ -36,7 +38,7 @@ module RubyLsp
36
38
  end
37
39
  end
38
40
 
39
- sig { params(document: Document, code_action: T::Hash[Symbol, T.untyped]).void }
41
+ sig { params(document: RubyDocument, code_action: T::Hash[Symbol, T.untyped]).void }
40
42
  def initialize(document, code_action)
41
43
  super()
42
44
  @document = document
@@ -45,20 +47,62 @@ module RubyLsp
45
47
 
46
48
  sig { override.returns(T.any(Interface::CodeAction, Error)) }
47
49
  def perform
50
+ return Error::EmptySelection if @document.source.empty?
51
+
48
52
  case @code_action[:title]
49
53
  when CodeActions::EXTRACT_TO_VARIABLE_TITLE
50
54
  refactor_variable
51
55
  when CodeActions::EXTRACT_TO_METHOD_TITLE
52
56
  refactor_method
57
+ when CodeActions::TOGGLE_BLOCK_STYLE_TITLE
58
+ switch_block_style
53
59
  else
54
60
  Error::UnknownCodeAction
55
61
  end
56
62
  end
57
63
 
64
+ private
65
+
58
66
  sig { returns(T.any(Interface::CodeAction, Error)) }
59
- def refactor_variable
60
- return Error::EmptySelection if @document.source.empty?
67
+ def switch_block_style
68
+ source_range = @code_action.dig(:data, :range)
69
+ return Error::EmptySelection if source_range[:start] == source_range[:end]
70
+
71
+ target = @document.locate_first_within_range(
72
+ @code_action.dig(:data, :range),
73
+ node_types: [Prism::CallNode],
74
+ )
75
+
76
+ return Error::InvalidTargetRange unless target.is_a?(Prism::CallNode)
77
+
78
+ node = target.block
79
+ return Error::InvalidTargetRange unless node.is_a?(Prism::BlockNode)
61
80
 
81
+ indentation = " " * target.location.start_column unless node.opening_loc.slice == "do"
82
+
83
+ Interface::CodeAction.new(
84
+ title: CodeActions::TOGGLE_BLOCK_STYLE_TITLE,
85
+ edit: Interface::WorkspaceEdit.new(
86
+ document_changes: [
87
+ Interface::TextDocumentEdit.new(
88
+ text_document: Interface::OptionalVersionedTextDocumentIdentifier.new(
89
+ uri: @code_action.dig(:data, :uri),
90
+ version: nil,
91
+ ),
92
+ edits: [
93
+ Interface::TextEdit.new(
94
+ range: range_from_location(node.location),
95
+ new_text: recursively_switch_nested_block_styles(node, indentation),
96
+ ),
97
+ ],
98
+ ),
99
+ ],
100
+ ),
101
+ )
102
+ end
103
+
104
+ sig { returns(T.any(Interface::CodeAction, Error)) }
105
+ def refactor_variable
62
106
  source_range = @code_action.dig(:data, :range)
63
107
  return Error::EmptySelection if source_range[:start] == source_range[:end]
64
108
 
@@ -153,8 +197,6 @@ module RubyLsp
153
197
 
154
198
  sig { returns(T.any(Interface::CodeAction, Error)) }
155
199
  def refactor_method
156
- return Error::EmptySelection if @document.source.empty?
157
-
158
200
  source_range = @code_action.dig(:data, :range)
159
201
  return Error::EmptySelection if source_range[:start] == source_range[:end]
160
202
 
@@ -206,8 +248,6 @@ module RubyLsp
206
248
  )
207
249
  end
208
250
 
209
- private
210
-
211
251
  sig { params(range: T::Hash[Symbol, T.untyped], new_text: String).returns(Interface::TextEdit) }
212
252
  def create_text_edit(range, new_text)
213
253
  Interface::TextEdit.new(
@@ -218,6 +258,64 @@ module RubyLsp
218
258
  new_text: new_text,
219
259
  )
220
260
  end
261
+
262
+ sig { params(node: Prism::BlockNode, indentation: T.nilable(String)).returns(String) }
263
+ def recursively_switch_nested_block_styles(node, indentation)
264
+ parameters = node.parameters
265
+ body = node.body
266
+
267
+ # We use the indentation to differentiate between do...end and brace style blocks because only the do...end
268
+ # style requires the indentation to build the edit.
269
+ #
270
+ # If the block is using `do...end` style, we change it to a single line brace block. Newlines are turned into
271
+ # semi colons, so that the result is valid Ruby code and still a one liner. If the block is using brace style,
272
+ # we do the opposite and turn it into a `do...end` block, making all semi colons into newlines.
273
+ source = +""
274
+
275
+ if indentation
276
+ source << "do"
277
+ source << " #{parameters.slice}" if parameters
278
+ source << "\n#{indentation} "
279
+ source << switch_block_body(body, indentation) if body
280
+ source << "\n#{indentation}end"
281
+ else
282
+ source << "{ "
283
+ source << "#{parameters.slice} " if parameters
284
+ source << switch_block_body(body, nil) if body
285
+ source << "}"
286
+ end
287
+
288
+ source
289
+ end
290
+
291
+ sig { params(body: Prism::Node, indentation: T.nilable(String)).returns(String) }
292
+ def switch_block_body(body, indentation)
293
+ # Check if there are any nested blocks inside of the current block
294
+ body_loc = body.location
295
+ nested_block = @document.locate_first_within_range(
296
+ {
297
+ start: { line: body_loc.start_line - 1, character: body_loc.start_column },
298
+ end: { line: body_loc.end_line - 1, character: body_loc.end_column },
299
+ },
300
+ node_types: [Prism::BlockNode],
301
+ )
302
+
303
+ body_content = body.slice.dup
304
+
305
+ # If there are nested blocks, then we change their style too and we have to mutate the string using the
306
+ # relative position in respect to the beginning of the body
307
+ if nested_block.is_a?(Prism::BlockNode)
308
+ location = nested_block.location
309
+ correction_start = location.start_offset - body_loc.start_offset
310
+ correction_end = location.end_offset - body_loc.start_offset
311
+ next_indentation = indentation ? "#{indentation} " : nil
312
+
313
+ body_content[correction_start...correction_end] =
314
+ recursively_switch_nested_block_styles(nested_block, next_indentation)
315
+ end
316
+
317
+ indentation ? body_content.gsub(";", "\n") : "#{body_content.gsub("\n", ";")} "
318
+ end
221
319
  end
222
320
  end
223
321
  end
@@ -21,6 +21,7 @@ module RubyLsp
21
21
 
22
22
  EXTRACT_TO_VARIABLE_TITLE = "Refactor: Extract Variable"
23
23
  EXTRACT_TO_METHOD_TITLE = "Refactor: Extract Method"
24
+ TOGGLE_BLOCK_STYLE_TITLE = "Refactor: Toggle block style"
24
25
 
25
26
  class << self
26
27
  extend T::Sig
@@ -69,6 +70,11 @@ module RubyLsp
69
70
  kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
70
71
  data: { range: @range, uri: @uri.to_s },
71
72
  )
73
+ code_actions << Interface::CodeAction.new(
74
+ title: TOGGLE_BLOCK_STYLE_TITLE,
75
+ kind: Constant::CodeActionKind::REFACTOR_REWRITE,
76
+ data: { range: @range, uri: @uri.to_s },
77
+ )
72
78
  end
73
79
 
74
80
  code_actions
@@ -49,7 +49,7 @@ module RubyLsp
49
49
  document: Document,
50
50
  global_state: GlobalState,
51
51
  params: T::Hash[Symbol, T.untyped],
52
- sorbet_level: Document::SorbetLevel,
52
+ sorbet_level: RubyDocument::SorbetLevel,
53
53
  dispatcher: Prism::Dispatcher,
54
54
  ).void
55
55
  end
@@ -63,6 +63,7 @@ module RubyLsp
63
63
 
64
64
  if first_entry.is_a?(RubyIndexer::Entry::Member)
65
65
  label = +"#{label}#{first_entry.decorated_parameters}"
66
+ label << first_entry.formatted_signatures
66
67
  end
67
68
 
68
69
  guessed_type = @item.dig(:data, :guessed_type)
@@ -42,7 +42,7 @@ module RubyLsp
42
42
  global_state: GlobalState,
43
43
  position: T::Hash[Symbol, T.untyped],
44
44
  dispatcher: Prism::Dispatcher,
45
- sorbet_level: Document::SorbetLevel,
45
+ sorbet_level: RubyDocument::SorbetLevel,
46
46
  ).void
47
47
  end
48
48
  def initialize(document, global_state, position, dispatcher, sorbet_level)
@@ -36,7 +36,7 @@ module RubyLsp
36
36
  global_state: GlobalState,
37
37
  position: T::Hash[Symbol, T.untyped],
38
38
  dispatcher: Prism::Dispatcher,
39
- sorbet_level: Document::SorbetLevel,
39
+ sorbet_level: RubyDocument::SorbetLevel,
40
40
  ).void
41
41
  end
42
42
  def initialize(document, global_state, position, dispatcher, sorbet_level)
@@ -46,7 +46,7 @@ module RubyLsp
46
46
  position: T::Hash[Symbol, T.untyped],
47
47
  context: T.nilable(T::Hash[Symbol, T.untyped]),
48
48
  dispatcher: Prism::Dispatcher,
49
- sorbet_level: Document::SorbetLevel,
49
+ sorbet_level: RubyDocument::SorbetLevel,
50
50
  ).void
51
51
  end
52
52
  def initialize(document, global_state, position, context, dispatcher, sorbet_level) # rubocop:disable Metrics/ParameterLists
@@ -209,9 +209,9 @@ module RubyLsp
209
209
  end
210
210
  end
211
211
 
212
- sig { params(sorbet_level: Document::SorbetLevel).returns(T::Boolean) }
212
+ sig { params(sorbet_level: RubyDocument::SorbetLevel).returns(T::Boolean) }
213
213
  def sorbet_level_true_or_higher?(sorbet_level)
214
- sorbet_level == Document::SorbetLevel::True || sorbet_level == Document::SorbetLevel::Strict
214
+ sorbet_level == RubyDocument::SorbetLevel::True || sorbet_level == RubyDocument::SorbetLevel::Strict
215
215
  end
216
216
  end
217
217
  end
@@ -3,6 +3,16 @@
3
3
 
4
4
  module RubyLsp
5
5
  class RubyDocument < Document
6
+ class SorbetLevel < T::Enum
7
+ enums do
8
+ None = new("none")
9
+ Ignore = new("ignore")
10
+ False = new("false")
11
+ True = new("true")
12
+ Strict = new("strict")
13
+ end
14
+ end
15
+
6
16
  sig { override.returns(Prism::ParseResult) }
7
17
  def parse
8
18
  return @parse_result unless @needs_parsing
@@ -20,5 +30,59 @@ module RubyLsp
20
30
  def language_id
21
31
  LanguageId::Ruby
22
32
  end
33
+
34
+ sig { returns(SorbetLevel) }
35
+ def sorbet_level
36
+ sigil = parse_result.magic_comments.find do |comment|
37
+ comment.key == "typed"
38
+ end&.value
39
+
40
+ case sigil
41
+ when "ignore"
42
+ SorbetLevel::Ignore
43
+ when "false"
44
+ SorbetLevel::False
45
+ when "true"
46
+ SorbetLevel::True
47
+ when "strict", "strong"
48
+ SorbetLevel::Strict
49
+ else
50
+ SorbetLevel::None
51
+ end
52
+ end
53
+
54
+ sig do
55
+ params(
56
+ range: T::Hash[Symbol, T.untyped],
57
+ node_types: T::Array[T.class_of(Prism::Node)],
58
+ ).returns(T.nilable(Prism::Node))
59
+ end
60
+ def locate_first_within_range(range, node_types: [])
61
+ scanner = create_scanner
62
+ start_position = scanner.find_char_position(range[:start])
63
+ end_position = scanner.find_char_position(range[:end])
64
+ desired_range = (start_position...end_position)
65
+ queue = T.let(@parse_result.value.child_nodes.compact, T::Array[T.nilable(Prism::Node)])
66
+
67
+ until queue.empty?
68
+ candidate = queue.shift
69
+
70
+ # Skip nil child nodes
71
+ next if candidate.nil?
72
+
73
+ # Add the next child_nodes to the queue to be processed. The order here is important! We want to move in the
74
+ # same order as the visiting mechanism, which means searching the child nodes before moving on to the next
75
+ # sibling
76
+ T.unsafe(queue).unshift(*candidate.child_nodes)
77
+
78
+ # Skip if the current node doesn't cover the desired position
79
+ loc = candidate.location
80
+
81
+ if desired_range.cover?(loc.start_offset...loc.end_offset) &&
82
+ (node_types.empty? || node_types.any? { |type| candidate.class == type })
83
+ return candidate
84
+ end
85
+ end
86
+ end
23
87
  end
24
88
  end
@@ -19,10 +19,10 @@ module RubyLsp
19
19
  def process_message(message)
20
20
  case message[:method]
21
21
  when "initialize"
22
- $stderr.puts("Initializing Ruby LSP v#{VERSION}...")
22
+ send_log_message("Initializing Ruby LSP v#{VERSION}...")
23
23
  run_initialize(message)
24
24
  when "initialized"
25
- $stderr.puts("Finished initializing Ruby LSP!") unless @test_mode
25
+ send_log_message("Finished initializing Ruby LSP!") unless @test_mode
26
26
  run_initialized
27
27
  when "textDocument/didOpen"
28
28
  text_document_did_open(message)
@@ -121,12 +121,20 @@ module RubyLsp
121
121
  end
122
122
  end
123
123
 
124
- $stderr.puts("Error processing #{message[:method]}: #{e.full_message}")
124
+ send_log_message("Error processing #{message[:method]}: #{e.full_message}", type: Constant::MessageType::ERROR)
125
125
  end
126
126
 
127
127
  sig { void }
128
128
  def load_addons
129
- Addon.load_addons(@global_state, @outgoing_queue)
129
+ errors = Addon.load_addons(@global_state, @outgoing_queue)
130
+
131
+ if errors.any?
132
+ send_log_message(
133
+ "Error loading addons:\n\n#{errors.map(&:full_message).join("\n\n")}",
134
+ type: Constant::MessageType::WARNING,
135
+ )
136
+ end
137
+
130
138
  errored_addons = Addon.addons.select(&:error?)
131
139
 
132
140
  if errored_addons.any?
@@ -140,7 +148,12 @@ module RubyLsp
140
148
  ),
141
149
  )
142
150
 
143
- $stderr.puts(errored_addons.map(&:errors_details).join("\n\n")) unless @test_mode
151
+ unless @test_mode
152
+ send_log_message(
153
+ errored_addons.map(&:errors_details).join("\n\n"),
154
+ type: Constant::MessageType::WARNING,
155
+ )
156
+ end
144
157
  end
145
158
  end
146
159
 
@@ -149,7 +162,7 @@ module RubyLsp
149
162
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
150
163
  def run_initialize(message)
151
164
  options = message[:params]
152
- @global_state.apply_options(options)
165
+ global_state_notifications = @global_state.apply_options(options)
153
166
 
154
167
  client_name = options.dig(:clientInfo, :name)
155
168
  @store.client_name = client_name if client_name
@@ -258,6 +271,8 @@ module RubyLsp
258
271
  process_indexing_configuration(options.dig(:initializationOptions, :indexing))
259
272
 
260
273
  begin_progress("indexing-progress", "Ruby LSP: indexing files")
274
+
275
+ global_state_notifications.each { |notification| send_message(notification) }
261
276
  end
262
277
 
263
278
  sig { void }
@@ -281,7 +296,7 @@ module RubyLsp
281
296
  @mutex.synchronize do
282
297
  text_document = message.dig(:params, :textDocument)
283
298
  language_id = case text_document[:languageId]
284
- when "erb"
299
+ when "erb", "eruby"
285
300
  Document::LanguageId::ERB
286
301
  else
287
302
  Document::LanguageId::Ruby
@@ -480,9 +495,10 @@ module RubyLsp
480
495
  )
481
496
  end
482
497
 
483
- sig { params(document: Document).returns(Document::SorbetLevel) }
498
+ sig { params(document: Document).returns(RubyDocument::SorbetLevel) }
484
499
  def sorbet_level(document)
485
- return Document::SorbetLevel::Ignore unless @global_state.has_type_checker
500
+ return RubyDocument::SorbetLevel::Ignore unless @global_state.has_type_checker
501
+ return RubyDocument::SorbetLevel::Ignore unless document.is_a?(RubyDocument)
486
502
 
487
503
  document.sorbet_level
488
504
  end
@@ -520,6 +536,12 @@ module RubyLsp
520
536
  params = message[:params]
521
537
  uri = URI(params.dig(:data, :uri))
522
538
  document = @store.get(uri)
539
+
540
+ unless document.is_a?(RubyDocument)
541
+ send_message(Notification.window_show_error("Code actions are currently only available for Ruby documents"))
542
+ raise Requests::CodeActionResolve::CodeActionError
543
+ end
544
+
523
545
  result = Requests::CodeActionResolve.new(document, params).perform
524
546
 
525
547
  case result
@@ -862,7 +884,7 @@ module RubyLsp
862
884
 
863
885
  if File.exist?(index_path)
864
886
  begin
865
- RubyIndexer.configuration.apply_config(YAML.parse_file(index_path).to_ruby)
887
+ @global_state.index.configuration.apply_config(YAML.parse_file(index_path).to_ruby)
866
888
  send_message(
867
889
  Notification.new(
868
890
  method: "window/showMessage",
@@ -891,7 +913,7 @@ module RubyLsp
891
913
  return unless indexing_options
892
914
 
893
915
  # The index expects snake case configurations, but VS Code standardizes on camel case settings
894
- RubyIndexer.configuration.apply_config(
916
+ @global_state.index.configuration.apply_config(
895
917
  indexing_options.transform_keys { |key| key.to_s.gsub(/([A-Z])/, "_\\1").downcase },
896
918
  )
897
919
  end