ruby-lsp 0.3.8 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
# 
|
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
|