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 +4 -4
- data/README.md +39 -1
- data/VERSION +1 -1
- data/exe/ruby-lsp +2 -1
- data/lib/ruby_lsp/document.rb +5 -3
- data/lib/ruby_lsp/executor.rb +390 -0
- data/lib/ruby_lsp/internal.rb +6 -1
- data/lib/ruby_lsp/requests/base_request.rb +10 -3
- data/lib/ruby_lsp/requests/code_action_resolve.rb +100 -0
- data/lib/ruby_lsp/requests/code_actions.rb +38 -8
- data/lib/ruby_lsp/requests/diagnostics.rb +7 -11
- data/lib/ruby_lsp/requests/formatting.rb +10 -5
- data/lib/ruby_lsp/requests/on_type_formatting.rb +33 -14
- data/lib/ruby_lsp/requests/path_completion.rb +95 -0
- data/lib/ruby_lsp/requests/semantic_highlighting.rb +35 -11
- data/lib/ruby_lsp/requests/support/annotation.rb +46 -0
- data/lib/ruby_lsp/requests/support/highlight_target.rb +14 -3
- data/lib/ruby_lsp/requests/support/prefix_tree.rb +80 -0
- data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +29 -44
- data/lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb +1 -1
- data/lib/ruby_lsp/requests/support/rubocop_runner.rb +27 -1
- data/lib/ruby_lsp/requests/support/sorbet.rb +120 -0
- data/lib/ruby_lsp/requests.rb +7 -1
- data/lib/ruby_lsp/server.rb +129 -221
- data/lib/ruby_lsp/utils.rb +78 -0
- metadata +13 -9
- data/lib/ruby_lsp/handler.rb +0 -118
- data/lib/ruby_lsp/queue.rb +0 -182
- data/lib/ruby_lsp/requests/support/syntax_error_diagnostic.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0812eed9a874fa78fbe75cf2e8dc2bd58c2dfb67a61353f732aaba965ae9d66f'
|
4
|
+
data.tar.gz: 6d9a567aa54c95f5fd7ada3756d4b5575f068bee90f19f99f83f485d91de83a0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
1
|
+
0.4.1
|
data/exe/ruby-lsp
CHANGED
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) }
|
@@ -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
|
data/lib/ruby_lsp/internal.rb
CHANGED
@@ -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/
|
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
|
-
|
14
|
-
|
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
|