ruby-lsp 0.3.8 → 0.4.0
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/executor.rb +362 -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_actions.rb +21 -7
- data/lib/ruby_lsp/requests/diagnostics.rb +7 -11
- data/lib/ruby_lsp/requests/formatting.rb +8 -5
- data/lib/ruby_lsp/requests/on_type_formatting.rb +33 -14
- data/lib/ruby_lsp/requests/path_completion.rb +88 -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 +5 -1
- data/lib/ruby_lsp/server.rb +128 -221
- data/lib/ruby_lsp/utils.rb +78 -0
- metadata +12 -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: 65db08cd80bfe40116f70065b717ad88d1b1fd4fd86c6fcf1de2d5be9636ddce
|
4
|
+
data.tar.gz: 84a483aef0e88650a0b2bdbd0db0c3a5c1cd1cfff1e87508a09d7d4d8a616f36
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1ff522bae2c2d469c1fb5817c1b9ed65867ad19d64e00c0b14d4faa6b38bd62321c3109292f1021174766e2da240bd17faea1f8185672164307d2dafe88845bb
|
7
|
+
data.tar.gz: 7e89c4c38b833269e9d9a09f55333d32f6e1ef4872174d92e7b92a048cf51f62a837b7d9d4c217f3cccb9a196bb6d3b004439e4de8cc9e588c9b445a2f0c46e8
|
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.0
|
data/exe/ruby-lsp
CHANGED
@@ -0,0 +1,362 @@
|
|
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 "textDocument/didOpen"
|
41
|
+
text_document_did_open(uri, request.dig(:params, :textDocument, :text))
|
42
|
+
when "textDocument/didClose"
|
43
|
+
@notifications << Notification.new(
|
44
|
+
message: "textDocument/publishDiagnostics",
|
45
|
+
params: Interface::PublishDiagnosticsParams.new(uri: uri, diagnostics: []),
|
46
|
+
)
|
47
|
+
|
48
|
+
text_document_did_close(uri)
|
49
|
+
when "textDocument/didChange"
|
50
|
+
text_document_did_change(uri, request.dig(:params, :contentChanges))
|
51
|
+
when "textDocument/foldingRange"
|
52
|
+
folding_range(uri)
|
53
|
+
when "textDocument/documentLink"
|
54
|
+
document_link(uri)
|
55
|
+
when "textDocument/selectionRange"
|
56
|
+
selection_range(uri, request.dig(:params, :positions))
|
57
|
+
when "textDocument/documentSymbol"
|
58
|
+
document_symbol(uri)
|
59
|
+
when "textDocument/semanticTokens/full"
|
60
|
+
semantic_tokens_full(uri)
|
61
|
+
when "textDocument/semanticTokens/range"
|
62
|
+
semantic_tokens_range(uri, request.dig(:params, :range))
|
63
|
+
when "textDocument/formatting"
|
64
|
+
begin
|
65
|
+
formatting(uri)
|
66
|
+
rescue StandardError => error
|
67
|
+
@notifications << Notification.new(
|
68
|
+
message: "window/showMessage",
|
69
|
+
params: Interface::ShowMessageParams.new(
|
70
|
+
type: Constant::MessageType::ERROR,
|
71
|
+
message: "Formatting error: #{error.message}",
|
72
|
+
),
|
73
|
+
)
|
74
|
+
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
when "textDocument/documentHighlight"
|
78
|
+
document_highlight(uri, request.dig(:params, :position))
|
79
|
+
when "textDocument/onTypeFormatting"
|
80
|
+
on_type_formatting(uri, request.dig(:params, :position), request.dig(:params, :ch))
|
81
|
+
when "textDocument/hover"
|
82
|
+
hover(uri, request.dig(:params, :position))
|
83
|
+
when "textDocument/inlayHint"
|
84
|
+
inlay_hint(uri, request.dig(:params, :range))
|
85
|
+
when "textDocument/codeAction"
|
86
|
+
code_action(uri, request.dig(:params, :range), request.dig(:params, :context))
|
87
|
+
when "textDocument/diagnostic"
|
88
|
+
begin
|
89
|
+
diagnostic(uri)
|
90
|
+
rescue StandardError => error
|
91
|
+
@notifications << Notification.new(
|
92
|
+
message: "window/showMessage",
|
93
|
+
params: Interface::ShowMessageParams.new(
|
94
|
+
type: Constant::MessageType::ERROR,
|
95
|
+
message: "Error running diagnostics: #{error.message}",
|
96
|
+
),
|
97
|
+
)
|
98
|
+
|
99
|
+
nil
|
100
|
+
end
|
101
|
+
when "textDocument/completion"
|
102
|
+
completion(uri, request.dig(:params, :position))
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
sig { params(uri: String).returns(T::Array[Interface::FoldingRange]) }
|
107
|
+
def folding_range(uri)
|
108
|
+
@store.cache_fetch(uri, :folding_ranges) do |document|
|
109
|
+
Requests::FoldingRanges.new(document).run
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
sig do
|
114
|
+
params(
|
115
|
+
uri: String,
|
116
|
+
position: Document::PositionShape,
|
117
|
+
).returns(T.nilable(Interface::Hover))
|
118
|
+
end
|
119
|
+
def hover(uri, position)
|
120
|
+
RubyLsp::Requests::Hover.new(@store.get(uri), position).run
|
121
|
+
end
|
122
|
+
|
123
|
+
sig { params(uri: String).returns(T::Array[Interface::DocumentLink]) }
|
124
|
+
def document_link(uri)
|
125
|
+
@store.cache_fetch(uri, :document_link) do |document|
|
126
|
+
RubyLsp::Requests::DocumentLink.new(uri, document).run
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
sig { params(uri: String).returns(T::Array[Interface::DocumentSymbol]) }
|
131
|
+
def document_symbol(uri)
|
132
|
+
@store.cache_fetch(uri, :document_symbol) do |document|
|
133
|
+
Requests::DocumentSymbol.new(document).run
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
sig { params(uri: String, content_changes: T::Array[Document::EditShape]).returns(Object) }
|
138
|
+
def text_document_did_change(uri, content_changes)
|
139
|
+
@store.push_edits(uri, content_changes)
|
140
|
+
VOID
|
141
|
+
end
|
142
|
+
|
143
|
+
sig { params(uri: String, text: String).returns(Object) }
|
144
|
+
def text_document_did_open(uri, text)
|
145
|
+
@store.set(uri, text)
|
146
|
+
VOID
|
147
|
+
end
|
148
|
+
|
149
|
+
sig { params(uri: String).returns(Object) }
|
150
|
+
def text_document_did_close(uri)
|
151
|
+
@store.delete(uri)
|
152
|
+
VOID
|
153
|
+
end
|
154
|
+
|
155
|
+
sig do
|
156
|
+
params(
|
157
|
+
uri: String,
|
158
|
+
positions: T::Array[Document::PositionShape],
|
159
|
+
).returns(T.nilable(T::Array[T.nilable(Requests::Support::SelectionRange)]))
|
160
|
+
end
|
161
|
+
def selection_range(uri, positions)
|
162
|
+
ranges = @store.cache_fetch(uri, :selection_ranges) do |document|
|
163
|
+
Requests::SelectionRanges.new(document).run
|
164
|
+
end
|
165
|
+
|
166
|
+
# Per the selection range request spec (https://microsoft.github.io/language-server-protocol/specification#textDocument_selectionRange),
|
167
|
+
# every position in the positions array should have an element at the same index in the response
|
168
|
+
# array. For positions without a valid selection range, the corresponding element in the response
|
169
|
+
# array will be nil.
|
170
|
+
|
171
|
+
unless ranges.nil?
|
172
|
+
positions.map do |position|
|
173
|
+
ranges.find do |range|
|
174
|
+
range.cover?(position)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
sig { params(uri: String).returns(Interface::SemanticTokens) }
|
181
|
+
def semantic_tokens_full(uri)
|
182
|
+
@store.cache_fetch(uri, :semantic_highlighting) do |document|
|
183
|
+
T.cast(
|
184
|
+
Requests::SemanticHighlighting.new(
|
185
|
+
document,
|
186
|
+
encoder: Requests::Support::SemanticTokenEncoder.new,
|
187
|
+
).run,
|
188
|
+
Interface::SemanticTokens,
|
189
|
+
)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
sig { params(uri: String).returns(T.nilable(T::Array[Interface::TextEdit])) }
|
194
|
+
def formatting(uri)
|
195
|
+
Requests::Formatting.new(uri, @store.get(uri)).run
|
196
|
+
end
|
197
|
+
|
198
|
+
sig do
|
199
|
+
params(
|
200
|
+
uri: String,
|
201
|
+
position: Document::PositionShape,
|
202
|
+
character: String,
|
203
|
+
).returns(T::Array[Interface::TextEdit])
|
204
|
+
end
|
205
|
+
def on_type_formatting(uri, position, character)
|
206
|
+
Requests::OnTypeFormatting.new(@store.get(uri), position, character).run
|
207
|
+
end
|
208
|
+
|
209
|
+
sig do
|
210
|
+
params(
|
211
|
+
uri: String,
|
212
|
+
position: Document::PositionShape,
|
213
|
+
).returns(T::Array[Interface::DocumentHighlight])
|
214
|
+
end
|
215
|
+
def document_highlight(uri, position)
|
216
|
+
Requests::DocumentHighlight.new(@store.get(uri), position).run
|
217
|
+
end
|
218
|
+
|
219
|
+
sig { params(uri: String, range: Document::RangeShape).returns(T::Array[Interface::InlayHint]) }
|
220
|
+
def inlay_hint(uri, range)
|
221
|
+
document = @store.get(uri)
|
222
|
+
start_line = range.dig(:start, :line)
|
223
|
+
end_line = range.dig(:end, :line)
|
224
|
+
|
225
|
+
Requests::InlayHints.new(document, start_line..end_line).run
|
226
|
+
end
|
227
|
+
|
228
|
+
sig do
|
229
|
+
params(
|
230
|
+
uri: String,
|
231
|
+
range: Document::RangeShape,
|
232
|
+
context: T::Hash[Symbol, T.untyped],
|
233
|
+
).returns(T.nilable(T::Array[Interface::CodeAction]))
|
234
|
+
end
|
235
|
+
def code_action(uri, range, context)
|
236
|
+
start_line = range.dig(:start, :line)
|
237
|
+
end_line = range.dig(:end, :line)
|
238
|
+
document = @store.get(uri)
|
239
|
+
|
240
|
+
Requests::CodeActions.new(uri, document, start_line..end_line, context).run
|
241
|
+
end
|
242
|
+
|
243
|
+
sig { params(uri: String).returns(T.nilable(Interface::FullDocumentDiagnosticReport)) }
|
244
|
+
def diagnostic(uri)
|
245
|
+
response = @store.cache_fetch(uri, :diagnostics) do |document|
|
246
|
+
Requests::Diagnostics.new(uri, document).run
|
247
|
+
end
|
248
|
+
|
249
|
+
Interface::FullDocumentDiagnosticReport.new(kind: "full", items: response.map(&:to_lsp_diagnostic)) if response
|
250
|
+
end
|
251
|
+
|
252
|
+
sig { params(uri: String, range: Document::RangeShape).returns(Interface::SemanticTokens) }
|
253
|
+
def semantic_tokens_range(uri, range)
|
254
|
+
document = @store.get(uri)
|
255
|
+
start_line = range.dig(:start, :line)
|
256
|
+
end_line = range.dig(:end, :line)
|
257
|
+
|
258
|
+
T.cast(
|
259
|
+
Requests::SemanticHighlighting.new(
|
260
|
+
document,
|
261
|
+
range: start_line..end_line,
|
262
|
+
encoder: Requests::Support::SemanticTokenEncoder.new,
|
263
|
+
).run,
|
264
|
+
Interface::SemanticTokens,
|
265
|
+
)
|
266
|
+
end
|
267
|
+
|
268
|
+
sig do
|
269
|
+
params(uri: String, position: Document::PositionShape).returns(T.nilable(T::Array[Interface::CompletionItem]))
|
270
|
+
end
|
271
|
+
def completion(uri, position)
|
272
|
+
Requests::PathCompletion.new(@store.get(uri), position).run
|
273
|
+
end
|
274
|
+
|
275
|
+
sig { params(options: T::Hash[Symbol, T.untyped]).returns(Interface::InitializeResult) }
|
276
|
+
def initialize_request(options)
|
277
|
+
@store.clear
|
278
|
+
@store.encoding = options.dig(:capabilities, :general, :positionEncodings)
|
279
|
+
enabled_features = options.dig(:initializationOptions, :enabledFeatures) || []
|
280
|
+
|
281
|
+
document_symbol_provider = if enabled_features.include?("documentSymbols")
|
282
|
+
Interface::DocumentSymbolClientCapabilities.new(
|
283
|
+
hierarchical_document_symbol_support: true,
|
284
|
+
symbol_kind: {
|
285
|
+
value_set: Requests::DocumentSymbol::SYMBOL_KIND.values,
|
286
|
+
},
|
287
|
+
)
|
288
|
+
end
|
289
|
+
|
290
|
+
document_link_provider = if enabled_features.include?("documentLink")
|
291
|
+
Interface::DocumentLinkOptions.new(resolve_provider: false)
|
292
|
+
end
|
293
|
+
|
294
|
+
hover_provider = if enabled_features.include?("hover")
|
295
|
+
Interface::HoverClientCapabilities.new(dynamic_registration: false)
|
296
|
+
end
|
297
|
+
|
298
|
+
folding_ranges_provider = if enabled_features.include?("foldingRanges")
|
299
|
+
Interface::FoldingRangeClientCapabilities.new(line_folding_only: true)
|
300
|
+
end
|
301
|
+
|
302
|
+
semantic_tokens_provider = if enabled_features.include?("semanticHighlighting")
|
303
|
+
Interface::SemanticTokensRegistrationOptions.new(
|
304
|
+
document_selector: { scheme: "file", language: "ruby" },
|
305
|
+
legend: Interface::SemanticTokensLegend.new(
|
306
|
+
token_types: Requests::SemanticHighlighting::TOKEN_TYPES.keys,
|
307
|
+
token_modifiers: Requests::SemanticHighlighting::TOKEN_MODIFIERS.keys,
|
308
|
+
),
|
309
|
+
range: true,
|
310
|
+
full: { delta: false },
|
311
|
+
)
|
312
|
+
end
|
313
|
+
|
314
|
+
diagnostics_provider = if enabled_features.include?("diagnostics")
|
315
|
+
{
|
316
|
+
interFileDependencies: false,
|
317
|
+
workspaceDiagnostics: false,
|
318
|
+
}
|
319
|
+
end
|
320
|
+
|
321
|
+
on_type_formatting_provider = if enabled_features.include?("onTypeFormatting")
|
322
|
+
Interface::DocumentOnTypeFormattingOptions.new(
|
323
|
+
first_trigger_character: "{",
|
324
|
+
more_trigger_character: ["\n", "|"],
|
325
|
+
)
|
326
|
+
end
|
327
|
+
|
328
|
+
inlay_hint_provider = if enabled_features.include?("inlayHint")
|
329
|
+
Interface::InlayHintOptions.new(resolve_provider: false)
|
330
|
+
end
|
331
|
+
|
332
|
+
completion_provider = if enabled_features.include?("completion")
|
333
|
+
Interface::CompletionOptions.new(
|
334
|
+
resolve_provider: false,
|
335
|
+
trigger_characters: ["/"],
|
336
|
+
)
|
337
|
+
end
|
338
|
+
|
339
|
+
Interface::InitializeResult.new(
|
340
|
+
capabilities: Interface::ServerCapabilities.new(
|
341
|
+
text_document_sync: Interface::TextDocumentSyncOptions.new(
|
342
|
+
change: Constant::TextDocumentSyncKind::INCREMENTAL,
|
343
|
+
open_close: true,
|
344
|
+
),
|
345
|
+
selection_range_provider: enabled_features.include?("selectionRanges"),
|
346
|
+
hover_provider: hover_provider,
|
347
|
+
document_symbol_provider: document_symbol_provider,
|
348
|
+
document_link_provider: document_link_provider,
|
349
|
+
folding_range_provider: folding_ranges_provider,
|
350
|
+
semantic_tokens_provider: semantic_tokens_provider,
|
351
|
+
document_formatting_provider: enabled_features.include?("formatting"),
|
352
|
+
document_highlight_provider: enabled_features.include?("documentHighlights"),
|
353
|
+
code_action_provider: enabled_features.include?("codeActions"),
|
354
|
+
document_on_type_formatting_provider: on_type_formatting_provider,
|
355
|
+
diagnostic_provider: diagnostics_provider,
|
356
|
+
inlay_hint_provider: inlay_hint_provider,
|
357
|
+
completion_provider: completion_provider,
|
358
|
+
),
|
359
|
+
)
|
360
|
+
end
|
361
|
+
end
|
362
|
+
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]
|
@@ -24,24 +24,38 @@ module RubyLsp
|
|
24
24
|
uri: String,
|
25
25
|
document: Document,
|
26
26
|
range: T::Range[Integer],
|
27
|
+
context: T::Hash[Symbol, T.untyped],
|
27
28
|
).void
|
28
29
|
end
|
29
|
-
def initialize(uri, document, range)
|
30
|
+
def initialize(uri, document, range, context)
|
30
31
|
super(document)
|
31
32
|
|
32
33
|
@uri = uri
|
33
34
|
@range = range
|
35
|
+
@context = context
|
34
36
|
end
|
35
37
|
|
36
|
-
sig { override.returns(T.all(T::Array[
|
38
|
+
sig { override.returns(T.nilable(T.all(T::Array[Interface::CodeAction], Object))) }
|
37
39
|
def run
|
38
|
-
diagnostics = @
|
39
|
-
|
40
|
-
|
40
|
+
diagnostics = @context[:diagnostics]
|
41
|
+
return if diagnostics.nil? || diagnostics.empty?
|
42
|
+
|
43
|
+
diagnostics.filter_map do |diagnostic|
|
44
|
+
code_action = diagnostic.dig(:data, :code_action)
|
45
|
+
next if code_action.nil?
|
46
|
+
|
47
|
+
# We want to return only code actions that are within range or that do not have any edits, such as refactor
|
48
|
+
# code actions
|
49
|
+
range = code_action.dig(:edit, :documentChanges, 0, :edits, 0, :range)
|
50
|
+
code_action if diagnostic.dig(:data, :correctable) && cover?(range)
|
41
51
|
end
|
42
|
-
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
43
55
|
|
44
|
-
|
56
|
+
sig { params(range: T.nilable(Document::RangeShape)).returns(T::Boolean) }
|
57
|
+
def cover?(range)
|
58
|
+
range.nil? || @range.cover?(range.dig(:start, :line)..range.dig(:end, :line))
|
45
59
|
end
|
46
60
|
end
|
47
61
|
end
|
@@ -15,7 +15,7 @@ module RubyLsp
|
|
15
15
|
#
|
16
16
|
# ```ruby
|
17
17
|
# def say_hello
|
18
|
-
# puts "Hello" # --> diagnostics: incorrect
|
18
|
+
# puts "Hello" # --> diagnostics: incorrect indentation
|
19
19
|
# end
|
20
20
|
# ```
|
21
21
|
class Diagnostics < BaseRequest
|
@@ -28,17 +28,13 @@ module RubyLsp
|
|
28
28
|
@uri = uri
|
29
29
|
end
|
30
30
|
|
31
|
-
sig
|
32
|
-
override.returns(
|
33
|
-
T.any(
|
34
|
-
T.all(T::Array[Support::RuboCopDiagnostic], Object),
|
35
|
-
T.all(T::Array[Support::SyntaxErrorDiagnostic], Object),
|
36
|
-
),
|
37
|
-
)
|
38
|
-
end
|
31
|
+
sig { override.returns(T.nilable(T.all(T::Array[Support::RuboCopDiagnostic], Object))) }
|
39
32
|
def run
|
40
|
-
return
|
41
|
-
return
|
33
|
+
return if @document.syntax_error?
|
34
|
+
return unless defined?(Support::RuboCopDiagnosticsRunner)
|
35
|
+
|
36
|
+
# Don't try to run RuboCop diagnostics for files outside the current working directory
|
37
|
+
return unless @uri.sub("file://", "").start_with?(Dir.pwd)
|
42
38
|
|
43
39
|
Support::RuboCopDiagnosticsRunner.instance.run(@uri, @document)
|
44
40
|
end
|
@@ -32,8 +32,11 @@ module RubyLsp
|
|
32
32
|
@uri = uri
|
33
33
|
end
|
34
34
|
|
35
|
-
sig { override.returns(T.nilable(T.all(T::Array[
|
35
|
+
sig { override.returns(T.nilable(T.all(T::Array[Interface::TextEdit], Object))) }
|
36
36
|
def run
|
37
|
+
# Don't try to format files outside the current working directory
|
38
|
+
return unless @uri.sub("file://", "").start_with?(Dir.pwd)
|
39
|
+
|
37
40
|
formatted_text = formatted_file
|
38
41
|
return unless formatted_text
|
39
42
|
|
@@ -41,10 +44,10 @@ module RubyLsp
|
|
41
44
|
return if formatted_text.size == size && formatted_text == @document.source
|
42
45
|
|
43
46
|
[
|
44
|
-
|
45
|
-
range:
|
46
|
-
start:
|
47
|
-
end:
|
47
|
+
Interface::TextEdit.new(
|
48
|
+
range: Interface::Range.new(
|
49
|
+
start: Interface::Position.new(line: 0, character: 0),
|
50
|
+
end: Interface::Position.new(line: size, character: size),
|
48
51
|
),
|
49
52
|
new_text: formatted_text,
|
50
53
|
),
|
@@ -32,8 +32,8 @@ module RubyLsp
|
|
32
32
|
|
33
33
|
scanner = document.create_scanner
|
34
34
|
line_begin = position[:line] == 0 ? 0 : scanner.find_char_position({ line: position[:line] - 1, character: 0 })
|
35
|
-
line_end = scanner.find_char_position(position)
|
36
|
-
line = T.must(@document.source[line_begin
|
35
|
+
@line_end = T.let(scanner.find_char_position(position), Integer)
|
36
|
+
line = T.must(@document.source[line_begin..@line_end])
|
37
37
|
|
38
38
|
@indentation = T.let(find_indentation(line), Integer)
|
39
39
|
@previous_line = T.let(line.strip.chomp, String)
|
@@ -42,7 +42,7 @@ module RubyLsp
|
|
42
42
|
@trigger_character = trigger_character
|
43
43
|
end
|
44
44
|
|
45
|
-
sig { override.returns(T.
|
45
|
+
sig { override.returns(T.all(T::Array[Interface::TextEdit], Object)) }
|
46
46
|
def run
|
47
47
|
case @trigger_character
|
48
48
|
when "{"
|
@@ -84,8 +84,30 @@ module RubyLsp
|
|
84
84
|
|
85
85
|
indents = " " * @indentation
|
86
86
|
|
87
|
-
|
88
|
-
|
87
|
+
if @previous_line.include?("\n")
|
88
|
+
# If the previous line has a line break, then it means there's content after the line break that triggered
|
89
|
+
# this completion. For these cases, we want to add the `end` after the content and move the cursor back to the
|
90
|
+
# keyword that triggered the completion
|
91
|
+
|
92
|
+
line = @position[:line]
|
93
|
+
|
94
|
+
# If there are enough lines in the document, we want to add the `end` token on the line below the extra
|
95
|
+
# content. Otherwise, we want to insert and extra line break ourselves
|
96
|
+
correction = if T.must(@document.source[@line_end..-1]).count("\n") >= 2
|
97
|
+
line -= 1
|
98
|
+
"#{indents}end"
|
99
|
+
else
|
100
|
+
"#{indents}\nend"
|
101
|
+
end
|
102
|
+
|
103
|
+
add_edit_with_text(correction, { line: @position[:line] + 1, character: @position[:character] })
|
104
|
+
move_cursor_to(line, @indentation + 3)
|
105
|
+
else
|
106
|
+
# If there's nothing after the new line break that triggered the completion, then we want to add the `end` and
|
107
|
+
# move the cursor to the body of the statement
|
108
|
+
add_edit_with_text(" \n#{indents}end")
|
109
|
+
move_cursor_to(@position[:line], @indentation + 2)
|
110
|
+
end
|
89
111
|
end
|
90
112
|
|
91
113
|
sig { params(spaces: String).void }
|
@@ -94,18 +116,15 @@ module RubyLsp
|
|
94
116
|
move_cursor_to(@position[:line], @indentation + spaces.size + 1)
|
95
117
|
end
|
96
118
|
|
97
|
-
sig { params(text: String).void }
|
98
|
-
def add_edit_with_text(text)
|
99
|
-
|
100
|
-
line:
|
101
|
-
character:
|
119
|
+
sig { params(text: String, position: Document::PositionShape).void }
|
120
|
+
def add_edit_with_text(text, position = @position)
|
121
|
+
pos = Interface::Position.new(
|
122
|
+
line: position[:line],
|
123
|
+
character: position[:character],
|
102
124
|
)
|
103
125
|
|
104
126
|
@edits << Interface::TextEdit.new(
|
105
|
-
range: Interface::Range.new(
|
106
|
-
start: position,
|
107
|
-
end: position,
|
108
|
-
),
|
127
|
+
range: Interface::Range.new(start: pos, end: pos),
|
109
128
|
new_text: text,
|
110
129
|
)
|
111
130
|
end
|