ruby-lsp 0.17.11 → 0.17.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -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