ruby-lsp 0.4.0 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +22 -23
- data/VERSION +1 -1
- data/exe/ruby-lsp +12 -0
- data/lib/ruby_lsp/document.rb +5 -3
- data/lib/ruby_lsp/executor.rb +82 -18
- data/lib/ruby_lsp/internal.rb +1 -0
- data/lib/ruby_lsp/requests/base_request.rb +11 -2
- data/lib/ruby_lsp/requests/code_action_resolve.rb +121 -0
- data/lib/ruby_lsp/requests/code_actions.rb +20 -4
- data/lib/ruby_lsp/requests/diagnostics.rb +1 -1
- data/lib/ruby_lsp/requests/document_symbol.rb +1 -1
- data/lib/ruby_lsp/requests/folding_ranges.rb +25 -2
- data/lib/ruby_lsp/requests/formatting.rb +18 -4
- data/lib/ruby_lsp/requests/path_completion.rb +20 -13
- data/lib/ruby_lsp/requests/semantic_highlighting.rb +56 -6
- data/lib/ruby_lsp/requests.rb +2 -0
- data/lib/ruby_lsp/server.rb +2 -1
- data/lib/ruby_lsp/store.rb +4 -0
- data/lib/ruby_lsp/utils.rb +3 -0
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c5d9c6b487aab55ce25eb2741fff92afa8ca0ee544f21374debacce7f887cc50
|
4
|
+
data.tar.gz: 8da9da85900753306f5e8558bc1a42f25b7173ffe1087ea70612c10ef5558e27
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3dca630b9cae262f1af8d0aef0300ab7399dafd348db5f578ed7fdffb22a7afd48fe9467aa65e467fb50e16663f8fad32f1d0cc46b30819b06a6b7472700dcc0
|
7
|
+
data.tar.gz: f048c6c9fe14d20edefeca7ac504942af34c61699163f0d09ec54f4d80e7469e1bb1762757b96aa3d8d06a6e8168473dacfc932112b2d31ca4b380748eef8899
|
data/README.md
CHANGED
@@ -45,8 +45,9 @@ group :development do
|
|
45
45
|
end
|
46
46
|
```
|
47
47
|
|
48
|
-
If using VS Code, install the [Ruby LSP extension](https://github.com/Shopify/vscode-ruby-lsp) to get the extra features
|
49
|
-
the editor.
|
48
|
+
If using VS Code, install the [Ruby LSP extension](https://github.com/Shopify/vscode-ruby-lsp) to get the extra features
|
49
|
+
in the editor. See [editors](https://github.com/Shopify/ruby-lsp/blob/main/EDITORS.md) for community instructions on
|
50
|
+
setting up the Ruby LSP in different editors.
|
50
51
|
|
51
52
|
See the [documentation](https://shopify.github.io/ruby-lsp) for more in-depth details about the
|
52
53
|
[supported features](https://shopify.github.io/ruby-lsp/RubyLsp/Requests.html).
|
@@ -114,27 +115,25 @@ To add a new expectations test runner for a new request handler:
|
|
114
115
|
|
115
116
|
## Debugging
|
116
117
|
|
117
|
-
###
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
1. When the breakpoint is triggered, the process will pause and VS Code will connect to the debugger and activate the debugger UI.
|
137
|
-
1. Open the Debug Console view to use the debugger's REPL.
|
118
|
+
### Debugging Tests
|
119
|
+
|
120
|
+
1. Open the test file.
|
121
|
+
2. Set a breakpoint(s) on lines by clicking next to their numbers.
|
122
|
+
3. Open VS Code's `Run and Debug` panel.
|
123
|
+
4. At the top of the panel, select `Minitset - current file` and click the green triangle (or press F5).
|
124
|
+
|
125
|
+
### Debugging Running Ruby LSP Process
|
126
|
+
|
127
|
+
1. Open the `vscode-ruby-lsp` project in VS Code.
|
128
|
+
2. [`vscode-ruby-lsp`] Open VS Code's `Run and Debug` panel.
|
129
|
+
3. [`vscode-ruby-lsp`] Select `Run Extension` and click the green triangle (or press F5).
|
130
|
+
4. [`vscode-ruby-lsp`] Now VS Code will:
|
131
|
+
- Open another workspace as the `Extension Development Host`.
|
132
|
+
- Run `vscode-ruby-lsp` extension in debug mode, which will start a new `ruby-lsp` process with the `--debug` flag.
|
133
|
+
5. Open `ruby-lsp` in VS Code.
|
134
|
+
6. [`ruby-lsp`] Run `bin/rdbg -A` to connect to the running `ruby-lsp` process.
|
135
|
+
7. [`ruby-lsp`] Use commands like `b <file>:<line>` or `b Class#method` to set breakpoints and type `c` to continue the process.
|
136
|
+
8. In your `Extension Development Host` project (e.g. [`Tapioca`](https://github.com/Shopify/tapioca)), trigger the request that will hit the breakpoint.
|
138
137
|
|
139
138
|
## Spell Checking
|
140
139
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.4.
|
1
|
+
0.4.2
|
data/exe/ruby-lsp
CHANGED
@@ -19,4 +19,16 @@ rescue
|
|
19
19
|
end
|
20
20
|
|
21
21
|
require_relative "../lib/ruby_lsp/internal"
|
22
|
+
|
23
|
+
if ARGV.include?("--debug")
|
24
|
+
sockets_dir = "/tmp/ruby-lsp-debug-sockets"
|
25
|
+
Dir.mkdir(sockets_dir) unless Dir.exist?(sockets_dir)
|
26
|
+
# ruby-debug-ENV["USER"] is an implicit naming pattern in ruby/debug
|
27
|
+
# if it's not present, rdbg will not find the socket
|
28
|
+
socket_identifier = "ruby-debug-#{ENV["USER"]}-#{File.basename(Dir.pwd)}.sock"
|
29
|
+
ENV["RUBY_DEBUG_SOCK_PATH"] = "#{sockets_dir}/#{socket_identifier}"
|
30
|
+
|
31
|
+
require "debug/open_nonstop"
|
32
|
+
end
|
33
|
+
|
22
34
|
RubyLsp::Server.new.start
|
data/lib/ruby_lsp/document.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
75
|
+
@syntax_error = true
|
74
76
|
end
|
75
77
|
|
76
78
|
sig { returns(T::Boolean) }
|
77
79
|
def syntax_error?
|
78
|
-
@
|
80
|
+
@syntax_error
|
79
81
|
end
|
80
82
|
|
81
83
|
sig { returns(T::Boolean) }
|
data/lib/ruby_lsp/executor.rb
CHANGED
@@ -37,6 +37,9 @@ module RubyLsp
|
|
37
37
|
case request[:method]
|
38
38
|
when "initialize"
|
39
39
|
initialize_request(request.dig(:params))
|
40
|
+
when "initialized"
|
41
|
+
warn("Ruby LSP is ready")
|
42
|
+
VOID
|
40
43
|
when "textDocument/didOpen"
|
41
44
|
text_document_did_open(uri, request.dig(:params, :textDocument, :text))
|
42
45
|
when "textDocument/didClose"
|
@@ -63,6 +66,16 @@ module RubyLsp
|
|
63
66
|
when "textDocument/formatting"
|
64
67
|
begin
|
65
68
|
formatting(uri)
|
69
|
+
rescue Requests::Formatting::InvalidFormatter => error
|
70
|
+
@notifications << Notification.new(
|
71
|
+
message: "window/showMessage",
|
72
|
+
params: Interface::ShowMessageParams.new(
|
73
|
+
type: Constant::MessageType::ERROR,
|
74
|
+
message: "Configuration error: #{error.message}",
|
75
|
+
),
|
76
|
+
)
|
77
|
+
|
78
|
+
nil
|
66
79
|
rescue StandardError => error
|
67
80
|
@notifications << Notification.new(
|
68
81
|
message: "window/showMessage",
|
@@ -84,6 +97,8 @@ module RubyLsp
|
|
84
97
|
inlay_hint(uri, request.dig(:params, :range))
|
85
98
|
when "textDocument/codeAction"
|
86
99
|
code_action(uri, request.dig(:params, :range), request.dig(:params, :context))
|
100
|
+
when "codeAction/resolve"
|
101
|
+
code_action_resolve(request.dig(:params))
|
87
102
|
when "textDocument/diagnostic"
|
88
103
|
begin
|
89
104
|
diagnostic(uri)
|
@@ -192,7 +207,7 @@ module RubyLsp
|
|
192
207
|
|
193
208
|
sig { params(uri: String).returns(T.nilable(T::Array[Interface::TextEdit])) }
|
194
209
|
def formatting(uri)
|
195
|
-
Requests::Formatting.new(uri, @store.get(uri)).run
|
210
|
+
Requests::Formatting.new(uri, @store.get(uri), formatter: @store.formatter).run
|
196
211
|
end
|
197
212
|
|
198
213
|
sig do
|
@@ -233,11 +248,39 @@ module RubyLsp
|
|
233
248
|
).returns(T.nilable(T::Array[Interface::CodeAction]))
|
234
249
|
end
|
235
250
|
def code_action(uri, range, context)
|
236
|
-
start_line = range.dig(:start, :line)
|
237
|
-
end_line = range.dig(:end, :line)
|
238
251
|
document = @store.get(uri)
|
239
252
|
|
240
|
-
Requests::CodeActions.new(uri, document,
|
253
|
+
Requests::CodeActions.new(uri, document, range, context).run
|
254
|
+
end
|
255
|
+
|
256
|
+
sig { params(params: T::Hash[Symbol, T.untyped]).returns(Interface::CodeAction) }
|
257
|
+
def code_action_resolve(params)
|
258
|
+
uri = params.dig(:data, :uri)
|
259
|
+
document = @store.get(uri)
|
260
|
+
result = Requests::CodeActionResolve.new(document, params).run
|
261
|
+
|
262
|
+
case result
|
263
|
+
when Requests::CodeActionResolve::Error::EmptySelection
|
264
|
+
@notifications << Notification.new(
|
265
|
+
message: "window/showMessage",
|
266
|
+
params: Interface::ShowMessageParams.new(
|
267
|
+
type: Constant::MessageType::ERROR,
|
268
|
+
message: "Invalid selection for Extract Variable refactor",
|
269
|
+
),
|
270
|
+
)
|
271
|
+
raise Requests::CodeActionResolve::CodeActionError
|
272
|
+
when Requests::CodeActionResolve::Error::InvalidTargetRange
|
273
|
+
@notifications << Notification.new(
|
274
|
+
message: "window/showMessage",
|
275
|
+
params: Interface::ShowMessageParams.new(
|
276
|
+
type: Constant::MessageType::ERROR,
|
277
|
+
message: "Couldn't find an appropriate location to place extracted refactor",
|
278
|
+
),
|
279
|
+
)
|
280
|
+
raise Requests::CodeActionResolve::CodeActionError
|
281
|
+
else
|
282
|
+
result
|
283
|
+
end
|
241
284
|
end
|
242
285
|
|
243
286
|
sig { params(uri: String).returns(T.nilable(Interface::FullDocumentDiagnosticReport)) }
|
@@ -276,9 +319,26 @@ module RubyLsp
|
|
276
319
|
def initialize_request(options)
|
277
320
|
@store.clear
|
278
321
|
@store.encoding = options.dig(:capabilities, :general, :positionEncodings)
|
279
|
-
|
322
|
+
formatter = options.dig(:initializationOptions, :formatter)
|
323
|
+
@store.formatter = formatter unless formatter.nil?
|
324
|
+
|
325
|
+
configured_features = options.dig(:initializationOptions, :enabledFeatures)
|
326
|
+
|
327
|
+
enabled_features = case configured_features
|
328
|
+
when Array
|
329
|
+
# If the configuration is using an array, then absent features are disabled and present ones are enabled. That's
|
330
|
+
# why we use `false` as the default value
|
331
|
+
Hash.new(false).merge!(configured_features.to_h { |feature| [feature, true] })
|
332
|
+
when Hash
|
333
|
+
# If the configuration is already a hash, merge it with a default value of `true`. That way clients don't have
|
334
|
+
# to opt-in to every single feature
|
335
|
+
Hash.new(true).merge!(configured_features)
|
336
|
+
else
|
337
|
+
# If no configuration was passed by the client, just enable every feature
|
338
|
+
Hash.new(true)
|
339
|
+
end
|
280
340
|
|
281
|
-
document_symbol_provider = if enabled_features
|
341
|
+
document_symbol_provider = if enabled_features["documentSymbols"]
|
282
342
|
Interface::DocumentSymbolClientCapabilities.new(
|
283
343
|
hierarchical_document_symbol_support: true,
|
284
344
|
symbol_kind: {
|
@@ -287,19 +347,19 @@ module RubyLsp
|
|
287
347
|
)
|
288
348
|
end
|
289
349
|
|
290
|
-
document_link_provider = if enabled_features
|
350
|
+
document_link_provider = if enabled_features["documentLink"]
|
291
351
|
Interface::DocumentLinkOptions.new(resolve_provider: false)
|
292
352
|
end
|
293
353
|
|
294
|
-
hover_provider = if enabled_features
|
354
|
+
hover_provider = if enabled_features["hover"]
|
295
355
|
Interface::HoverClientCapabilities.new(dynamic_registration: false)
|
296
356
|
end
|
297
357
|
|
298
|
-
folding_ranges_provider = if enabled_features
|
358
|
+
folding_ranges_provider = if enabled_features["foldingRanges"]
|
299
359
|
Interface::FoldingRangeClientCapabilities.new(line_folding_only: true)
|
300
360
|
end
|
301
361
|
|
302
|
-
semantic_tokens_provider = if enabled_features
|
362
|
+
semantic_tokens_provider = if enabled_features["semanticHighlighting"]
|
303
363
|
Interface::SemanticTokensRegistrationOptions.new(
|
304
364
|
document_selector: { scheme: "file", language: "ruby" },
|
305
365
|
legend: Interface::SemanticTokensLegend.new(
|
@@ -311,25 +371,29 @@ module RubyLsp
|
|
311
371
|
)
|
312
372
|
end
|
313
373
|
|
314
|
-
diagnostics_provider = if enabled_features
|
374
|
+
diagnostics_provider = if enabled_features["diagnostics"]
|
315
375
|
{
|
316
376
|
interFileDependencies: false,
|
317
377
|
workspaceDiagnostics: false,
|
318
378
|
}
|
319
379
|
end
|
320
380
|
|
321
|
-
on_type_formatting_provider = if enabled_features
|
381
|
+
on_type_formatting_provider = if enabled_features["onTypeFormatting"]
|
322
382
|
Interface::DocumentOnTypeFormattingOptions.new(
|
323
383
|
first_trigger_character: "{",
|
324
384
|
more_trigger_character: ["\n", "|"],
|
325
385
|
)
|
326
386
|
end
|
327
387
|
|
328
|
-
|
388
|
+
code_action_provider = if enabled_features["codeActions"]
|
389
|
+
Interface::CodeActionOptions.new(resolve_provider: true)
|
390
|
+
end
|
391
|
+
|
392
|
+
inlay_hint_provider = if enabled_features["inlayHint"]
|
329
393
|
Interface::InlayHintOptions.new(resolve_provider: false)
|
330
394
|
end
|
331
395
|
|
332
|
-
completion_provider = if enabled_features
|
396
|
+
completion_provider = if enabled_features["completion"]
|
333
397
|
Interface::CompletionOptions.new(
|
334
398
|
resolve_provider: false,
|
335
399
|
trigger_characters: ["/"],
|
@@ -342,15 +406,15 @@ module RubyLsp
|
|
342
406
|
change: Constant::TextDocumentSyncKind::INCREMENTAL,
|
343
407
|
open_close: true,
|
344
408
|
),
|
345
|
-
selection_range_provider: enabled_features
|
409
|
+
selection_range_provider: enabled_features["selectionRanges"],
|
346
410
|
hover_provider: hover_provider,
|
347
411
|
document_symbol_provider: document_symbol_provider,
|
348
412
|
document_link_provider: document_link_provider,
|
349
413
|
folding_range_provider: folding_ranges_provider,
|
350
414
|
semantic_tokens_provider: semantic_tokens_provider,
|
351
|
-
document_formatting_provider: enabled_features
|
352
|
-
document_highlight_provider: enabled_features
|
353
|
-
code_action_provider:
|
415
|
+
document_formatting_provider: enabled_features["formatting"] && formatter != "none",
|
416
|
+
document_highlight_provider: enabled_features["documentHighlights"],
|
417
|
+
code_action_provider: code_action_provider,
|
354
418
|
document_on_type_formatting_provider: on_type_formatting_provider,
|
355
419
|
diagnostic_provider: diagnostics_provider,
|
356
420
|
inlay_hint_provider: inlay_hint_provider,
|
data/lib/ruby_lsp/internal.rb
CHANGED
@@ -28,6 +28,14 @@ module RubyLsp
|
|
28
28
|
sig { abstract.returns(Object) }
|
29
29
|
def run; end
|
30
30
|
|
31
|
+
# Syntax Tree implements `visit_all` using `map` instead of `each` for users who want to use the pattern
|
32
|
+
# `result = visitor.visit(tree)`. However, we don't use that pattern and should avoid producing a new array for
|
33
|
+
# every single node visited
|
34
|
+
sig { params(nodes: T::Array[SyntaxTree::Node]).void }
|
35
|
+
def visit_all(nodes)
|
36
|
+
nodes.each { |node| visit(node) }
|
37
|
+
end
|
38
|
+
|
31
39
|
sig { params(node: SyntaxTree::Node).returns(LanguageServer::Protocol::Interface::Range) }
|
32
40
|
def range_from_syntax_tree_node(node)
|
33
41
|
loc = node.location
|
@@ -89,14 +97,15 @@ module RubyLsp
|
|
89
97
|
# If the node's start character is already past the position, then we should've found the closest node already
|
90
98
|
break if position < loc.start_char
|
91
99
|
|
100
|
+
# If there are node types to filter by, and the current node is not one of those types, then skip it
|
101
|
+
next if node_types.any? && node_types.none? { |type| candidate.is_a?(type) }
|
102
|
+
|
92
103
|
# If the current node is narrower than or equal to the previous closest node, then it is more precise
|
93
104
|
closest_loc = closest.location
|
94
105
|
if loc.end_char - loc.start_char <= closest_loc.end_char - closest_loc.start_char
|
95
106
|
parent = T.let(closest, SyntaxTree::Node)
|
96
107
|
closest = candidate
|
97
108
|
end
|
98
|
-
|
99
|
-
break if node_types.any? { |type| candidate.is_a?(type) }
|
100
109
|
end
|
101
110
|
|
102
111
|
[closest, parent]
|
@@ -0,0 +1,121 @@
|
|
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
|
+
InvalidTargetRange = new
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
sig { params(document: Document, code_action: T::Hash[Symbol, T.untyped]).void }
|
38
|
+
def initialize(document, code_action)
|
39
|
+
super(document)
|
40
|
+
|
41
|
+
@code_action = code_action
|
42
|
+
end
|
43
|
+
|
44
|
+
sig { override.returns(T.any(Interface::CodeAction, Error)) }
|
45
|
+
def run
|
46
|
+
source_range = @code_action.dig(:data, :range)
|
47
|
+
return Error::EmptySelection if source_range[:start] == source_range[:end]
|
48
|
+
|
49
|
+
return Error::InvalidTargetRange if @document.syntax_error?
|
50
|
+
|
51
|
+
scanner = @document.create_scanner
|
52
|
+
start_index = scanner.find_char_position(source_range[:start])
|
53
|
+
end_index = scanner.find_char_position(source_range[:end])
|
54
|
+
extracted_source = T.must(@document.source[start_index...end_index])
|
55
|
+
|
56
|
+
# Find the closest statements node, so that we place the refactor in a valid position
|
57
|
+
closest_statements = locate(T.must(@document.tree), start_index, node_types: [SyntaxTree::Statements]).first
|
58
|
+
return Error::InvalidTargetRange if closest_statements.nil?
|
59
|
+
|
60
|
+
# Find the node with the end line closest to the requested position, so that we can place the refactor
|
61
|
+
# immediately after that closest node
|
62
|
+
closest_node = closest_statements.child_nodes.compact.min_by do |node|
|
63
|
+
distance = source_range.dig(:start, :line) - (node.location.end_line - 1)
|
64
|
+
distance <= 0 ? Float::INFINITY : distance
|
65
|
+
end
|
66
|
+
|
67
|
+
# When trying to extract the first node inside of a statements block, then we can just select one line above it
|
68
|
+
target_line = if closest_node == closest_statements.child_nodes.first
|
69
|
+
closest_node.location.start_line - 1
|
70
|
+
else
|
71
|
+
closest_node.location.end_line
|
72
|
+
end
|
73
|
+
|
74
|
+
lines = @document.source.lines
|
75
|
+
indentation = T.must(T.must(lines[target_line - 1])[/\A */]).size
|
76
|
+
|
77
|
+
target_range = {
|
78
|
+
start: { line: target_line, character: indentation },
|
79
|
+
end: { line: target_line, character: indentation },
|
80
|
+
}
|
81
|
+
|
82
|
+
variable_source = if T.must(lines[target_line]).strip.empty?
|
83
|
+
"\n#{" " * indentation}#{NEW_VARIABLE_NAME} = #{extracted_source}"
|
84
|
+
else
|
85
|
+
"#{NEW_VARIABLE_NAME} = #{extracted_source}\n#{" " * indentation}"
|
86
|
+
end
|
87
|
+
|
88
|
+
Interface::CodeAction.new(
|
89
|
+
title: "Refactor: Extract Variable",
|
90
|
+
edit: Interface::WorkspaceEdit.new(
|
91
|
+
document_changes: [
|
92
|
+
Interface::TextDocumentEdit.new(
|
93
|
+
text_document: Interface::OptionalVersionedTextDocumentIdentifier.new(
|
94
|
+
uri: @code_action.dig(:data, :uri),
|
95
|
+
version: nil,
|
96
|
+
),
|
97
|
+
edits: [
|
98
|
+
create_text_edit(source_range, NEW_VARIABLE_NAME),
|
99
|
+
create_text_edit(target_range, variable_source),
|
100
|
+
],
|
101
|
+
),
|
102
|
+
],
|
103
|
+
),
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
sig { params(range: Document::RangeShape, new_text: String).returns(Interface::TextEdit) }
|
110
|
+
def create_text_edit(range, new_text)
|
111
|
+
Interface::TextEdit.new(
|
112
|
+
range: Interface::Range.new(
|
113
|
+
start: Interface::Position.new(line: range.dig(:start, :line), character: range.dig(:start, :character)),
|
114
|
+
end: Interface::Position.new(line: range.dig(:end, :line), character: range.dig(:end, :character)),
|
115
|
+
),
|
116
|
+
new_text: new_text,
|
117
|
+
)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -23,7 +23,7 @@ module RubyLsp
|
|
23
23
|
params(
|
24
24
|
uri: String,
|
25
25
|
document: Document,
|
26
|
-
range:
|
26
|
+
range: Document::RangeShape,
|
27
27
|
context: T::Hash[Symbol, T.untyped],
|
28
28
|
).void
|
29
29
|
end
|
@@ -38,9 +38,8 @@ module RubyLsp
|
|
38
38
|
sig { override.returns(T.nilable(T.all(T::Array[Interface::CodeAction], Object))) }
|
39
39
|
def run
|
40
40
|
diagnostics = @context[:diagnostics]
|
41
|
-
return if diagnostics.nil? || diagnostics.empty?
|
42
41
|
|
43
|
-
diagnostics.filter_map do |diagnostic|
|
42
|
+
code_actions = diagnostics.filter_map do |diagnostic|
|
44
43
|
code_action = diagnostic.dig(:data, :code_action)
|
45
44
|
next if code_action.nil?
|
46
45
|
|
@@ -49,13 +48,30 @@ module RubyLsp
|
|
49
48
|
range = code_action.dig(:edit, :documentChanges, 0, :edits, 0, :range)
|
50
49
|
code_action if diagnostic.dig(:data, :correctable) && cover?(range)
|
51
50
|
end
|
51
|
+
|
52
|
+
code_actions << refactor_code_action(@range, @uri)
|
52
53
|
end
|
53
54
|
|
54
55
|
private
|
55
56
|
|
56
57
|
sig { params(range: T.nilable(Document::RangeShape)).returns(T::Boolean) }
|
57
58
|
def cover?(range)
|
58
|
-
range.nil? ||
|
59
|
+
range.nil? ||
|
60
|
+
((@range.dig(:start, :line))..(@range.dig(:end, :line))).cover?(
|
61
|
+
(range.dig(:start, :line))..(range.dig(:end, :line)),
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
sig { params(range: Document::RangeShape, uri: String).returns(Interface::CodeAction) }
|
66
|
+
def refactor_code_action(range, uri)
|
67
|
+
Interface::CodeAction.new(
|
68
|
+
title: "Refactor: Extract Variable",
|
69
|
+
kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
|
70
|
+
data: {
|
71
|
+
range: range,
|
72
|
+
uri: uri,
|
73
|
+
},
|
74
|
+
)
|
59
75
|
end
|
60
76
|
end
|
61
77
|
end
|
@@ -34,7 +34,7 @@ module RubyLsp
|
|
34
34
|
return unless defined?(Support::RuboCopDiagnosticsRunner)
|
35
35
|
|
36
36
|
# Don't try to run RuboCop diagnostics for files outside the current working directory
|
37
|
-
return unless @uri.
|
37
|
+
return unless @uri.start_with?(WORKSPACE_URI)
|
38
38
|
|
39
39
|
Support::RuboCopDiagnosticsRunner.instance.run(@uri, @document)
|
40
40
|
end
|
@@ -108,7 +108,7 @@ module RubyLsp
|
|
108
108
|
|
109
109
|
sig { override.params(node: SyntaxTree::Command).void }
|
110
110
|
def visit_command(node)
|
111
|
-
return unless ATTR_ACCESSORS.include?(node.message.value)
|
111
|
+
return visit(node.arguments) unless ATTR_ACCESSORS.include?(node.message.value)
|
112
112
|
|
113
113
|
node.arguments.parts.each do |argument|
|
114
114
|
next unless argument.is_a?(SyntaxTree::SymbolLiteral)
|
@@ -24,7 +24,6 @@ module RubyLsp
|
|
24
24
|
SyntaxTree::BlockNode,
|
25
25
|
SyntaxTree::Case,
|
26
26
|
SyntaxTree::ClassDeclaration,
|
27
|
-
SyntaxTree::Command,
|
28
27
|
SyntaxTree::For,
|
29
28
|
SyntaxTree::HashLiteral,
|
30
29
|
SyntaxTree::Heredoc,
|
@@ -100,6 +99,11 @@ module RubyLsp
|
|
100
99
|
add_call_range(node)
|
101
100
|
return
|
102
101
|
end
|
102
|
+
when SyntaxTree::Command
|
103
|
+
unless same_lines_for_command_and_block?(node)
|
104
|
+
location = node.location
|
105
|
+
add_lines_range(location.start_line, location.end_line - 1)
|
106
|
+
end
|
103
107
|
when SyntaxTree::DefNode
|
104
108
|
add_def_range(node)
|
105
109
|
when SyntaxTree::StringConcat
|
@@ -110,6 +114,17 @@ module RubyLsp
|
|
110
114
|
super
|
111
115
|
end
|
112
116
|
|
117
|
+
# This is to prevent duplicate ranges
|
118
|
+
sig { params(node: T.any(SyntaxTree::Command, SyntaxTree::CommandCall)).returns(T::Boolean) }
|
119
|
+
def same_lines_for_command_and_block?(node)
|
120
|
+
node_block = node.block
|
121
|
+
return false unless node_block
|
122
|
+
|
123
|
+
location = node.location
|
124
|
+
block_location = node_block.location
|
125
|
+
block_location.start_line == location.start_line && block_location.end_line == location.end_line
|
126
|
+
end
|
127
|
+
|
113
128
|
class PartialRange
|
114
129
|
extend T::Sig
|
115
130
|
|
@@ -223,9 +238,17 @@ module RubyLsp
|
|
223
238
|
end
|
224
239
|
end
|
225
240
|
|
226
|
-
|
241
|
+
if receiver
|
242
|
+
unless node.is_a?(SyntaxTree::CommandCall) && same_lines_for_command_and_block?(node)
|
243
|
+
add_lines_range(
|
244
|
+
receiver.location.start_line,
|
245
|
+
node.location.end_line - 1,
|
246
|
+
)
|
247
|
+
end
|
248
|
+
end
|
227
249
|
|
228
250
|
visit(node.arguments)
|
251
|
+
visit(node.block) if node.is_a?(SyntaxTree::CommandCall)
|
229
252
|
end
|
230
253
|
|
231
254
|
sig { params(node: SyntaxTree::DefNode).void }
|
@@ -22,14 +22,23 @@ module RubyLsp
|
|
22
22
|
# ```
|
23
23
|
class Formatting < BaseRequest
|
24
24
|
class Error < StandardError; end
|
25
|
+
class InvalidFormatter < StandardError; end
|
25
26
|
|
26
27
|
extend T::Sig
|
27
28
|
|
28
|
-
sig { params(uri: String, document: Document).void }
|
29
|
-
def initialize(uri, document)
|
29
|
+
sig { params(uri: String, document: Document, formatter: String).void }
|
30
|
+
def initialize(uri, document, formatter: "auto")
|
30
31
|
super(document)
|
31
32
|
|
32
33
|
@uri = uri
|
34
|
+
@formatter = T.let(
|
35
|
+
if formatter == "auto"
|
36
|
+
defined?(Support::RuboCopFormattingRunner) ? "rubocop" : "syntax_tree"
|
37
|
+
else
|
38
|
+
formatter
|
39
|
+
end,
|
40
|
+
String,
|
41
|
+
)
|
33
42
|
end
|
34
43
|
|
35
44
|
sig { override.returns(T.nilable(T.all(T::Array[Interface::TextEdit], Object))) }
|
@@ -37,6 +46,8 @@ module RubyLsp
|
|
37
46
|
# Don't try to format files outside the current working directory
|
38
47
|
return unless @uri.sub("file://", "").start_with?(Dir.pwd)
|
39
48
|
|
49
|
+
return if @document.syntax_error?
|
50
|
+
|
40
51
|
formatted_text = formatted_file
|
41
52
|
return unless formatted_text
|
42
53
|
|
@@ -58,10 +69,13 @@ module RubyLsp
|
|
58
69
|
|
59
70
|
sig { returns(T.nilable(String)) }
|
60
71
|
def formatted_file
|
61
|
-
|
72
|
+
case @formatter
|
73
|
+
when "rubocop"
|
62
74
|
Support::RuboCopFormattingRunner.instance.run(@uri, @document)
|
63
|
-
|
75
|
+
when "syntax_tree"
|
64
76
|
SyntaxTree.format(@document.source)
|
77
|
+
else
|
78
|
+
raise InvalidFormatter, "Unknown formatter: #{@formatter}"
|
65
79
|
end
|
66
80
|
end
|
67
81
|
end
|
@@ -21,17 +21,20 @@ module RubyLsp
|
|
21
21
|
super(document)
|
22
22
|
|
23
23
|
@tree = T.let(Support::PrefixTree.new(collect_load_path_files), Support::PrefixTree)
|
24
|
-
|
25
|
-
char_position = document.create_scanner.find_char_position(position)
|
26
|
-
@target = T.let(find(char_position), T.nilable(SyntaxTree::TStringContent))
|
24
|
+
@position = position
|
27
25
|
end
|
28
26
|
|
29
|
-
sig { override.returns(T.all(T::Array[
|
27
|
+
sig { override.returns(T.all(T::Array[Interface::CompletionItem], Object)) }
|
30
28
|
def run
|
31
|
-
#
|
32
|
-
return []
|
29
|
+
# We can't verify if we're inside a require when there are syntax errors
|
30
|
+
return [] if @document.syntax_error?
|
31
|
+
|
32
|
+
char_position = @document.create_scanner.find_char_position(@position)
|
33
|
+
target = T.let(find(char_position), T.nilable(SyntaxTree::TStringContent))
|
34
|
+
# no target means the we are not inside a `require` call
|
35
|
+
return [] unless target
|
33
36
|
|
34
|
-
text =
|
37
|
+
text = target.value
|
35
38
|
@tree.search(text).sort.map! do |path|
|
36
39
|
build_completion(path, path.delete_prefix(text))
|
37
40
|
end
|
@@ -73,14 +76,18 @@ module RubyLsp
|
|
73
76
|
end
|
74
77
|
end
|
75
78
|
|
76
|
-
sig
|
77
|
-
params(label: String, insert_text: String).returns(LanguageServer::Protocol::Interface::CompletionItem)
|
78
|
-
end
|
79
|
+
sig { params(label: String, insert_text: String).returns(Interface::CompletionItem) }
|
79
80
|
def build_completion(label, insert_text)
|
80
|
-
|
81
|
+
Interface::CompletionItem.new(
|
81
82
|
label: label,
|
82
|
-
|
83
|
-
|
83
|
+
text_edit: Interface::TextEdit.new(
|
84
|
+
range: Interface::Range.new(
|
85
|
+
start: @position,
|
86
|
+
end: @position,
|
87
|
+
),
|
88
|
+
new_text: insert_text,
|
89
|
+
),
|
90
|
+
kind: Constant::CompletionItemKind::REFERENCE,
|
84
91
|
)
|
85
92
|
end
|
86
93
|
end
|
@@ -160,6 +160,7 @@ module RubyLsp
|
|
160
160
|
add_token(node.message.location, :method)
|
161
161
|
end
|
162
162
|
visit(node.arguments)
|
163
|
+
visit(node.block)
|
163
164
|
end
|
164
165
|
|
165
166
|
sig { override.params(node: SyntaxTree::CommandCall).void }
|
@@ -169,6 +170,7 @@ module RubyLsp
|
|
169
170
|
visit(node.receiver)
|
170
171
|
add_token(node.message.location, :method)
|
171
172
|
visit(node.arguments)
|
173
|
+
visit(node.block)
|
172
174
|
end
|
173
175
|
|
174
176
|
sig { override.params(node: SyntaxTree::Const).void }
|
@@ -236,13 +238,12 @@ module RubyLsp
|
|
236
238
|
|
237
239
|
value = node.value
|
238
240
|
|
239
|
-
|
240
|
-
when SyntaxTree::Ident
|
241
|
+
if value.is_a?(SyntaxTree::Ident)
|
241
242
|
type = type_for_local(value)
|
242
243
|
add_token(value.location, type)
|
243
|
-
else
|
244
|
-
visit(value)
|
245
244
|
end
|
245
|
+
|
246
|
+
super
|
246
247
|
end
|
247
248
|
|
248
249
|
sig { override.params(node: SyntaxTree::VarRef).void }
|
@@ -260,16 +261,65 @@ module RubyLsp
|
|
260
261
|
end
|
261
262
|
end
|
262
263
|
|
264
|
+
# All block locals are variables. E.g.: [].each do |x; block_local|
|
265
|
+
sig { override.params(node: SyntaxTree::BlockVar).void }
|
266
|
+
def visit_block_var(node)
|
267
|
+
node.locals.each { |local| add_token(local.location, :variable) }
|
268
|
+
super
|
269
|
+
end
|
270
|
+
|
271
|
+
# All lambda locals are variables. E.g.: ->(x; lambda_local) {}
|
272
|
+
sig { override.params(node: SyntaxTree::LambdaVar).void }
|
273
|
+
def visit_lambda_var(node)
|
274
|
+
node.locals.each { |local| add_token(local.location, :variable) }
|
275
|
+
super
|
276
|
+
end
|
277
|
+
|
263
278
|
sig { override.params(node: SyntaxTree::VCall).void }
|
264
279
|
def visit_vcall(node)
|
265
280
|
return super unless visible?(node, @range)
|
266
281
|
|
267
|
-
|
282
|
+
# A VCall may exist as a local in the current_scope. This happens when used named capture groups in a regexp
|
283
|
+
ident = node.value
|
284
|
+
value = ident.value
|
285
|
+
local = current_scope.find_local(value)
|
286
|
+
return if local.nil? && special_method?(value)
|
287
|
+
|
288
|
+
type = if local
|
289
|
+
:variable
|
290
|
+
elsif Support::Sorbet.annotation?(node)
|
291
|
+
:type
|
292
|
+
else
|
293
|
+
:method
|
294
|
+
end
|
268
295
|
|
269
|
-
type = Support::Sorbet.annotation?(node) ? :type : :method
|
270
296
|
add_token(node.value.location, type)
|
271
297
|
end
|
272
298
|
|
299
|
+
sig { override.params(node: SyntaxTree::Binary).void }
|
300
|
+
def visit_binary(node)
|
301
|
+
# It's important to visit the regexp first in the WithScope module
|
302
|
+
super
|
303
|
+
|
304
|
+
# You can only capture local variables with regexp by using the =~ operator
|
305
|
+
return unless node.operator == :=~
|
306
|
+
|
307
|
+
left = node.left
|
308
|
+
parts = left.parts
|
309
|
+
|
310
|
+
if left.is_a?(SyntaxTree::RegexpLiteral) && parts.one? && parts.first.is_a?(SyntaxTree::TStringContent)
|
311
|
+
content = parts.first
|
312
|
+
|
313
|
+
# For each capture name we find in the regexp, look for a local in the current_scope
|
314
|
+
Regexp.new(content.value, Regexp::FIXEDENCODING).names.each do |name|
|
315
|
+
local = current_scope.find_local(name)
|
316
|
+
next unless local
|
317
|
+
|
318
|
+
local.definitions.each { |definition| add_token(definition, :variable) }
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
273
323
|
sig { override.params(node: SyntaxTree::ClassDeclaration).void }
|
274
324
|
def visit_class(node)
|
275
325
|
return super unless visible?(node, @range)
|
data/lib/ruby_lsp/requests.rb
CHANGED
@@ -14,6 +14,7 @@ module RubyLsp
|
|
14
14
|
# - {RubyLsp::Requests::OnTypeFormatting}
|
15
15
|
# - {RubyLsp::Requests::Diagnostics}
|
16
16
|
# - {RubyLsp::Requests::CodeActions}
|
17
|
+
# - {RubyLsp::Requests::CodeActionResolve}
|
17
18
|
# - {RubyLsp::Requests::DocumentHighlight}
|
18
19
|
# - {RubyLsp::Requests::InlayHints}
|
19
20
|
# - {RubyLsp::Requests::PathCompletion}
|
@@ -30,6 +31,7 @@ module RubyLsp
|
|
30
31
|
autoload :OnTypeFormatting, "ruby_lsp/requests/on_type_formatting"
|
31
32
|
autoload :Diagnostics, "ruby_lsp/requests/diagnostics"
|
32
33
|
autoload :CodeActions, "ruby_lsp/requests/code_actions"
|
34
|
+
autoload :CodeActionResolve, "ruby_lsp/requests/code_action_resolve"
|
33
35
|
autoload :DocumentHighlight, "ruby_lsp/requests/document_highlight"
|
34
36
|
autoload :InlayHints, "ruby_lsp/requests/inlay_hints"
|
35
37
|
autoload :PathCompletion, "ruby_lsp/requests/path_completion"
|
data/lib/ruby_lsp/server.rb
CHANGED
@@ -33,7 +33,7 @@ module RubyLsp
|
|
33
33
|
# fall under the else branch which just pushes requests to the queue
|
34
34
|
@reader.read do |request|
|
35
35
|
case request[:method]
|
36
|
-
when "initialize", "textDocument/didOpen", "textDocument/didClose", "textDocument/didChange"
|
36
|
+
when "initialize", "initialized", "textDocument/didOpen", "textDocument/didClose", "textDocument/didChange"
|
37
37
|
result = Executor.new(@store).execute(request)
|
38
38
|
finalize_request(result, request)
|
39
39
|
when "$/cancelRequest"
|
@@ -56,6 +56,7 @@ module RubyLsp
|
|
56
56
|
# We return zero if shutdown has already been received or one otherwise as per the recommendation in the spec
|
57
57
|
# https://microsoft.github.io/language-server-protocol/specification/#exit
|
58
58
|
status = @store.empty? ? 0 : 1
|
59
|
+
warn("Shutdown complete with status #{status}")
|
59
60
|
exit(status)
|
60
61
|
else
|
61
62
|
# Default case: push the request to the queue to be executed by the worker
|
data/lib/ruby_lsp/store.rb
CHANGED
@@ -12,10 +12,14 @@ module RubyLsp
|
|
12
12
|
sig { params(encoding: String).void }
|
13
13
|
attr_writer :encoding
|
14
14
|
|
15
|
+
sig { returns(String) }
|
16
|
+
attr_accessor :formatter
|
17
|
+
|
15
18
|
sig { void }
|
16
19
|
def initialize
|
17
20
|
@state = T.let({}, T::Hash[String, Document])
|
18
21
|
@encoding = T.let("utf-8", String)
|
22
|
+
@formatter = T.let("auto", String)
|
19
23
|
end
|
20
24
|
|
21
25
|
sig { params(uri: String).returns(Document) }
|
data/lib/ruby_lsp/utils.rb
CHANGED
@@ -5,6 +5,9 @@ module RubyLsp
|
|
5
5
|
# Used to indicate that a request shouldn't return a response
|
6
6
|
VOID = T.let(Object.new.freeze, Object)
|
7
7
|
|
8
|
+
# This freeze is not redundant since the interpolated string is mutable
|
9
|
+
WORKSPACE_URI = T.let("file://#{Dir.pwd}".freeze, String) # rubocop:disable Style/RedundantFreeze
|
10
|
+
|
8
11
|
# A notification to be sent to the client
|
9
12
|
class Notification
|
10
13
|
extend T::Sig
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-lsp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-03-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: language_server-protocol
|
@@ -44,7 +44,7 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: 6.0.2
|
48
48
|
- - "<"
|
49
49
|
- !ruby/object:Gem::Version
|
50
50
|
version: '7'
|
@@ -54,7 +54,7 @@ dependencies:
|
|
54
54
|
requirements:
|
55
55
|
- - ">="
|
56
56
|
- !ruby/object:Gem::Version
|
57
|
-
version:
|
57
|
+
version: 6.0.2
|
58
58
|
- - "<"
|
59
59
|
- !ruby/object:Gem::Version
|
60
60
|
version: '7'
|
@@ -76,6 +76,7 @@ files:
|
|
76
76
|
- lib/ruby_lsp/internal.rb
|
77
77
|
- lib/ruby_lsp/requests.rb
|
78
78
|
- lib/ruby_lsp/requests/base_request.rb
|
79
|
+
- lib/ruby_lsp/requests/code_action_resolve.rb
|
79
80
|
- lib/ruby_lsp/requests/code_actions.rb
|
80
81
|
- lib/ruby_lsp/requests/diagnostics.rb
|
81
82
|
- lib/ruby_lsp/requests/document_highlight.rb
|