ruby-lsp 0.3.8 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f1ed447e51909ada66f6bebceca82d807686a4889468838a797e2ed8afff50e
4
- data.tar.gz: 7b429773bbdebc171debbe446adb868136bd15b5f493542898df0a9c82f3514e
3
+ metadata.gz: '0812eed9a874fa78fbe75cf2e8dc2bd58c2dfb67a61353f732aaba965ae9d66f'
4
+ data.tar.gz: 6d9a567aa54c95f5fd7ada3756d4b5575f068bee90f19f99f83f485d91de83a0
5
5
  SHA512:
6
- metadata.gz: 62e8a6ee36c43dec6ec0af16fd19830b6369446455a5fc16871c368c8bb4ccdb47c0d9c5678990a43604bf03c622c2ef13bb82c6a6e9f26fdcde72b3b18ec99e
7
- data.tar.gz: e441a670c22bcdbfdc3afe99743fe6e1c4aa5f28edaedcf09f47255372322c52ead68988bc6d7ce09170e368e4059c94dac73b395a640c86f20b2969ae8e8095
6
+ metadata.gz: 53311978b0c6d32a34e1e69ddeea1d4b154ee0e2bc1cd426fd7e4db6ab46eeb7a5dff7d0516b5c2c722f95f05a0c5051fe7caf874e14740b5023b5f0e79da962
7
+ data.tar.gz: ce04150602707622dd0d463a35a2a9c94fe7b24b1e376dbeeae4e45f4487438219f8ab48056658946e9f7cc56136714496fdfbddf7513b77fea334aac2687ae3
data/README.md CHANGED
@@ -4,6 +4,37 @@
4
4
 
5
5
  This gem is an implementation of the [language server protocol specification](https://microsoft.github.io/language-server-protocol/) for Ruby, used to improve editor features.
6
6
 
7
+ # Overview
8
+
9
+ The intention of Ruby LSP is to provide a fast, robust and feature-rich coding environment for Ruby developers.
10
+
11
+ It's part of a [wider Shopify goal](https://github.com/Shopify/vscode-shopify-ruby) to provide a state-of-the-art experience to Ruby developers using modern standards for cross-editor features, documentation and debugging.
12
+
13
+ It provides many features, including:
14
+
15
+ * Syntax highlighting
16
+ * Linting and formatting
17
+ * Code folding
18
+ * Selection ranges
19
+
20
+ It does not perform typechecking, so its features are implemented on a best-effort basis, aiming to be as accurate as possible.
21
+
22
+ Planned future features include:
23
+
24
+ * Auto-completion and navigation ("Go To Definition") ([prototype](https://github.com/Shopify/ruby-lsp/pull/429))
25
+ * Support for plug-ins to extend behavior
26
+
27
+ The Ruby LSP does not perform any type-checking or provide any type-related assistance, but it can be used alongside [Sorbet](https://github.com/sorbet/sorbet)'s LSP server.
28
+
29
+ At the time of writing, these are the major differences between Ruby LSP and [Solargraph](https://solargraph.org/):
30
+
31
+ * Solargraph [uses](https://solargraph.org/guides/yard) YARD documentation to gather information about your project and its gem dependencies. This provides functionality such as context-aware auto-completion and navigation ("Go To Definition")
32
+ * Solargraph can be used as a globally installed gem, but Ruby LSP must be added to the Gemfile or gemspec if using RuboCop. (There are pros and cons to each approach)
33
+
34
+ ## Learn More
35
+
36
+ * [RubyConf 2022: Improving the development experience with language servers](https://www.youtube.com/watch?v=kEfXPTm1aCI) ([Vinicius Stock](https://github.com/vinistock))
37
+
7
38
  ## Usage
8
39
 
9
40
  Install the gem. There's no need to require it, since the server is used as a standalone executable.
@@ -17,7 +48,7 @@ end
17
48
  If using VS Code, install the [Ruby LSP extension](https://github.com/Shopify/vscode-ruby-lsp) to get the extra features in
18
49
  the editor.
19
50
 
20
- See the [documentation](https://shopify.github.io/ruby-lsp) for
51
+ See the [documentation](https://shopify.github.io/ruby-lsp) for more in-depth details about the
21
52
  [supported features](https://shopify.github.io/ruby-lsp/RubyLsp/Requests.html).
22
53
 
23
54
  ## Contributing
@@ -105,6 +136,13 @@ The `launch.json` contains a 'Minitest - current file' configuration for the deb
105
136
  1. When the breakpoint is triggered, the process will pause and VS Code will connect to the debugger and activate the debugger UI.
106
137
  1. Open the Debug Console view to use the debugger's REPL.
107
138
 
139
+ ## Spell Checking
140
+
141
+ VS Code users will be prompted to enable the [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) extension.
142
+ By default this will be enabled for all workspaces, but you can choose to selectively enable or disable it per workspace.
143
+
144
+ If you introduce a word which the spell checker does not recognize, you can add it to the `cspell.json` configuration alongside your PR.
145
+
108
146
  ## License
109
147
 
110
148
  The gem is available as open source under the terms of the
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.8
1
+ 0.4.1
data/exe/ruby-lsp CHANGED
@@ -18,4 +18,5 @@ rescue
18
18
  nil
19
19
  end
20
20
 
21
- require_relative "../lib/ruby_lsp/server"
21
+ require_relative "../lib/ruby_lsp/internal"
22
+ RubyLsp::Server.new.start
@@ -21,9 +21,10 @@ module RubyLsp
21
21
  @encoding = T.let(encoding, String)
22
22
  @source = T.let(source, String)
23
23
  @unparsed_edits = T.let([], T::Array[EditShape])
24
+ @syntax_error = T.let(false, T::Boolean)
24
25
  @tree = T.let(SyntaxTree.parse(@source), T.nilable(SyntaxTree::Node))
25
26
  rescue SyntaxTree::Parser::ParseError
26
- # Do not raise if we failed to parse
27
+ @syntax_error = true
27
28
  end
28
29
 
29
30
  sig { params(other: Document).returns(T::Boolean) }
@@ -68,14 +69,15 @@ module RubyLsp
68
69
  return if @unparsed_edits.empty?
69
70
 
70
71
  @tree = SyntaxTree.parse(@source)
72
+ @syntax_error = false
71
73
  @unparsed_edits.clear
72
74
  rescue SyntaxTree::Parser::ParseError
73
- # Do nothing if we fail parse
75
+ @syntax_error = true
74
76
  end
75
77
 
76
78
  sig { returns(T::Boolean) }
77
79
  def syntax_error?
78
- @unparsed_edits.any?
80
+ @syntax_error
79
81
  end
80
82
 
81
83
  sig { returns(T::Boolean) }
@@ -0,0 +1,390 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ # This class dispatches a request execution to the right request class. No IO should happen anywhere here!
6
+ class Executor
7
+ extend T::Sig
8
+
9
+ sig { params(store: Store).void }
10
+ def initialize(store)
11
+ # Requests that mutate the store must be run sequentially! Parallel requests only receive a temporary copy of the
12
+ # store
13
+ @store = store
14
+ @notifications = T.let([], T::Array[Notification])
15
+ end
16
+
17
+ sig { params(request: T::Hash[Symbol, T.untyped]).returns(Result) }
18
+ def execute(request)
19
+ response = T.let(nil, T.untyped)
20
+ error = T.let(nil, T.nilable(Exception))
21
+
22
+ request_time = Benchmark.realtime do
23
+ response = run(request)
24
+ rescue StandardError, LoadError => e
25
+ error = e
26
+ end
27
+
28
+ Result.new(response: response, error: error, request_time: request_time, notifications: @notifications)
29
+ end
30
+
31
+ private
32
+
33
+ sig { params(request: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
34
+ def run(request)
35
+ uri = request.dig(:params, :textDocument, :uri)
36
+
37
+ case request[:method]
38
+ when "initialize"
39
+ initialize_request(request.dig(:params))
40
+ when "initialized"
41
+ warn("Ruby LSP is ready")
42
+ VOID
43
+ when "textDocument/didOpen"
44
+ text_document_did_open(uri, request.dig(:params, :textDocument, :text))
45
+ when "textDocument/didClose"
46
+ @notifications << Notification.new(
47
+ message: "textDocument/publishDiagnostics",
48
+ params: Interface::PublishDiagnosticsParams.new(uri: uri, diagnostics: []),
49
+ )
50
+
51
+ text_document_did_close(uri)
52
+ when "textDocument/didChange"
53
+ text_document_did_change(uri, request.dig(:params, :contentChanges))
54
+ when "textDocument/foldingRange"
55
+ folding_range(uri)
56
+ when "textDocument/documentLink"
57
+ document_link(uri)
58
+ when "textDocument/selectionRange"
59
+ selection_range(uri, request.dig(:params, :positions))
60
+ when "textDocument/documentSymbol"
61
+ document_symbol(uri)
62
+ when "textDocument/semanticTokens/full"
63
+ semantic_tokens_full(uri)
64
+ when "textDocument/semanticTokens/range"
65
+ semantic_tokens_range(uri, request.dig(:params, :range))
66
+ when "textDocument/formatting"
67
+ begin
68
+ formatting(uri)
69
+ rescue StandardError => error
70
+ @notifications << Notification.new(
71
+ message: "window/showMessage",
72
+ params: Interface::ShowMessageParams.new(
73
+ type: Constant::MessageType::ERROR,
74
+ message: "Formatting error: #{error.message}",
75
+ ),
76
+ )
77
+
78
+ nil
79
+ end
80
+ when "textDocument/documentHighlight"
81
+ document_highlight(uri, request.dig(:params, :position))
82
+ when "textDocument/onTypeFormatting"
83
+ on_type_formatting(uri, request.dig(:params, :position), request.dig(:params, :ch))
84
+ when "textDocument/hover"
85
+ hover(uri, request.dig(:params, :position))
86
+ when "textDocument/inlayHint"
87
+ inlay_hint(uri, request.dig(:params, :range))
88
+ when "textDocument/codeAction"
89
+ code_action(uri, request.dig(:params, :range), request.dig(:params, :context))
90
+ when "codeAction/resolve"
91
+ code_action_resolve(request.dig(:params))
92
+ when "textDocument/diagnostic"
93
+ begin
94
+ diagnostic(uri)
95
+ rescue StandardError => error
96
+ @notifications << Notification.new(
97
+ message: "window/showMessage",
98
+ params: Interface::ShowMessageParams.new(
99
+ type: Constant::MessageType::ERROR,
100
+ message: "Error running diagnostics: #{error.message}",
101
+ ),
102
+ )
103
+
104
+ nil
105
+ end
106
+ when "textDocument/completion"
107
+ completion(uri, request.dig(:params, :position))
108
+ end
109
+ end
110
+
111
+ sig { params(uri: String).returns(T::Array[Interface::FoldingRange]) }
112
+ def folding_range(uri)
113
+ @store.cache_fetch(uri, :folding_ranges) do |document|
114
+ Requests::FoldingRanges.new(document).run
115
+ end
116
+ end
117
+
118
+ sig do
119
+ params(
120
+ uri: String,
121
+ position: Document::PositionShape,
122
+ ).returns(T.nilable(Interface::Hover))
123
+ end
124
+ def hover(uri, position)
125
+ RubyLsp::Requests::Hover.new(@store.get(uri), position).run
126
+ end
127
+
128
+ sig { params(uri: String).returns(T::Array[Interface::DocumentLink]) }
129
+ def document_link(uri)
130
+ @store.cache_fetch(uri, :document_link) do |document|
131
+ RubyLsp::Requests::DocumentLink.new(uri, document).run
132
+ end
133
+ end
134
+
135
+ sig { params(uri: String).returns(T::Array[Interface::DocumentSymbol]) }
136
+ def document_symbol(uri)
137
+ @store.cache_fetch(uri, :document_symbol) do |document|
138
+ Requests::DocumentSymbol.new(document).run
139
+ end
140
+ end
141
+
142
+ sig { params(uri: String, content_changes: T::Array[Document::EditShape]).returns(Object) }
143
+ def text_document_did_change(uri, content_changes)
144
+ @store.push_edits(uri, content_changes)
145
+ VOID
146
+ end
147
+
148
+ sig { params(uri: String, text: String).returns(Object) }
149
+ def text_document_did_open(uri, text)
150
+ @store.set(uri, text)
151
+ VOID
152
+ end
153
+
154
+ sig { params(uri: String).returns(Object) }
155
+ def text_document_did_close(uri)
156
+ @store.delete(uri)
157
+ VOID
158
+ end
159
+
160
+ sig do
161
+ params(
162
+ uri: String,
163
+ positions: T::Array[Document::PositionShape],
164
+ ).returns(T.nilable(T::Array[T.nilable(Requests::Support::SelectionRange)]))
165
+ end
166
+ def selection_range(uri, positions)
167
+ ranges = @store.cache_fetch(uri, :selection_ranges) do |document|
168
+ Requests::SelectionRanges.new(document).run
169
+ end
170
+
171
+ # Per the selection range request spec (https://microsoft.github.io/language-server-protocol/specification#textDocument_selectionRange),
172
+ # every position in the positions array should have an element at the same index in the response
173
+ # array. For positions without a valid selection range, the corresponding element in the response
174
+ # array will be nil.
175
+
176
+ unless ranges.nil?
177
+ positions.map do |position|
178
+ ranges.find do |range|
179
+ range.cover?(position)
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ sig { params(uri: String).returns(Interface::SemanticTokens) }
186
+ def semantic_tokens_full(uri)
187
+ @store.cache_fetch(uri, :semantic_highlighting) do |document|
188
+ T.cast(
189
+ Requests::SemanticHighlighting.new(
190
+ document,
191
+ encoder: Requests::Support::SemanticTokenEncoder.new,
192
+ ).run,
193
+ Interface::SemanticTokens,
194
+ )
195
+ end
196
+ end
197
+
198
+ sig { params(uri: String).returns(T.nilable(T::Array[Interface::TextEdit])) }
199
+ def formatting(uri)
200
+ Requests::Formatting.new(uri, @store.get(uri)).run
201
+ end
202
+
203
+ sig do
204
+ params(
205
+ uri: String,
206
+ position: Document::PositionShape,
207
+ character: String,
208
+ ).returns(T::Array[Interface::TextEdit])
209
+ end
210
+ def on_type_formatting(uri, position, character)
211
+ Requests::OnTypeFormatting.new(@store.get(uri), position, character).run
212
+ end
213
+
214
+ sig do
215
+ params(
216
+ uri: String,
217
+ position: Document::PositionShape,
218
+ ).returns(T::Array[Interface::DocumentHighlight])
219
+ end
220
+ def document_highlight(uri, position)
221
+ Requests::DocumentHighlight.new(@store.get(uri), position).run
222
+ end
223
+
224
+ sig { params(uri: String, range: Document::RangeShape).returns(T::Array[Interface::InlayHint]) }
225
+ def inlay_hint(uri, range)
226
+ document = @store.get(uri)
227
+ start_line = range.dig(:start, :line)
228
+ end_line = range.dig(:end, :line)
229
+
230
+ Requests::InlayHints.new(document, start_line..end_line).run
231
+ end
232
+
233
+ sig do
234
+ params(
235
+ uri: String,
236
+ range: Document::RangeShape,
237
+ context: T::Hash[Symbol, T.untyped],
238
+ ).returns(T.nilable(T::Array[Interface::CodeAction]))
239
+ end
240
+ def code_action(uri, range, context)
241
+ document = @store.get(uri)
242
+
243
+ Requests::CodeActions.new(uri, document, range, context).run
244
+ end
245
+
246
+ sig { params(params: T::Hash[Symbol, T.untyped]).returns(Interface::CodeAction) }
247
+ def code_action_resolve(params)
248
+ uri = params.dig(:data, :uri)
249
+ document = @store.get(uri)
250
+ result = Requests::CodeActionResolve.new(document, params).run
251
+
252
+ case result
253
+ when Requests::CodeActionResolve::Error::EmptySelection
254
+ @notifications << Notification.new(
255
+ message: "window/showMessage",
256
+ params: Interface::ShowMessageParams.new(
257
+ type: Constant::MessageType::ERROR,
258
+ message: "Invalid selection for Extract Variable refactor",
259
+ ),
260
+ )
261
+ raise Requests::CodeActionResolve::CodeActionError
262
+ else
263
+ result
264
+ end
265
+ end
266
+
267
+ sig { params(uri: String).returns(T.nilable(Interface::FullDocumentDiagnosticReport)) }
268
+ def diagnostic(uri)
269
+ response = @store.cache_fetch(uri, :diagnostics) do |document|
270
+ Requests::Diagnostics.new(uri, document).run
271
+ end
272
+
273
+ Interface::FullDocumentDiagnosticReport.new(kind: "full", items: response.map(&:to_lsp_diagnostic)) if response
274
+ end
275
+
276
+ sig { params(uri: String, range: Document::RangeShape).returns(Interface::SemanticTokens) }
277
+ def semantic_tokens_range(uri, range)
278
+ document = @store.get(uri)
279
+ start_line = range.dig(:start, :line)
280
+ end_line = range.dig(:end, :line)
281
+
282
+ T.cast(
283
+ Requests::SemanticHighlighting.new(
284
+ document,
285
+ range: start_line..end_line,
286
+ encoder: Requests::Support::SemanticTokenEncoder.new,
287
+ ).run,
288
+ Interface::SemanticTokens,
289
+ )
290
+ end
291
+
292
+ sig do
293
+ params(uri: String, position: Document::PositionShape).returns(T.nilable(T::Array[Interface::CompletionItem]))
294
+ end
295
+ def completion(uri, position)
296
+ Requests::PathCompletion.new(@store.get(uri), position).run
297
+ end
298
+
299
+ sig { params(options: T::Hash[Symbol, T.untyped]).returns(Interface::InitializeResult) }
300
+ def initialize_request(options)
301
+ @store.clear
302
+ @store.encoding = options.dig(:capabilities, :general, :positionEncodings)
303
+ enabled_features = options.dig(:initializationOptions, :enabledFeatures) || []
304
+
305
+ document_symbol_provider = if enabled_features.include?("documentSymbols")
306
+ Interface::DocumentSymbolClientCapabilities.new(
307
+ hierarchical_document_symbol_support: true,
308
+ symbol_kind: {
309
+ value_set: Requests::DocumentSymbol::SYMBOL_KIND.values,
310
+ },
311
+ )
312
+ end
313
+
314
+ document_link_provider = if enabled_features.include?("documentLink")
315
+ Interface::DocumentLinkOptions.new(resolve_provider: false)
316
+ end
317
+
318
+ hover_provider = if enabled_features.include?("hover")
319
+ Interface::HoverClientCapabilities.new(dynamic_registration: false)
320
+ end
321
+
322
+ folding_ranges_provider = if enabled_features.include?("foldingRanges")
323
+ Interface::FoldingRangeClientCapabilities.new(line_folding_only: true)
324
+ end
325
+
326
+ semantic_tokens_provider = if enabled_features.include?("semanticHighlighting")
327
+ Interface::SemanticTokensRegistrationOptions.new(
328
+ document_selector: { scheme: "file", language: "ruby" },
329
+ legend: Interface::SemanticTokensLegend.new(
330
+ token_types: Requests::SemanticHighlighting::TOKEN_TYPES.keys,
331
+ token_modifiers: Requests::SemanticHighlighting::TOKEN_MODIFIERS.keys,
332
+ ),
333
+ range: true,
334
+ full: { delta: false },
335
+ )
336
+ end
337
+
338
+ diagnostics_provider = if enabled_features.include?("diagnostics")
339
+ {
340
+ interFileDependencies: false,
341
+ workspaceDiagnostics: false,
342
+ }
343
+ end
344
+
345
+ on_type_formatting_provider = if enabled_features.include?("onTypeFormatting")
346
+ Interface::DocumentOnTypeFormattingOptions.new(
347
+ first_trigger_character: "{",
348
+ more_trigger_character: ["\n", "|"],
349
+ )
350
+ end
351
+
352
+ code_action_provider = if enabled_features.include?("codeActions")
353
+ Interface::CodeActionOptions.new(resolve_provider: true)
354
+ end
355
+
356
+ inlay_hint_provider = if enabled_features.include?("inlayHint")
357
+ Interface::InlayHintOptions.new(resolve_provider: false)
358
+ end
359
+
360
+ completion_provider = if enabled_features.include?("completion")
361
+ Interface::CompletionOptions.new(
362
+ resolve_provider: false,
363
+ trigger_characters: ["/"],
364
+ )
365
+ end
366
+
367
+ Interface::InitializeResult.new(
368
+ capabilities: Interface::ServerCapabilities.new(
369
+ text_document_sync: Interface::TextDocumentSyncOptions.new(
370
+ change: Constant::TextDocumentSyncKind::INCREMENTAL,
371
+ open_close: true,
372
+ ),
373
+ selection_range_provider: enabled_features.include?("selectionRanges"),
374
+ hover_provider: hover_provider,
375
+ document_symbol_provider: document_symbol_provider,
376
+ document_link_provider: document_link_provider,
377
+ folding_range_provider: folding_ranges_provider,
378
+ semantic_tokens_provider: semantic_tokens_provider,
379
+ document_formatting_provider: enabled_features.include?("formatting"),
380
+ document_highlight_provider: enabled_features.include?("documentHighlights"),
381
+ code_action_provider: code_action_provider,
382
+ document_on_type_formatting_provider: on_type_formatting_provider,
383
+ diagnostic_provider: diagnostics_provider,
384
+ inlay_hint_provider: inlay_hint_provider,
385
+ completion_provider: completion_provider,
386
+ ),
387
+ )
388
+ end
389
+ end
390
+ end
@@ -4,6 +4,11 @@
4
4
  require "sorbet-runtime"
5
5
  require "syntax_tree"
6
6
  require "language_server-protocol"
7
+ require "benchmark"
7
8
 
8
9
  require "ruby-lsp"
9
- require "ruby_lsp/handler"
10
+ require "ruby_lsp/utils"
11
+ require "ruby_lsp/server"
12
+ require "ruby_lsp/executor"
13
+ require "ruby_lsp/requests"
14
+ require "ruby_lsp/store"
@@ -10,8 +10,12 @@ module RubyLsp
10
10
 
11
11
  abstract!
12
12
 
13
- sig { params(document: Document).void }
14
- def initialize(document)
13
+ # We must accept rest keyword arguments here, so that the argument count matches when
14
+ # SyntaxTree::WithScope#initialize invokes `super` for Sorbet. We don't actually use these parameters for
15
+ # anything. We can remove these arguments once we drop support for Ruby 2.7
16
+ # https://github.com/ruby-syntax-tree/syntax_tree/blob/4dac90b53df388f726dce50ce638a1ba71cc59f8/lib/syntax_tree/with_scope.rb#L122
17
+ sig { params(document: Document, _kwargs: T.untyped).void }
18
+ def initialize(document, **_kwargs)
15
19
  @document = document
16
20
 
17
21
  # Parsing the document here means we're taking a lazy approach by only doing it when the first feature request
@@ -62,9 +66,10 @@ module RubyLsp
62
66
  params(
63
67
  node: SyntaxTree::Node,
64
68
  position: Integer,
69
+ node_types: T::Array[T.class_of(SyntaxTree::Node)],
65
70
  ).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node)])
66
71
  end
67
- def locate(node, position)
72
+ def locate(node, position, node_types: [])
68
73
  queue = T.let(node.child_nodes.compact, T::Array[T.nilable(SyntaxTree::Node)])
69
74
  closest = node
70
75
 
@@ -90,6 +95,8 @@ module RubyLsp
90
95
  parent = T.let(closest, SyntaxTree::Node)
91
96
  closest = candidate
92
97
  end
98
+
99
+ break if node_types.any? { |type| candidate.is_a?(type) }
93
100
  end
94
101
 
95
102
  [closest, parent]
@@ -0,0 +1,100 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # ![Code action resolve demo](../../misc/code_action_resolve.gif)
7
+ #
8
+ # The [code action resolve](https://microsoft.github.io/language-server-protocol/specification#codeAction_resolve)
9
+ # request is used to to resolve the edit field for a given code action, if it is not already provided in the
10
+ # textDocument/codeAction response. We can use it for scenarios that require more computation such as refactoring.
11
+ #
12
+ # # Example: Extract to variable
13
+ #
14
+ # ```ruby
15
+ # # Before:
16
+ # 1 + 1 # Select the text and use Refactor: Extract Variable
17
+ #
18
+ # # After:
19
+ # new_variable = 1 + 1
20
+ # new_variable
21
+ #
22
+ # ```
23
+ #
24
+ class CodeActionResolve < BaseRequest
25
+ extend T::Sig
26
+ NEW_VARIABLE_NAME = "new_variable"
27
+
28
+ class CodeActionError < StandardError; end
29
+
30
+ class Error < ::T::Enum
31
+ enums do
32
+ EmptySelection = new
33
+ end
34
+ end
35
+
36
+ sig { params(document: Document, code_action: T::Hash[Symbol, T.untyped]).void }
37
+ def initialize(document, code_action)
38
+ super(document)
39
+
40
+ @code_action = code_action
41
+ end
42
+
43
+ sig { override.returns(T.any(Interface::CodeAction, Error)) }
44
+ def run
45
+ source_range = @code_action.dig(:data, :range)
46
+ return Error::EmptySelection if source_range[:start] == source_range[:end]
47
+
48
+ scanner = @document.create_scanner
49
+ start_index = scanner.find_char_position(source_range[:start])
50
+ end_index = scanner.find_char_position(source_range[:end])
51
+ extraction_source = T.must(@document.source[start_index...end_index])
52
+ source_line_indentation = T.must(T.must(@document.source.lines[source_range.dig(:start, :line)])[/\A */]).size
53
+
54
+ Interface::CodeAction.new(
55
+ title: "Refactor: Extract Variable",
56
+ edit: Interface::WorkspaceEdit.new(
57
+ document_changes: [
58
+ Interface::TextDocumentEdit.new(
59
+ text_document: Interface::OptionalVersionedTextDocumentIdentifier.new(
60
+ uri: @code_action.dig(:data, :uri),
61
+ version: nil,
62
+ ),
63
+ edits: edits_to_extract_variable(source_range, extraction_source, source_line_indentation),
64
+ ),
65
+ ],
66
+ ),
67
+ )
68
+ end
69
+
70
+ private
71
+
72
+ sig do
73
+ params(range: Document::RangeShape, source: String, indentation: Integer)
74
+ .returns(T::Array[Interface::TextEdit])
75
+ end
76
+ def edits_to_extract_variable(range, source, indentation)
77
+ target_range = {
78
+ start: { line: range.dig(:start, :line), character: indentation },
79
+ end: { line: range.dig(:start, :line), character: indentation },
80
+ }
81
+
82
+ [
83
+ create_text_edit(range, NEW_VARIABLE_NAME),
84
+ create_text_edit(target_range, "#{NEW_VARIABLE_NAME} = #{source}\n#{" " * indentation}"),
85
+ ]
86
+ end
87
+
88
+ sig { params(range: Document::RangeShape, new_text: String).returns(Interface::TextEdit) }
89
+ def create_text_edit(range, new_text)
90
+ Interface::TextEdit.new(
91
+ range: Interface::Range.new(
92
+ start: Interface::Position.new(line: range.dig(:start, :line), character: range.dig(:start, :character)),
93
+ end: Interface::Position.new(line: range.dig(:end, :line), character: range.dig(:end, :character)),
94
+ ),
95
+ new_text: new_text,
96
+ )
97
+ end
98
+ end
99
+ end
100
+ end