sadr 0.1.0 → 0.2.0
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/CHANGELOG.md +7 -0
- data/README.md +67 -1
- data/lib/sadr/client.rb +994 -175
- data/lib/sadr/future.rb +1 -1
- data/lib/sadr/protocol.rb +135 -11
- data/lib/sadr/testing/fake_client.rb +20 -0
- data/lib/sadr/testing/fake_server.rb +173 -0
- data/lib/sadr/testing/fake_transport.rb +108 -0
- data/lib/sadr/testing.rb +6 -0
- data/lib/sadr/transport.rb +67 -21
- data/lib/sadr/version.rb +1 -1
- data/lib/sadr.rb +27 -3
- data/sig/sadr.rbs +65 -9
- metadata +6 -2
data/lib/sadr/client.rb
CHANGED
|
@@ -8,8 +8,39 @@ module Sadr
|
|
|
8
8
|
definition: "definition",
|
|
9
9
|
type_definition: "typeDefinition",
|
|
10
10
|
implementation: "implementation",
|
|
11
|
-
signature_help: "signatureHelp"
|
|
11
|
+
signature_help: "signatureHelp",
|
|
12
|
+
prepare_rename: "prepareRename",
|
|
13
|
+
document_highlight: "documentHighlight",
|
|
14
|
+
prepare_call_hierarchy: "prepareCallHierarchy",
|
|
15
|
+
prepare_type_hierarchy: "prepareTypeHierarchy",
|
|
16
|
+
linked_editing_range: "linkedEditingRange"
|
|
12
17
|
}.freeze
|
|
18
|
+
RESPONSE_KINDS = {
|
|
19
|
+
"completion" => :completion,
|
|
20
|
+
"hover" => :hover,
|
|
21
|
+
"definition" => :locations,
|
|
22
|
+
"typeDefinition" => :locations,
|
|
23
|
+
"implementation" => :locations,
|
|
24
|
+
"references" => :locations,
|
|
25
|
+
"rename" => :workspace_edit,
|
|
26
|
+
"prepareRename" => :prepare_rename,
|
|
27
|
+
"documentHighlight" => :document_highlights,
|
|
28
|
+
"prepareCallHierarchy" => :hierarchy_items,
|
|
29
|
+
"prepareTypeHierarchy" => :hierarchy_items,
|
|
30
|
+
"linkedEditingRange" => :linked_editing_ranges,
|
|
31
|
+
"signatureHelp" => :signature_help,
|
|
32
|
+
"documentSymbol" => :document_symbols,
|
|
33
|
+
"formatting" => :text_edits,
|
|
34
|
+
"rangeFormatting" => :text_edits,
|
|
35
|
+
"codeAction" => :code_actions,
|
|
36
|
+
"codeLens" => :code_lenses,
|
|
37
|
+
"inlayHint" => :inlay_hints,
|
|
38
|
+
"foldingRange" => :folding_ranges,
|
|
39
|
+
"documentLink" => :document_links,
|
|
40
|
+
"diagnostic" => :diagnostic
|
|
41
|
+
}.freeze
|
|
42
|
+
RESPONSE_ITEM_LIMIT = 10_000
|
|
43
|
+
private_constant :RESPONSE_ITEM_LIMIT
|
|
13
44
|
|
|
14
45
|
attr_reader :capabilities, :transport, :diagnostics, :state, :errors, :server_info, :position_encoding
|
|
15
46
|
|
|
@@ -20,85 +51,93 @@ module Sadr
|
|
|
20
51
|
@restart = restart
|
|
21
52
|
@env = env
|
|
22
53
|
@initialization_options = initialization_options
|
|
23
|
-
@configuration = configuration
|
|
54
|
+
@configuration = json_snapshot(configuration, "configuration must be JSON")
|
|
24
55
|
@pending = {}
|
|
25
56
|
@handlers = {}
|
|
26
57
|
@documents = {}
|
|
27
58
|
@diagnostics = {}
|
|
28
59
|
@semantic = {}
|
|
60
|
+
@semantic_requests = Hash.new(0)
|
|
29
61
|
@sequence = 0
|
|
30
62
|
@lock = Mutex.new
|
|
63
|
+
@document_lock = Mutex.new
|
|
64
|
+
@configuration_lock = Mutex.new
|
|
65
|
+
@connecting_transport = nil
|
|
31
66
|
@state = :stopped
|
|
32
67
|
@restarts = 0
|
|
33
68
|
@epoch = 0
|
|
69
|
+
@semantic_generation = 0
|
|
34
70
|
@errors = []
|
|
35
71
|
@capabilities = {}
|
|
36
72
|
end
|
|
37
73
|
|
|
38
74
|
def start(timeout: 10)
|
|
39
|
-
|
|
75
|
+
connect(timeout: timeout, state: :starting)
|
|
76
|
+
@lock.synchronize do
|
|
77
|
+
raise Error, "language server closed during initialization" unless @state == :starting
|
|
40
78
|
|
|
41
|
-
|
|
42
|
-
@state = :starting
|
|
43
|
-
epoch = (@epoch += 1)
|
|
44
|
-
@semantic.clear
|
|
45
|
-
@transport = Transport.new(@command, cwd: @root, env: @env) do |message, error|
|
|
46
|
-
receive(message, error, epoch)
|
|
79
|
+
@state = :running
|
|
47
80
|
end
|
|
48
|
-
|
|
49
|
-
raise Error, "invalid initialize result" unless result.is_a?(Hash) && result["capabilities"].is_a?(Hash)
|
|
50
|
-
|
|
51
|
-
@capabilities = result["capabilities"]
|
|
52
|
-
@server_info = result["serverInfo"]
|
|
53
|
-
validate_capabilities
|
|
54
|
-
notify("initialized", {})
|
|
55
|
-
@state = :running
|
|
56
|
-
self
|
|
57
|
-
rescue StandardError => error
|
|
58
|
-
@transport&.close if epoch
|
|
59
|
-
fail_pending(error) if epoch
|
|
60
|
-
@state = :failed if epoch
|
|
61
|
-
raise
|
|
81
|
+
@capabilities
|
|
62
82
|
end
|
|
63
83
|
|
|
64
84
|
def stop
|
|
65
|
-
|
|
85
|
+
running, transport, stop_epoch, connecting, restart_thread = @lock.synchronize do
|
|
86
|
+
@closing = true
|
|
87
|
+
[@state == :running, @transport, @epoch, @connecting_transport, @restart_thread]
|
|
88
|
+
end
|
|
89
|
+
shutdown = nil
|
|
66
90
|
begin
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
91
|
+
shutdown = Thread.new do
|
|
92
|
+
locked = false
|
|
93
|
+
begin
|
|
94
|
+
locked = @document_lock.try_lock
|
|
95
|
+
next unless locked
|
|
96
|
+
|
|
97
|
+
send_request("shutdown", {}, nil, state: :running, epoch: stop_epoch, transport: transport, allow_closing: true).await(timeout: 2)
|
|
98
|
+
send_notification("exit", {}, state: :running, epoch: stop_epoch, transport: transport, allow_closing: true) if transport&.alive?
|
|
99
|
+
rescue Error
|
|
100
|
+
nil
|
|
101
|
+
ensure
|
|
102
|
+
@document_lock.unlock if locked
|
|
103
|
+
end
|
|
104
|
+
end if running
|
|
105
|
+
shutdown&.join(2)
|
|
71
106
|
ensure
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
@
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
107
|
+
[transport, connecting].compact.uniq.each(&:close)
|
|
108
|
+
[shutdown, restart_thread].compact.uniq.each { |thread| finish_thread(thread) }
|
|
109
|
+
pending = @lock.synchronize do
|
|
110
|
+
next unless @epoch == stop_epoch && @transport.equal?(transport)
|
|
111
|
+
|
|
112
|
+
@epoch += 1
|
|
113
|
+
@documents.clear
|
|
114
|
+
@semantic.clear
|
|
115
|
+
@semantic_requests.clear
|
|
116
|
+
@semantic_generation += 1
|
|
117
|
+
@diagnostics.clear
|
|
118
|
+
@connecting_transport = nil
|
|
119
|
+
@restart_thread = nil
|
|
120
|
+
@state = :stopped
|
|
121
|
+
values = @pending.values
|
|
122
|
+
@pending.clear
|
|
123
|
+
values
|
|
124
|
+
end
|
|
125
|
+
error = Error.new("language server stopped")
|
|
126
|
+
pending&.each { |future, _| future.fulfill(error: error) }
|
|
79
127
|
end
|
|
80
128
|
end
|
|
81
129
|
|
|
82
|
-
def running?
|
|
130
|
+
def running?
|
|
131
|
+
transport = @lock.synchronize { @transport if @state == :running && !@closing }
|
|
132
|
+
!!transport&.alive?
|
|
133
|
+
end
|
|
83
134
|
|
|
84
135
|
def request(method, params = {})
|
|
85
|
-
|
|
86
|
-
future = Future.new(id, on_error: method(:report_error)) { |number| cancel(number) }
|
|
87
|
-
@lock.synchronize { @pending[id] = future }
|
|
88
|
-
raise Error, "language server is not connected" unless @transport&.alive?
|
|
89
|
-
|
|
90
|
-
@transport.write(jsonrpc: "2.0", id: id, method: method.to_s, params: params)
|
|
91
|
-
future
|
|
92
|
-
rescue StandardError => error
|
|
93
|
-
@lock.synchronize { @pending.delete(id) }
|
|
94
|
-
future.fulfill(error: error)
|
|
95
|
-
future
|
|
136
|
+
send_request(method, params, nil)
|
|
96
137
|
end
|
|
97
138
|
|
|
98
139
|
def notify(method, params = {})
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
@transport.write(jsonrpc: "2.0", method: method.to_s, params: params)
|
|
140
|
+
send_notification(method, params)
|
|
102
141
|
end
|
|
103
142
|
|
|
104
143
|
def on(method, &handler)
|
|
@@ -110,72 +149,100 @@ module Sadr
|
|
|
110
149
|
def supports?(capability) = !!@capabilities[capability.to_s]
|
|
111
150
|
|
|
112
151
|
def open(document)
|
|
113
|
-
validate_document(document)
|
|
114
|
-
stored = Document.new(uri:
|
|
115
|
-
version: document.version, text:
|
|
116
|
-
@
|
|
117
|
-
|
|
152
|
+
uri, language_id, text = validate_document(document)
|
|
153
|
+
stored = Document.new(uri: uri.freeze, language_id: language_id.freeze,
|
|
154
|
+
version: document.version, text: text.freeze)
|
|
155
|
+
@document_lock.synchronize do
|
|
156
|
+
send_open = @lock.synchronize do
|
|
157
|
+
ensure_running!
|
|
158
|
+
@documents[stored.uri] = stored
|
|
159
|
+
@diagnostics.delete(stored.uri)
|
|
160
|
+
@semantic.delete(stored.uri)
|
|
161
|
+
open_close?
|
|
162
|
+
end
|
|
163
|
+
notify_open(stored) if send_open
|
|
164
|
+
end
|
|
118
165
|
stored.uri
|
|
119
166
|
end
|
|
120
167
|
|
|
121
168
|
def change(uri, version, changes)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
169
|
+
uri = valid_uri(uri)
|
|
170
|
+
@document_lock.synchronize do
|
|
171
|
+
updated, notification = @lock.synchronize do
|
|
172
|
+
ensure_running!
|
|
173
|
+
document = @documents.fetch(uri) { raise Error, "document is not open" }
|
|
174
|
+
unless Protocol.uint?(version) && version > document.version
|
|
175
|
+
raise Error, "document version must increase"
|
|
176
|
+
end
|
|
177
|
+
raise Error, "content changes must be a nonempty Array" unless changes.is_a?(Array) && !changes.empty?
|
|
178
|
+
|
|
179
|
+
text = document.text
|
|
180
|
+
wire_changes = changes.map do |change|
|
|
181
|
+
change_text = validate_change(change)
|
|
182
|
+
if change.range
|
|
183
|
+
index = DocumentIndex.new(text)
|
|
184
|
+
first = Protocol.offset(index, change.range.start)
|
|
185
|
+
last = Protocol.offset(index, change.range.end)
|
|
186
|
+
raise Error, "invalid content change range" if last < first
|
|
187
|
+
|
|
188
|
+
text = text.byteslice(0, first) + change_text + text.byteslice(last, text.bytesize - last)
|
|
189
|
+
{range: Protocol.range_hash(change.range), text: change_text}
|
|
190
|
+
else
|
|
191
|
+
text = change_text
|
|
192
|
+
{text: change_text}
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
updated = Document.new(uri: document.uri, language_id: document.language_id, version: version, text: text.freeze)
|
|
196
|
+
@documents[uri] = updated
|
|
197
|
+
|
|
198
|
+
mode = sync_mode
|
|
199
|
+
notification = if [1, 2].include?(mode)
|
|
200
|
+
content_changes = mode == 2 ? wire_changes : [{text: text}]
|
|
201
|
+
{textDocument: {uri: uri, version: version}, contentChanges: content_changes}
|
|
202
|
+
end
|
|
203
|
+
[updated, notification]
|
|
142
204
|
end
|
|
205
|
+
notify("textDocument/didChange", notification) if notification
|
|
206
|
+
updated
|
|
143
207
|
end
|
|
144
|
-
updated = Document.new(uri: document.uri, language_id: document.language_id, version: version, text: text.freeze)
|
|
145
|
-
@documents[uri] = updated
|
|
146
|
-
|
|
147
|
-
mode = sync_mode
|
|
148
|
-
return updated unless [1, 2].include?(mode)
|
|
149
|
-
|
|
150
|
-
content_changes = mode == 2 ? wire_changes : [{text: text}]
|
|
151
|
-
notify("textDocument/didChange", {textDocument: {uri: uri, version: version}, contentChanges: content_changes})
|
|
152
|
-
updated
|
|
153
208
|
rescue KeyError
|
|
154
209
|
raise Error, "document is not open"
|
|
155
210
|
end
|
|
156
211
|
|
|
157
212
|
def save(uri, text: nil)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
213
|
+
uri = valid_uri(uri)
|
|
214
|
+
text = utf8_string(text, "saved text must be valid UTF-8") unless text.nil?
|
|
215
|
+
@document_lock.synchronize do
|
|
216
|
+
params = @lock.synchronize do
|
|
217
|
+
ensure_running!
|
|
218
|
+
document = @documents.fetch(uri) { raise Error, "document is not open" }
|
|
219
|
+
sync = @capabilities["textDocumentSync"]
|
|
220
|
+
save = sync.is_a?(Hash) ? sync["save"] : sync.is_a?(Integer) && sync.positive?
|
|
221
|
+
next unless save
|
|
222
|
+
|
|
223
|
+
value = {textDocument: {uri: uri}}
|
|
224
|
+
value[:text] = text || document.text if save.is_a?(Hash) && save["includeText"]
|
|
225
|
+
value
|
|
226
|
+
end
|
|
227
|
+
notify("textDocument/didSave", params) if params
|
|
165
228
|
end
|
|
166
|
-
params = {textDocument: {uri: uri}}
|
|
167
|
-
params[:text] = text || document.text if save.is_a?(Hash) && save["includeText"]
|
|
168
|
-
notify("textDocument/didSave", params)
|
|
169
229
|
rescue KeyError
|
|
170
230
|
raise Error, "document is not open"
|
|
171
231
|
end
|
|
172
232
|
|
|
173
233
|
def close(uri)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
234
|
+
uri = valid_uri(uri)
|
|
235
|
+
@document_lock.synchronize do
|
|
236
|
+
send_close = @lock.synchronize do
|
|
237
|
+
ensure_running!
|
|
238
|
+
raise Error, "document is not open" unless @documents.delete(uri)
|
|
239
|
+
|
|
240
|
+
@diagnostics.delete(uri)
|
|
241
|
+
@semantic.delete(uri)
|
|
242
|
+
open_close?
|
|
243
|
+
end
|
|
244
|
+
notify("textDocument/didClose", {textDocument: {uri: uri}}) if send_close
|
|
245
|
+
end
|
|
179
246
|
end
|
|
180
247
|
|
|
181
248
|
POSITION_METHODS.each do |ruby_name, lsp_name|
|
|
@@ -189,7 +256,7 @@ module Sadr
|
|
|
189
256
|
end
|
|
190
257
|
|
|
191
258
|
def rename(uri, position, new_name)
|
|
192
|
-
|
|
259
|
+
new_name = utf8_string(new_name, "new name must be a valid UTF-8 String")
|
|
193
260
|
|
|
194
261
|
request_at("rename", uri, position, newName: new_name)
|
|
195
262
|
end
|
|
@@ -202,6 +269,10 @@ module Sadr
|
|
|
202
269
|
request_document("formatting", uri, options: options)
|
|
203
270
|
end
|
|
204
271
|
|
|
272
|
+
def range_formatting(uri, range, options)
|
|
273
|
+
request_document("rangeFormatting", uri, range: Protocol.range_hash(range), options: formatting_options(options))
|
|
274
|
+
end
|
|
275
|
+
|
|
205
276
|
def code_action(uri, range, context)
|
|
206
277
|
raise Error, "code action context must be an object" unless context.is_a?(Hash)
|
|
207
278
|
|
|
@@ -210,11 +281,51 @@ module Sadr
|
|
|
210
281
|
|
|
211
282
|
def code_lens(uri) = request_document("codeLens", uri)
|
|
212
283
|
def inlay_hint(uri, range) = request_document("inlayHint", uri, range: Protocol.range_hash(range))
|
|
284
|
+
def folding_range(uri) = request_document("foldingRange", uri)
|
|
285
|
+
def document_link(uri) = request_document("documentLink", uri)
|
|
286
|
+
|
|
287
|
+
def selection_range(uri, positions)
|
|
288
|
+
raise Error, "positions must be an Array" unless positions.is_a?(Array)
|
|
289
|
+
|
|
290
|
+
wire_positions = positions.map { |position| Protocol.position_hash(position) }
|
|
291
|
+
params = {textDocument: {uri: valid_uri(uri)}, positions: wire_positions}
|
|
292
|
+
send_request("textDocument/selectionRange", params, lambda do |value|
|
|
293
|
+
raise Error, "invalid LSP response" unless valid_selection_ranges?(value, wire_positions)
|
|
294
|
+
|
|
295
|
+
value
|
|
296
|
+
end)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def did_change_configuration(settings)
|
|
300
|
+
settings = json_snapshot(settings, "settings must be JSON")
|
|
301
|
+
@configuration_lock.synchronize do
|
|
302
|
+
@lock.synchronize do
|
|
303
|
+
ensure_running!
|
|
304
|
+
@configuration = settings
|
|
305
|
+
end
|
|
306
|
+
notify("workspace/didChangeConfiguration", settings: settings)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def did_change_watched_files(events)
|
|
311
|
+
raise Error, "file events must be an Array" unless events.is_a?(Array)
|
|
312
|
+
|
|
313
|
+
values = events.map do |event|
|
|
314
|
+
raise Error, "invalid file event" unless event.is_a?(Hash)
|
|
315
|
+
|
|
316
|
+
uri = event.key?(:uri) ? event[:uri] : event["uri"]
|
|
317
|
+
type = event.key?(:type) ? event[:type] : event["type"]
|
|
318
|
+
raise Error, "invalid file event" unless type.is_a?(Integer) && type.between?(1, 3)
|
|
319
|
+
|
|
320
|
+
{uri: valid_uri(uri), type: type}
|
|
321
|
+
end
|
|
322
|
+
notify("workspace/didChangeWatchedFiles", changes: values)
|
|
323
|
+
end
|
|
213
324
|
|
|
214
325
|
def diagnostic(uri, previous_result_id: nil)
|
|
215
326
|
params = {}
|
|
216
327
|
if previous_result_id
|
|
217
|
-
|
|
328
|
+
previous_result_id = utf8_string(previous_result_id, "previous result id must be a valid UTF-8 String")
|
|
218
329
|
|
|
219
330
|
params[:previousResultId] = previous_result_id
|
|
220
331
|
end
|
|
@@ -222,43 +333,60 @@ module Sadr
|
|
|
222
333
|
end
|
|
223
334
|
|
|
224
335
|
def semantic_tokens(uri, version:)
|
|
225
|
-
document = @
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
336
|
+
document, provider, previous, generation, request_sequence = @lock.synchronize do
|
|
337
|
+
document = @documents[uri]
|
|
338
|
+
provider = @capabilities["semanticTokensProvider"]
|
|
339
|
+
request_sequence = @semantic_requests[uri] += 1 if document&.version == version && provider.is_a?(Hash) && provider["full"]
|
|
340
|
+
[document, provider, @semantic[uri], @semantic_generation, request_sequence]
|
|
341
|
+
end
|
|
342
|
+
return [] unless request_sequence
|
|
230
343
|
|
|
231
|
-
previous = @semantic[uri]
|
|
232
344
|
delta = previous && previous[0] && provider["full"].is_a?(Hash) && provider["full"]["delta"]
|
|
233
345
|
method = delta ? "textDocument/semanticTokens/full/delta" : "textDocument/semanticTokens/full"
|
|
234
346
|
params = {textDocument: {uri: uri}}
|
|
235
347
|
params[:previousResultId] = previous[0] if delta
|
|
236
|
-
result =
|
|
237
|
-
current = @documents[uri]
|
|
238
|
-
return [] unless result && current && current.version == version
|
|
348
|
+
result = checked_request(method, params, :semantic).await
|
|
239
349
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
350
|
+
@lock.synchronize do
|
|
351
|
+
current = @documents[uri]
|
|
352
|
+
current_request = @semantic_requests[uri]
|
|
353
|
+
return [] unless result && current.equal?(document) && current.version == version &&
|
|
354
|
+
generation == @semantic_generation && request_sequence == current_request
|
|
355
|
+
|
|
356
|
+
raise Error, "unexpected semantic token delta" if !delta && !result.key?("data")
|
|
243
357
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
358
|
+
data = result["data"] || Protocol.semantic_delta(previous[1], result["edits"])
|
|
359
|
+
tokens = Protocol.semantic_tokens(data, legend: provider["legend"])
|
|
360
|
+
@semantic[uri] = [result["resultId"], data]
|
|
361
|
+
tokens
|
|
362
|
+
end
|
|
248
363
|
end
|
|
249
364
|
|
|
250
365
|
def workspace_symbols(query)
|
|
251
|
-
|
|
366
|
+
query = utf8_string(query, "query must be a valid UTF-8 String")
|
|
252
367
|
|
|
253
|
-
|
|
368
|
+
checked_request("workspace/symbol", {query: query}, :workspace_symbols)
|
|
254
369
|
end
|
|
255
370
|
|
|
256
|
-
def resolve_completion(item) = resolve("completionItem/resolve", item)
|
|
257
|
-
def resolve_code_action(action) = resolve("codeAction/resolve", action)
|
|
258
|
-
def resolve_code_lens(lens) = resolve("codeLens/resolve", lens)
|
|
371
|
+
def resolve_completion(item) = resolve("completionItem/resolve", item, :completion_item)
|
|
372
|
+
def resolve_code_action(action) = resolve("codeAction/resolve", action, :code_action)
|
|
373
|
+
def resolve_code_lens(lens) = resolve("codeLens/resolve", lens, :code_lens)
|
|
374
|
+
|
|
375
|
+
def resolve_document_link(link)
|
|
376
|
+
link = json_snapshot(link, "document link must be JSON")
|
|
377
|
+
raise Error, "invalid document link" unless valid_document_link?(link)
|
|
378
|
+
|
|
379
|
+
checked_request("documentLink/resolve", link, :document_link)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def call_hierarchy_incoming_calls(item) = hierarchy_request("callHierarchy/incomingCalls", item, :incoming_calls)
|
|
383
|
+
def call_hierarchy_outgoing_calls(item) = hierarchy_request("callHierarchy/outgoingCalls", item, :outgoing_calls)
|
|
384
|
+
def type_hierarchy_supertypes(item) = hierarchy_request("typeHierarchy/supertypes", item, :hierarchy_items)
|
|
385
|
+
def type_hierarchy_subtypes(item) = hierarchy_request("typeHierarchy/subtypes", item, :hierarchy_items)
|
|
259
386
|
|
|
260
387
|
def execute_command(command, arguments: [])
|
|
261
|
-
|
|
388
|
+
command = utf8_string(command, "command must be a nonempty valid UTF-8 String")
|
|
389
|
+
raise Error, "command must be a nonempty valid UTF-8 String" if command.empty?
|
|
262
390
|
raise Error, "arguments must be an Array" unless arguments.is_a?(Array)
|
|
263
391
|
|
|
264
392
|
request("workspace/executeCommand", {command: command, arguments: arguments})
|
|
@@ -266,6 +394,548 @@ module Sadr
|
|
|
266
394
|
|
|
267
395
|
private
|
|
268
396
|
|
|
397
|
+
def finish_thread(thread)
|
|
398
|
+
return if thread == Thread.current || thread.join(1)
|
|
399
|
+
|
|
400
|
+
thread.kill
|
|
401
|
+
thread.join(1)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def send_request(method, params, validator, state: :running, epoch: nil, transport: nil, allow_closing: false)
|
|
405
|
+
target = request_epoch = future = id = nil
|
|
406
|
+
@lock.synchronize do
|
|
407
|
+
id = @sequence += 1
|
|
408
|
+
target = connected_transport(state: state, epoch: epoch, transport: transport, allow_closing: allow_closing)
|
|
409
|
+
request_epoch = @epoch
|
|
410
|
+
future = Future.new(id, on_error: method(:report_error)) { |number| cancel(number, target, request_epoch) }
|
|
411
|
+
@pending[id] = [future, validator] if target
|
|
412
|
+
end
|
|
413
|
+
raise Error, "language server is not connected" unless target&.alive?
|
|
414
|
+
|
|
415
|
+
target.write(jsonrpc: "2.0", id: id, method: method.to_s, params: params)
|
|
416
|
+
future
|
|
417
|
+
rescue StandardError => error
|
|
418
|
+
@lock.synchronize { @pending.delete(id) }
|
|
419
|
+
future.fulfill(error: error)
|
|
420
|
+
future
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def checked_request(method, params, kind, within: nil, **options)
|
|
424
|
+
send_request(method, params, ->(value) { validate_response(kind, value, within: within) }, **options)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def send_notification(method, params, state: :running, epoch: nil, transport: nil, allow_closing: false)
|
|
428
|
+
target = @lock.synchronize do
|
|
429
|
+
connected_transport(state: state, epoch: epoch, transport: transport, allow_closing: allow_closing)
|
|
430
|
+
end
|
|
431
|
+
raise Error, "language server is not connected" unless target&.alive?
|
|
432
|
+
|
|
433
|
+
target.write(jsonrpc: "2.0", method: method.to_s, params: params)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def connected_transport(state:, epoch:, transport:, allow_closing:)
|
|
437
|
+
return if state && @state != state
|
|
438
|
+
return if !allow_closing && @closing
|
|
439
|
+
return if epoch && @epoch != epoch
|
|
440
|
+
return if transport && !@transport.equal?(transport)
|
|
441
|
+
|
|
442
|
+
@transport
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def connect(timeout:, state:, expected_epoch: nil)
|
|
446
|
+
transport = nil
|
|
447
|
+
epoch = @lock.synchronize do
|
|
448
|
+
if expected_epoch && (@epoch != expected_epoch || @closing || @state != :failed)
|
|
449
|
+
raise Error, "language server restart was cancelled"
|
|
450
|
+
end
|
|
451
|
+
raise Error, "language server is already started" if %i[starting restarting running].include?(@state)
|
|
452
|
+
|
|
453
|
+
@closing = false if state == :starting
|
|
454
|
+
@state = state
|
|
455
|
+
@epoch += 1
|
|
456
|
+
@semantic.clear
|
|
457
|
+
@semantic_requests.clear
|
|
458
|
+
@semantic_generation += 1
|
|
459
|
+
@epoch
|
|
460
|
+
end
|
|
461
|
+
transport = build_transport(epoch)
|
|
462
|
+
installed = @lock.synchronize do
|
|
463
|
+
next false unless @epoch == epoch && @state == state && !@closing
|
|
464
|
+
|
|
465
|
+
@transport = transport
|
|
466
|
+
@connecting_transport = nil if @connecting_transport.equal?(transport)
|
|
467
|
+
true
|
|
468
|
+
end
|
|
469
|
+
raise Error, "language server connection was cancelled" unless installed
|
|
470
|
+
|
|
471
|
+
result = checked_request("initialize", initialize_params, :initialize, state: state, epoch: epoch, transport: transport).await(timeout: timeout)
|
|
472
|
+
@lock.synchronize do
|
|
473
|
+
unless @epoch == epoch && @state == state && !@closing && @transport.equal?(transport)
|
|
474
|
+
raise Error, "language server connection was cancelled"
|
|
475
|
+
end
|
|
476
|
+
@capabilities = result["capabilities"]
|
|
477
|
+
@server_info = result["serverInfo"]
|
|
478
|
+
validate_capabilities
|
|
479
|
+
end
|
|
480
|
+
send_notification("initialized", {}, state: state, epoch: epoch, transport: transport)
|
|
481
|
+
@capabilities
|
|
482
|
+
rescue StandardError => error
|
|
483
|
+
transport&.close
|
|
484
|
+
active = @lock.synchronize do
|
|
485
|
+
next false unless epoch && @epoch == epoch
|
|
486
|
+
|
|
487
|
+
@state = :failed unless @closing
|
|
488
|
+
!@closing
|
|
489
|
+
end
|
|
490
|
+
fail_pending(error) if active
|
|
491
|
+
raise
|
|
492
|
+
ensure
|
|
493
|
+
@lock.synchronize do
|
|
494
|
+
@connecting_transport = nil if @connecting_transport.equal?(transport)
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def build_transport(epoch)
|
|
499
|
+
Transport.new(@command, cwd: @root, env: @env, on_spawn: ->(transport) {
|
|
500
|
+
tracked = @lock.synchronize do
|
|
501
|
+
next false unless @epoch == epoch && !@closing && %i[starting restarting].include?(@state)
|
|
502
|
+
|
|
503
|
+
@connecting_transport = transport
|
|
504
|
+
true
|
|
505
|
+
end
|
|
506
|
+
unless tracked
|
|
507
|
+
transport.close
|
|
508
|
+
raise Error, "language server connection was cancelled"
|
|
509
|
+
end
|
|
510
|
+
}) do |message, error|
|
|
511
|
+
receive(message, error, epoch)
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def fulfill_response(entry, message)
|
|
516
|
+
return unless entry
|
|
517
|
+
|
|
518
|
+
future, validator = entry
|
|
519
|
+
if message["error"]
|
|
520
|
+
future.fulfill(error: ServerError.new(message["error"]))
|
|
521
|
+
else
|
|
522
|
+
value = validator ? validator.call(message["result"]) : message["result"]
|
|
523
|
+
future.fulfill(value)
|
|
524
|
+
end
|
|
525
|
+
rescue StandardError => error
|
|
526
|
+
future&.fulfill(error: error)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def validate_response(kind, value, within: nil)
|
|
530
|
+
case kind
|
|
531
|
+
when :initialize
|
|
532
|
+
valid = value.is_a?(Hash) && value["capabilities"].is_a?(Hash)
|
|
533
|
+
when :hover
|
|
534
|
+
valid = valid_hover?(value)
|
|
535
|
+
when :signature_help
|
|
536
|
+
valid = valid_signature_help?(value)
|
|
537
|
+
when :document_symbols
|
|
538
|
+
valid = valid_document_symbols?(value)
|
|
539
|
+
when :workspace_symbols
|
|
540
|
+
valid = valid_workspace_symbols?(value)
|
|
541
|
+
when :locations
|
|
542
|
+
valid = value.nil? || valid_locations?(value)
|
|
543
|
+
when :prepare_rename
|
|
544
|
+
valid = valid_prepare_rename?(value)
|
|
545
|
+
when :document_highlights
|
|
546
|
+
valid = value.nil? || (value.is_a?(Array) && value.all? { |highlight| valid_document_highlight?(highlight) })
|
|
547
|
+
when :hierarchy_items
|
|
548
|
+
valid = value.nil? || (value.is_a?(Array) && value.length <= RESPONSE_ITEM_LIMIT &&
|
|
549
|
+
value.all? { |item| valid_hierarchy_item?(item) })
|
|
550
|
+
when :incoming_calls
|
|
551
|
+
valid = value.nil? || (value.is_a?(Array) && value.length <= RESPONSE_ITEM_LIMIT &&
|
|
552
|
+
value.all? { |call| valid_incoming_call?(call) })
|
|
553
|
+
when :outgoing_calls
|
|
554
|
+
valid = value.nil? || (value.is_a?(Array) && value.length <= RESPONSE_ITEM_LIMIT &&
|
|
555
|
+
value.all? { |call| valid_outgoing_call?(call, within: within) })
|
|
556
|
+
when :linked_editing_ranges
|
|
557
|
+
valid = valid_linked_editing_ranges?(value)
|
|
558
|
+
when :completion
|
|
559
|
+
valid = valid_completion?(value)
|
|
560
|
+
when :completion_item
|
|
561
|
+
valid = valid_completion_item?(value)
|
|
562
|
+
when :workspace_edit
|
|
563
|
+
valid = value.nil?
|
|
564
|
+
Protocol.workspace_edit(value) unless valid
|
|
565
|
+
valid = true
|
|
566
|
+
when :text_edits
|
|
567
|
+
valid = value.nil?
|
|
568
|
+
validate_text_edits(value) unless valid
|
|
569
|
+
valid = true
|
|
570
|
+
when :code_actions
|
|
571
|
+
valid = value.nil? || value.is_a?(Array)
|
|
572
|
+
value&.each do |action|
|
|
573
|
+
raise Error, "invalid LSP response" unless valid_code_action?(action)
|
|
574
|
+
|
|
575
|
+
Protocol.workspace_edit(action["edit"]) if action["edit"]
|
|
576
|
+
end
|
|
577
|
+
when :code_action
|
|
578
|
+
valid = valid_code_action?(value)
|
|
579
|
+
Protocol.workspace_edit(value["edit"]) if valid && value["edit"]
|
|
580
|
+
when :code_lenses
|
|
581
|
+
valid = value.nil? || value.is_a?(Array)
|
|
582
|
+
value&.each { |lens| validate_code_lens(lens) }
|
|
583
|
+
when :code_lens
|
|
584
|
+
valid = value.is_a?(Hash)
|
|
585
|
+
validate_code_lens(value) if valid
|
|
586
|
+
when :inlay_hints
|
|
587
|
+
valid = value.nil? || value.is_a?(Array)
|
|
588
|
+
value&.each { |hint| validate_inlay_hint(hint) }
|
|
589
|
+
when :folding_ranges
|
|
590
|
+
valid = value.nil? || (value.is_a?(Array) && value.all? { |range| valid_folding_range?(range) })
|
|
591
|
+
when :document_links
|
|
592
|
+
valid = value.nil? || (value.is_a?(Array) && value.length <= RESPONSE_ITEM_LIMIT &&
|
|
593
|
+
value.all? { |link| valid_document_link?(link) })
|
|
594
|
+
when :document_link
|
|
595
|
+
valid = valid_document_link?(value)
|
|
596
|
+
when :diagnostic
|
|
597
|
+
valid = value.nil? || valid_diagnostic_report?(value)
|
|
598
|
+
when :semantic
|
|
599
|
+
valid = value.nil? || (value.is_a?(Hash) &&
|
|
600
|
+
(!value.key?("resultId") || value["resultId"].is_a?(String)) &&
|
|
601
|
+
(!value.key?("data") || value["data"].is_a?(Array)) &&
|
|
602
|
+
(!value.key?("edits") || value["edits"].is_a?(Array)))
|
|
603
|
+
else
|
|
604
|
+
valid = false
|
|
605
|
+
end
|
|
606
|
+
raise Error, "invalid LSP response" unless valid
|
|
607
|
+
|
|
608
|
+
value
|
|
609
|
+
rescue KeyError, NoMethodError
|
|
610
|
+
raise Error, "invalid LSP response"
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def valid_completion?(value)
|
|
614
|
+
return true if value.nil?
|
|
615
|
+
|
|
616
|
+
items = if value.is_a?(Hash)
|
|
617
|
+
return false unless boolean?(value["isIncomplete"])
|
|
618
|
+
|
|
619
|
+
value["items"]
|
|
620
|
+
else
|
|
621
|
+
value
|
|
622
|
+
end
|
|
623
|
+
items.is_a?(Array) && items.all? { |item| valid_completion_item?(item) }
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def valid_completion_item?(item)
|
|
627
|
+
item.is_a?(Hash) && item["label"].is_a?(String)
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def valid_hover?(hover)
|
|
631
|
+
return true if hover.nil?
|
|
632
|
+
return false unless hover.is_a?(Hash) && hover.key?("contents") && valid_hover_contents?(hover["contents"])
|
|
633
|
+
|
|
634
|
+
Protocol.range_value(hover["range"]) if hover.key?("range")
|
|
635
|
+
true
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def valid_hover_contents?(contents)
|
|
639
|
+
return true if contents.is_a?(String)
|
|
640
|
+
return contents.all? { |item| valid_marked_string?(item) } if contents.is_a?(Array)
|
|
641
|
+
return false unless contents.is_a?(Hash)
|
|
642
|
+
|
|
643
|
+
if contents.key?("kind")
|
|
644
|
+
%w[plaintext markdown].include?(contents["kind"]) && contents["value"].is_a?(String)
|
|
645
|
+
else
|
|
646
|
+
valid_marked_string?(contents)
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def valid_marked_string?(value)
|
|
651
|
+
value.is_a?(String) || (value.is_a?(Hash) && value["language"].is_a?(String) && value["value"].is_a?(String))
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def valid_signature_help?(help)
|
|
655
|
+
return true if help.nil?
|
|
656
|
+
return false unless help.is_a?(Hash) && help["signatures"].is_a?(Array)
|
|
657
|
+
return false unless optional_uint?(help, "activeSignature") && optional_uint?(help, "activeParameter")
|
|
658
|
+
|
|
659
|
+
help["signatures"].all? { |signature| valid_signature?(signature) }
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def valid_signature?(signature)
|
|
663
|
+
return false unless signature.is_a?(Hash) && signature["label"].is_a?(String)
|
|
664
|
+
return false unless optional_uint?(signature, "activeParameter")
|
|
665
|
+
return true unless signature.key?("parameters")
|
|
666
|
+
|
|
667
|
+
signature["parameters"].is_a?(Array) && signature["parameters"].all? do |parameter|
|
|
668
|
+
next false unless parameter.is_a?(Hash)
|
|
669
|
+
|
|
670
|
+
label = parameter["label"]
|
|
671
|
+
label.is_a?(String) || (label.is_a?(Array) && label.length == 2 && label.all? { |offset| Protocol.uint?(offset) } && label[0] <= label[1])
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
def valid_document_symbols?(value)
|
|
676
|
+
value.nil? || (value.is_a?(Array) && value.all? do |symbol|
|
|
677
|
+
symbol.is_a?(Hash) && symbol.key?("location") ? valid_symbol_information?(symbol) : valid_document_symbol?(symbol)
|
|
678
|
+
end)
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
def valid_document_symbol?(symbol)
|
|
682
|
+
return false unless valid_symbol?(symbol)
|
|
683
|
+
|
|
684
|
+
Protocol.range_value(symbol.fetch("range"))
|
|
685
|
+
Protocol.range_value(symbol.fetch("selectionRange"))
|
|
686
|
+
!symbol.key?("children") || (symbol["children"].is_a?(Array) && symbol["children"].all? { |child| valid_document_symbol?(child) })
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def valid_workspace_symbols?(value)
|
|
690
|
+
value.nil? || (value.is_a?(Array) && value.all? { |symbol| valid_workspace_symbol?(symbol) })
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
def valid_workspace_symbol?(symbol)
|
|
694
|
+
return false unless valid_symbol?(symbol) && symbol["location"].is_a?(Hash)
|
|
695
|
+
|
|
696
|
+
location = symbol["location"]
|
|
697
|
+
valid_uri(location.fetch("uri"))
|
|
698
|
+
Protocol.range_value(location["range"]) if location.key?("range")
|
|
699
|
+
true
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def valid_symbol_information?(symbol)
|
|
703
|
+
valid_symbol?(symbol) && valid_location?(symbol["location"])
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
def valid_symbol?(symbol)
|
|
707
|
+
symbol.is_a?(Hash) && symbol["name"].is_a?(String) && symbol["kind"].is_a?(Integer) && symbol["kind"].between?(1, 26)
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def valid_locations?(value)
|
|
711
|
+
locations = value.is_a?(Array) ? value : [value]
|
|
712
|
+
locations.all? { |location| valid_location?(location) || valid_location_link?(location) }
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
def valid_prepare_rename?(value)
|
|
716
|
+
return true if value.nil?
|
|
717
|
+
|
|
718
|
+
if value.is_a?(Hash) && (value.key?("defaultBehavior") || value.key?(:defaultBehavior))
|
|
719
|
+
default_behavior = value.key?("defaultBehavior") ? value["defaultBehavior"] : value[:defaultBehavior]
|
|
720
|
+
return boolean?(default_behavior)
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
if value.is_a?(Hash) && (value.key?("range") || value.key?(:range))
|
|
724
|
+
range = value["range"] || value[:range]
|
|
725
|
+
placeholder = value.key?("placeholder") ? value["placeholder"] : value[:placeholder]
|
|
726
|
+
return false unless placeholder.is_a?(String)
|
|
727
|
+
|
|
728
|
+
Protocol.range_value(range)
|
|
729
|
+
else
|
|
730
|
+
Protocol.range_value(value)
|
|
731
|
+
end
|
|
732
|
+
true
|
|
733
|
+
rescue Error, KeyError
|
|
734
|
+
false
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
def valid_document_highlight?(highlight)
|
|
738
|
+
return false unless highlight.is_a?(Hash)
|
|
739
|
+
|
|
740
|
+
Protocol.range_value(highlight.fetch("range"))
|
|
741
|
+
!highlight.key?("kind") || (highlight["kind"].is_a?(Integer) && highlight["kind"].between?(1, 3))
|
|
742
|
+
rescue Error, KeyError
|
|
743
|
+
false
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def valid_hierarchy_item?(item)
|
|
747
|
+
return false unless valid_symbol?(item) && item["uri"].is_a?(String)
|
|
748
|
+
|
|
749
|
+
valid_uri(item["uri"])
|
|
750
|
+
range = Protocol.range_value(item.fetch("range"))
|
|
751
|
+
selection_range = Protocol.range_value(item.fetch("selectionRange"))
|
|
752
|
+
return false unless range_contains_range?(range, selection_range)
|
|
753
|
+
return false if item.key?("detail") && !item["detail"].is_a?(String)
|
|
754
|
+
return false if item.key?("tags") && !(item["tags"].is_a?(Array) && item["tags"].all? { |tag| tag == 1 })
|
|
755
|
+
|
|
756
|
+
json_snapshot(item["data"], "invalid LSP response") if item.key?("data")
|
|
757
|
+
|
|
758
|
+
true
|
|
759
|
+
rescue Error, KeyError
|
|
760
|
+
false
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def valid_linked_editing_ranges?(value)
|
|
764
|
+
return true if value.nil?
|
|
765
|
+
return false unless value.is_a?(Hash) && value["ranges"].is_a?(Array)
|
|
766
|
+
return false if value.key?("wordPattern") && !value["wordPattern"].is_a?(String)
|
|
767
|
+
|
|
768
|
+
ranges = value["ranges"].map { |range| Protocol.range_value(range) }
|
|
769
|
+
ranges.sort_by! { |range| position_tuple(range.start) }
|
|
770
|
+
ranges.each_cons(2).all? { |first, last| position_before_or_equal?(first.end, last.start) }
|
|
771
|
+
rescue Error
|
|
772
|
+
false
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
def valid_folding_range?(range)
|
|
776
|
+
return false unless range.is_a?(Hash)
|
|
777
|
+
|
|
778
|
+
first = range["startLine"]
|
|
779
|
+
last = range["endLine"]
|
|
780
|
+
return false unless Protocol.uint?(first) && Protocol.uint?(last) && first <= last
|
|
781
|
+
return false unless optional_uint?(range, "startCharacter") && optional_uint?(range, "endCharacter")
|
|
782
|
+
return false if range.key?("kind") && !range["kind"].is_a?(String)
|
|
783
|
+
return false if range.key?("collapsedText") && !range["collapsedText"].is_a?(String)
|
|
784
|
+
|
|
785
|
+
first != last || !range.key?("startCharacter") || !range.key?("endCharacter") || range["startCharacter"] <= range["endCharacter"]
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
def valid_selection_ranges?(value, positions)
|
|
789
|
+
return true if value.nil?
|
|
790
|
+
return false unless value.is_a?(Array) && value.length == positions.length
|
|
791
|
+
|
|
792
|
+
pending = value.each_index.map { |index| [value[index], positions[index], nil, 0] }
|
|
793
|
+
until pending.empty?
|
|
794
|
+
selection, position, child_range, depth = pending.pop
|
|
795
|
+
return false unless selection.is_a?(Hash) && depth < 256
|
|
796
|
+
|
|
797
|
+
range = Protocol.range_value(selection.fetch("range"))
|
|
798
|
+
return false unless range_contains_position?(range, position)
|
|
799
|
+
return false if child_range && !range_contains_range?(range, child_range)
|
|
800
|
+
|
|
801
|
+
pending << [selection["parent"], position, range, depth + 1] if selection.key?("parent")
|
|
802
|
+
end
|
|
803
|
+
true
|
|
804
|
+
rescue Error, KeyError
|
|
805
|
+
false
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
def valid_document_link?(link)
|
|
809
|
+
return false unless link.is_a?(Hash)
|
|
810
|
+
|
|
811
|
+
Protocol.range_value(link.fetch("range"))
|
|
812
|
+
valid_uri(link["target"]) if link.key?("target")
|
|
813
|
+
return false if link.key?("tooltip") && !link["tooltip"].is_a?(String)
|
|
814
|
+
|
|
815
|
+
json_snapshot(link["data"], "invalid LSP response") if link.key?("data")
|
|
816
|
+
|
|
817
|
+
true
|
|
818
|
+
rescue Error, KeyError
|
|
819
|
+
false
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
def range_contains_position?(range, position)
|
|
823
|
+
range = Protocol.range_value(range)
|
|
824
|
+
position_before_or_equal?(range.start, position) && position_before_or_equal?(position, range.end)
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
def valid_incoming_call?(call)
|
|
828
|
+
return false unless call.is_a?(Hash) && valid_hierarchy_item?(call["from"])
|
|
829
|
+
|
|
830
|
+
valid_ranges?(call["fromRanges"], within: call["from"]["range"])
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
def valid_outgoing_call?(call, within: nil)
|
|
834
|
+
call.is_a?(Hash) && valid_hierarchy_item?(call["to"]) && valid_ranges?(call["fromRanges"], within: within)
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
def valid_ranges?(values, within: nil)
|
|
838
|
+
return false unless values.is_a?(Array) && values.length <= RESPONSE_ITEM_LIMIT
|
|
839
|
+
|
|
840
|
+
ranges = values.map { |range| Protocol.range_value(range) }
|
|
841
|
+
!within || ranges.all? { |range| range_contains_range?(within, range) }
|
|
842
|
+
rescue Error, KeyError
|
|
843
|
+
false
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
def range_contains_range?(outer, inner)
|
|
847
|
+
outer = Protocol.range_value(outer)
|
|
848
|
+
inner = Protocol.range_value(inner)
|
|
849
|
+
position_before_or_equal?(outer.start, inner.start) && position_before_or_equal?(inner.end, outer.end)
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
def position_before_or_equal?(first, last)
|
|
853
|
+
(position_tuple(first) <=> position_tuple(last)) <= 0
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
def position_tuple(value)
|
|
857
|
+
value = Protocol.position_value(value)
|
|
858
|
+
[value.line, value.character]
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
def valid_location?(location)
|
|
862
|
+
return false unless location.is_a?(Hash) && location.key?("uri")
|
|
863
|
+
|
|
864
|
+
valid_uri(location["uri"])
|
|
865
|
+
Protocol.range_value(location.fetch("range"))
|
|
866
|
+
true
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
def valid_location_link?(location)
|
|
870
|
+
return false unless location.is_a?(Hash) && location.key?("targetUri")
|
|
871
|
+
|
|
872
|
+
valid_uri(location["targetUri"])
|
|
873
|
+
Protocol.range_value(location.fetch("targetRange"))
|
|
874
|
+
Protocol.range_value(location.fetch("targetSelectionRange"))
|
|
875
|
+
Protocol.range_value(location["originSelectionRange"]) if location.key?("originSelectionRange")
|
|
876
|
+
true
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
def valid_code_action?(action)
|
|
880
|
+
return false unless action.is_a?(Hash) && action["title"].is_a?(String)
|
|
881
|
+
return valid_command?(action) if action["command"].is_a?(String)
|
|
882
|
+
return false if action.key?("command") && !valid_command?(action["command"])
|
|
883
|
+
|
|
884
|
+
Protocol.diagnostics(action["diagnostics"]) if action.key?("diagnostics")
|
|
885
|
+
true
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
def valid_command?(command)
|
|
889
|
+
command.is_a?(Hash) && command["title"].is_a?(String) && command["command"].is_a?(String) &&
|
|
890
|
+
(!command.key?("arguments") || command["arguments"].is_a?(Array))
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
def validate_code_lens(lens)
|
|
894
|
+
raise Error, "invalid LSP response" unless lens.is_a?(Hash)
|
|
895
|
+
|
|
896
|
+
Protocol.range_value(lens.fetch("range"))
|
|
897
|
+
raise Error, "invalid LSP response" if lens.key?("command") && !valid_command?(lens["command"])
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
def validate_inlay_hint(hint)
|
|
901
|
+
label = hint["label"] if hint.is_a?(Hash)
|
|
902
|
+
valid_label = label.is_a?(String) || (label.is_a?(Array) && label.all? { |part| part.is_a?(Hash) && part["value"].is_a?(String) })
|
|
903
|
+
raise Error, "invalid LSP response" unless valid_label
|
|
904
|
+
|
|
905
|
+
Protocol.position_value(hint.fetch("position"))
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
def valid_diagnostic_report?(report)
|
|
909
|
+
return false unless report.is_a?(Hash)
|
|
910
|
+
|
|
911
|
+
case report["kind"]
|
|
912
|
+
when "full"
|
|
913
|
+
Protocol.diagnostics(report.fetch("items"))
|
|
914
|
+
true
|
|
915
|
+
when "unchanged"
|
|
916
|
+
report["resultId"].is_a?(String)
|
|
917
|
+
else
|
|
918
|
+
false
|
|
919
|
+
end
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
def optional_uint?(value, key)
|
|
923
|
+
!value.key?(key) || Protocol.uint?(value[key])
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
def boolean?(value) = value == true || value == false
|
|
927
|
+
|
|
928
|
+
def validate_text_edits(value)
|
|
929
|
+
raise Error, "invalid LSP response" unless value.is_a?(Array)
|
|
930
|
+
|
|
931
|
+
value.each do |edit|
|
|
932
|
+
valid = edit.is_a?(Hash) && edit["newText"].is_a?(String) && edit["newText"].valid_encoding?
|
|
933
|
+
raise Error, "invalid LSP response" unless valid
|
|
934
|
+
|
|
935
|
+
Protocol.range_value(edit["range"])
|
|
936
|
+
end
|
|
937
|
+
end
|
|
938
|
+
|
|
269
939
|
def initialize_params
|
|
270
940
|
{
|
|
271
941
|
processId: Process.pid,
|
|
@@ -282,6 +952,16 @@ module Sadr
|
|
|
282
952
|
hover: {contentFormat: %w[markdown plaintext]},
|
|
283
953
|
signatureHelp: {signatureInformation: {documentationFormat: %w[markdown plaintext], parameterInformation: {labelOffsetSupport: true}}},
|
|
284
954
|
documentSymbol: {hierarchicalDocumentSymbolSupport: true},
|
|
955
|
+
documentHighlight: {dynamicRegistration: false},
|
|
956
|
+
foldingRange: {dynamicRegistration: false, lineFoldingOnly: false},
|
|
957
|
+
selectionRange: {dynamicRegistration: false},
|
|
958
|
+
rename: {dynamicRegistration: false, prepareSupport: true, prepareSupportDefaultBehavior: 1},
|
|
959
|
+
callHierarchy: {dynamicRegistration: false},
|
|
960
|
+
typeHierarchy: {dynamicRegistration: false},
|
|
961
|
+
documentLink: {dynamicRegistration: false, tooltipSupport: true},
|
|
962
|
+
linkedEditingRange: {dynamicRegistration: false},
|
|
963
|
+
formatting: {dynamicRegistration: false},
|
|
964
|
+
rangeFormatting: {dynamicRegistration: false},
|
|
285
965
|
codeAction: {codeActionLiteralSupport: {codeActionKind: {valueSet: %w[quickfix refactor refactor.extract refactor.inline refactor.rewrite source source.organizeImports]}}, resolveSupport: {properties: ["edit"]}},
|
|
286
966
|
publishDiagnostics: {relatedInformation: true, versionSupport: true},
|
|
287
967
|
diagnostic: {dynamicRegistration: false, relatedDocumentSupport: false},
|
|
@@ -289,7 +969,9 @@ module Sadr
|
|
|
289
969
|
codeLens: {dynamicRegistration: false},
|
|
290
970
|
semanticTokens: {requests: {full: {delta: true}}, tokenTypes: %w[namespace type class enum interface struct typeParameter parameter variable property enumMember event function method macro keyword modifier comment string number regexp operator decorator], tokenModifiers: %w[declaration definition readonly static deprecated abstract async modification documentation defaultLibrary], formats: ["relative"], overlappingTokenSupport: false, multilineTokenSupport: false}
|
|
291
971
|
},
|
|
292
|
-
workspace: {applyEdit: true, configuration: true, workspaceFolders: true,
|
|
972
|
+
workspace: {applyEdit: true, configuration: true, workspaceFolders: true,
|
|
973
|
+
didChangeConfiguration: {dynamicRegistration: false}, didChangeWatchedFiles: {dynamicRegistration: false},
|
|
974
|
+
workspaceEdit: {documentChanges: true, resourceOperations: %w[create rename delete], failureHandling: "abort"}}
|
|
293
975
|
}
|
|
294
976
|
}
|
|
295
977
|
end
|
|
@@ -310,46 +992,118 @@ module Sadr
|
|
|
310
992
|
|
|
311
993
|
def validate_document(document)
|
|
312
994
|
raise Error, "expected a Document" unless document.is_a?(Document)
|
|
313
|
-
valid_uri(document.uri)
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
end
|
|
995
|
+
uri = valid_uri(document.uri)
|
|
996
|
+
language_id = utf8_string(document.language_id, "language id must be a nonempty valid UTF-8 String")
|
|
997
|
+
raise Error, "language id must be a nonempty valid UTF-8 String" if language_id.empty?
|
|
317
998
|
raise Error, "document version must be an unsigned integer" unless Protocol.uint?(document.version)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
999
|
+
text = utf8_string(document.text, "document text must be valid UTF-8")
|
|
1000
|
+
[uri, language_id, text]
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
def ensure_running!
|
|
1004
|
+
raise Error, "language server is not running" unless @state == :running && !@closing
|
|
321
1005
|
end
|
|
322
1006
|
|
|
323
1007
|
def validate_change(change)
|
|
324
1008
|
raise Error, "expected a ContentChange" unless change.is_a?(ContentChange)
|
|
325
|
-
raise Error, "change text must be valid UTF-8" unless change.text.is_a?(String) && change.text.valid_encoding?
|
|
326
1009
|
|
|
327
1010
|
Protocol.range_value(change.range) if change.range
|
|
1011
|
+
utf8_string(change.text, "change text must be valid UTF-8")
|
|
328
1012
|
end
|
|
329
1013
|
|
|
330
1014
|
def valid_uri(uri)
|
|
331
|
-
|
|
1015
|
+
uri = utf8_string(uri, "invalid URI")
|
|
1016
|
+
valid = !uri.empty? && !uri.include?("\0")
|
|
1017
|
+
parsed = URI::DEFAULT_PARSER.parse(uri) if valid
|
|
1018
|
+
raise Error, "invalid URI" unless valid && parsed&.scheme && !parsed.scheme.empty?
|
|
332
1019
|
|
|
333
1020
|
uri
|
|
1021
|
+
rescue URI::InvalidURIError
|
|
1022
|
+
raise Error, "invalid URI"
|
|
1023
|
+
end
|
|
1024
|
+
|
|
1025
|
+
def formatting_options(value)
|
|
1026
|
+
value = json_snapshot(value, "formatting options must be JSON")
|
|
1027
|
+
valid = value.is_a?(Hash) && Protocol.uint?(value["tabSize"]) && boolean?(value["insertSpaces"])
|
|
1028
|
+
valid &&= value.all? do |key, item|
|
|
1029
|
+
%w[tabSize insertSpaces].include?(key) || boolean?(item) || item.is_a?(String) ||
|
|
1030
|
+
(item.is_a?(Integer) && item.between?(-0x80000000, 0x7fffffff))
|
|
1031
|
+
end
|
|
1032
|
+
raise Error, "invalid formatting options" unless valid
|
|
1033
|
+
|
|
1034
|
+
value
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
def json_snapshot(value, message)
|
|
1038
|
+
pending = [[value, 0]]
|
|
1039
|
+
until pending.empty?
|
|
1040
|
+
item, depth = pending.pop
|
|
1041
|
+
case item
|
|
1042
|
+
when NilClass, TrueClass, FalseClass
|
|
1043
|
+
next
|
|
1044
|
+
when Integer
|
|
1045
|
+
raise Error, message unless item.between?(-0x80000000, 0x7fffffff)
|
|
1046
|
+
when Float
|
|
1047
|
+
raise Error, message unless item.finite?
|
|
1048
|
+
when String
|
|
1049
|
+
raise Error, message unless item.dup.force_encoding(Encoding::UTF_8).valid_encoding?
|
|
1050
|
+
when Array
|
|
1051
|
+
raise Error, message if depth >= 100
|
|
1052
|
+
|
|
1053
|
+
item.each { |child| pending << [child, depth + 1] }
|
|
1054
|
+
when Hash
|
|
1055
|
+
raise Error, message if depth >= 100
|
|
1056
|
+
raise Error, message unless item.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) }
|
|
1057
|
+
raise Error, message unless item.keys.map(&:to_s).uniq.length == item.length
|
|
1058
|
+
|
|
1059
|
+
item.each_key do |key|
|
|
1060
|
+
key = key.to_s
|
|
1061
|
+
raise Error, message unless key.dup.force_encoding(Encoding::UTF_8).valid_encoding?
|
|
1062
|
+
end
|
|
1063
|
+
item.each_value { |child| pending << [child, depth + 1] }
|
|
1064
|
+
else
|
|
1065
|
+
raise Error, message
|
|
1066
|
+
end
|
|
1067
|
+
end
|
|
1068
|
+
JSON.parse(JSON.generate(value), freeze: true)
|
|
1069
|
+
rescue JSON::JSONError, EncodingError, RuntimeError
|
|
1070
|
+
raise Error, message
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
def utf8_string(value, message)
|
|
1074
|
+
raise Error, message unless value.is_a?(String)
|
|
1075
|
+
|
|
1076
|
+
value = value.dup.force_encoding(Encoding::UTF_8)
|
|
1077
|
+
raise Error, message unless value.valid_encoding?
|
|
1078
|
+
|
|
1079
|
+
value
|
|
334
1080
|
end
|
|
335
1081
|
|
|
336
1082
|
def request_at(method, uri, position, params = {})
|
|
337
1083
|
core = {textDocument: {uri: valid_uri(uri)}, position: Protocol.position_hash(position)}
|
|
338
|
-
|
|
1084
|
+
checked_request("textDocument/#{method}", params.merge(core), RESPONSE_KINDS.fetch(method))
|
|
339
1085
|
end
|
|
340
1086
|
|
|
341
1087
|
def request_document(method, uri, **params)
|
|
342
|
-
|
|
1088
|
+
checked_request("textDocument/#{method}", params.merge(textDocument: {uri: valid_uri(uri)}), RESPONSE_KINDS.fetch(method))
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1091
|
+
def hierarchy_request(method, item, kind)
|
|
1092
|
+
item = json_snapshot(item, "hierarchy item must be JSON")
|
|
1093
|
+
raise Error, "invalid hierarchy item" unless valid_hierarchy_item?(item)
|
|
1094
|
+
|
|
1095
|
+
checked_request(method, {item: item}, kind, within: item["range"])
|
|
343
1096
|
end
|
|
344
1097
|
|
|
345
|
-
def resolve(method, value)
|
|
1098
|
+
def resolve(method, value, kind)
|
|
346
1099
|
raise Error, "resolve value must be an object" unless value.is_a?(Hash)
|
|
347
1100
|
|
|
348
|
-
|
|
1101
|
+
checked_request(method, value, kind)
|
|
349
1102
|
end
|
|
350
1103
|
|
|
351
1104
|
def notify_open(document)
|
|
352
|
-
|
|
1105
|
+
state, epoch, transport = @lock.synchronize { [@state, @epoch, @transport] }
|
|
1106
|
+
send_notification("textDocument/didOpen", {textDocument: {uri: document.uri, languageId: document.language_id, version: document.version, text: document.text}}, state: state, epoch: epoch, transport: transport)
|
|
353
1107
|
end
|
|
354
1108
|
|
|
355
1109
|
def sync_mode
|
|
@@ -362,9 +1116,12 @@ module Sadr
|
|
|
362
1116
|
sync.is_a?(Hash) ? sync["openClose"] : sync.is_a?(Integer) && sync.positive?
|
|
363
1117
|
end
|
|
364
1118
|
|
|
365
|
-
def cancel(id)
|
|
366
|
-
@lock.synchronize
|
|
367
|
-
|
|
1119
|
+
def cancel(id, transport, epoch)
|
|
1120
|
+
target = @lock.synchronize do
|
|
1121
|
+
@pending.delete(id)
|
|
1122
|
+
connected_transport(state: nil, epoch: epoch, transport: transport, allow_closing: false)
|
|
1123
|
+
end
|
|
1124
|
+
target&.try_write(jsonrpc: "2.0", method: "$/cancelRequest", params: {id: id})
|
|
368
1125
|
rescue Error
|
|
369
1126
|
nil
|
|
370
1127
|
end
|
|
@@ -375,7 +1132,7 @@ module Sadr
|
|
|
375
1132
|
@pending.clear
|
|
376
1133
|
values
|
|
377
1134
|
end
|
|
378
|
-
pending.each { |future| future.fulfill(error: error) }
|
|
1135
|
+
pending.each { |future, _| future.fulfill(error: error) }
|
|
379
1136
|
end
|
|
380
1137
|
|
|
381
1138
|
def report_error(error)
|
|
@@ -396,13 +1153,13 @@ module Sadr
|
|
|
396
1153
|
end
|
|
397
1154
|
|
|
398
1155
|
def receive(message, error, epoch = @epoch)
|
|
399
|
-
return unless epoch == @epoch
|
|
1156
|
+
return unless @lock.synchronize { epoch == @epoch }
|
|
400
1157
|
|
|
401
1158
|
if error
|
|
402
|
-
receive_error(error)
|
|
1159
|
+
receive_error(error, epoch)
|
|
403
1160
|
elsif message.key?("id") && !message.key?("method")
|
|
404
|
-
|
|
405
|
-
|
|
1161
|
+
entry = @lock.synchronize { @pending.delete(message["id"]) }
|
|
1162
|
+
fulfill_response(entry, message)
|
|
406
1163
|
elsif message["method"]
|
|
407
1164
|
receive_call(message, epoch)
|
|
408
1165
|
end
|
|
@@ -413,19 +1170,38 @@ module Sadr
|
|
|
413
1170
|
end
|
|
414
1171
|
end
|
|
415
1172
|
|
|
416
|
-
def receive_error(error)
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
1173
|
+
def receive_error(error, epoch)
|
|
1174
|
+
pending, restart_epoch = @lock.synchronize do
|
|
1175
|
+
next unless epoch == @epoch
|
|
1176
|
+
|
|
1177
|
+
running = @state == :running && !@closing
|
|
1178
|
+
@state = :failed unless @closing
|
|
1179
|
+
values = @pending.values
|
|
1180
|
+
@pending.clear
|
|
1181
|
+
[values, (@epoch if running && @restart && @restarts < 3)]
|
|
1182
|
+
end
|
|
1183
|
+
return unless pending
|
|
1184
|
+
|
|
1185
|
+
pending.each { |future, _| future.fulfill(error: Error.new(error.message)) }
|
|
420
1186
|
report_error(error)
|
|
421
|
-
restart_server if
|
|
1187
|
+
restart_server(restart_epoch) if restart_epoch
|
|
422
1188
|
end
|
|
423
1189
|
|
|
424
1190
|
def receive_call(message, epoch)
|
|
425
1191
|
method = message["method"]
|
|
426
1192
|
params = message.fetch("params", {})
|
|
1193
|
+
if method == "workspace/semanticTokens/refresh"
|
|
1194
|
+
active = @lock.synchronize do
|
|
1195
|
+
next false unless epoch == @epoch && !@closing
|
|
1196
|
+
|
|
1197
|
+
@semantic.clear
|
|
1198
|
+
@semantic_generation += 1
|
|
1199
|
+
true
|
|
1200
|
+
end
|
|
1201
|
+
return unless active
|
|
1202
|
+
end
|
|
427
1203
|
@dispatch.call do
|
|
428
|
-
next unless epoch == @epoch && !@closing
|
|
1204
|
+
next unless @lock.synchronize { epoch == @epoch && !@closing }
|
|
429
1205
|
|
|
430
1206
|
begin
|
|
431
1207
|
receive_diagnostics(params) if method == "textDocument/publishDiagnostics"
|
|
@@ -445,15 +1221,20 @@ module Sadr
|
|
|
445
1221
|
end
|
|
446
1222
|
|
|
447
1223
|
def receive_diagnostics(params)
|
|
448
|
-
valid = params.is_a?(Hash) && params["
|
|
1224
|
+
valid = params.is_a?(Hash) && params["diagnostics"].is_a?(Array)
|
|
449
1225
|
raise Error, "invalid diagnostics notification" unless valid
|
|
450
1226
|
|
|
451
|
-
|
|
1227
|
+
uri = valid_uri(params["uri"])
|
|
452
1228
|
version = params["version"]
|
|
453
|
-
|
|
454
|
-
|
|
1229
|
+
valid_version = version.nil? || (version.is_a?(Integer) && version.between?(-0x80000000, 0x7fffffff))
|
|
1230
|
+
raise Error, "invalid diagnostic version" unless valid_version
|
|
1231
|
+
diagnostics = Protocol.diagnostics(params["diagnostics"])
|
|
1232
|
+
@lock.synchronize do
|
|
1233
|
+
document = @documents[uri]
|
|
1234
|
+
return if document && version && version < document.version
|
|
455
1235
|
|
|
456
|
-
|
|
1236
|
+
@diagnostics[uri] = diagnostics
|
|
1237
|
+
end
|
|
457
1238
|
end
|
|
458
1239
|
|
|
459
1240
|
def built_in_request(method, params)
|
|
@@ -461,15 +1242,15 @@ module Sadr
|
|
|
461
1242
|
when "workspace/configuration"
|
|
462
1243
|
items = params.fetch("items")
|
|
463
1244
|
raise Error, "invalid configuration request" unless items.is_a?(Array)
|
|
1245
|
+
configuration = @configuration_lock.synchronize { @configuration }
|
|
464
1246
|
|
|
465
1247
|
[true, items.map do |item|
|
|
466
1248
|
section = item["section"]
|
|
467
|
-
section ?
|
|
1249
|
+
section && configuration.is_a?(Hash) ? configuration.dig(*section.split(".")) : (configuration unless section)
|
|
468
1250
|
end]
|
|
469
1251
|
when "workspace/workspaceFolders"
|
|
470
1252
|
[true, [{uri: Protocol.uri(@root), name: File.basename(@root)}]]
|
|
471
1253
|
when "window/workDoneProgress/create", "workspace/semanticTokens/refresh", "workspace/inlayHint/refresh", "workspace/codeLens/refresh", "workspace/diagnostic/refresh"
|
|
472
|
-
@semantic.clear if method == "workspace/semanticTokens/refresh"
|
|
473
1254
|
[true, nil]
|
|
474
1255
|
else
|
|
475
1256
|
[false, nil]
|
|
@@ -495,33 +1276,71 @@ module Sadr
|
|
|
495
1276
|
end
|
|
496
1277
|
|
|
497
1278
|
def reply(id, epoch, value: nil, error: nil)
|
|
498
|
-
|
|
1279
|
+
transport = @lock.synchronize do
|
|
1280
|
+
connected_transport(state: nil, epoch: epoch, transport: nil, allow_closing: false)
|
|
1281
|
+
end
|
|
1282
|
+
return unless transport
|
|
499
1283
|
|
|
500
1284
|
response = {jsonrpc: "2.0", id: id}
|
|
501
1285
|
error ? response[:error] = error : response[:result] = value
|
|
502
|
-
|
|
1286
|
+
transport.write(response)
|
|
503
1287
|
rescue StandardError => failure
|
|
504
1288
|
report_error(failure)
|
|
505
1289
|
end
|
|
506
1290
|
|
|
507
|
-
def restart_server
|
|
508
|
-
|
|
1291
|
+
def restart_server(epoch)
|
|
1292
|
+
@lock.synchronize do
|
|
1293
|
+
return unless @epoch == epoch && @state == :failed && !@closing
|
|
1294
|
+
return if @restart_thread&.alive?
|
|
1295
|
+
|
|
1296
|
+
previous = @transport
|
|
1297
|
+
@restart_thread = Thread.new { restart_loop(previous, epoch) }
|
|
1298
|
+
end
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1301
|
+
def restart_loop(previous, epoch)
|
|
1302
|
+
previous.close
|
|
1303
|
+
loop do
|
|
1304
|
+
attempt = @lock.synchronize do
|
|
1305
|
+
next if @closing || @state != :failed || @epoch != epoch || @restarts >= 3
|
|
509
1306
|
|
|
510
|
-
previous = @transport
|
|
511
|
-
@restart_thread = Thread.new do
|
|
512
|
-
previous.close
|
|
513
|
-
until @closing || @restarts >= 3
|
|
514
1307
|
@restarts += 1
|
|
515
|
-
|
|
516
|
-
|
|
1308
|
+
[@restarts, epoch]
|
|
1309
|
+
end
|
|
1310
|
+
break unless attempt
|
|
517
1311
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
1312
|
+
sleep(0.2 * attempt[0])
|
|
1313
|
+
begin
|
|
1314
|
+
connect(timeout: 10, state: :restarting, expected_epoch: attempt[1])
|
|
1315
|
+
@document_lock.synchronize do
|
|
1316
|
+
documents, send_open, connected_epoch = @lock.synchronize do
|
|
1317
|
+
raise Error, "language server restart was cancelled" if @closing || @state != :restarting
|
|
1318
|
+
|
|
1319
|
+
[@documents.values.dup, open_close?, @epoch]
|
|
1320
|
+
end
|
|
1321
|
+
documents.each { |document| notify_open(document) } if send_open
|
|
1322
|
+
@lock.synchronize do
|
|
1323
|
+
if @closing || @state != :restarting || @epoch != connected_epoch
|
|
1324
|
+
raise Error, "language server restart was cancelled"
|
|
1325
|
+
end
|
|
1326
|
+
@state = :running
|
|
1327
|
+
end
|
|
524
1328
|
end
|
|
1329
|
+
retry_epoch = @lock.synchronize do
|
|
1330
|
+
if @state == :failed && !@closing
|
|
1331
|
+
@epoch
|
|
1332
|
+
else
|
|
1333
|
+
@restart_thread = nil if @restart_thread == Thread.current
|
|
1334
|
+
nil
|
|
1335
|
+
end
|
|
1336
|
+
end
|
|
1337
|
+
break unless retry_epoch
|
|
1338
|
+
|
|
1339
|
+
epoch = retry_epoch
|
|
1340
|
+
rescue StandardError => failure
|
|
1341
|
+
report_error(failure)
|
|
1342
|
+
epoch = @lock.synchronize { @epoch if @state == :failed && !@closing }
|
|
1343
|
+
break unless epoch
|
|
525
1344
|
end
|
|
526
1345
|
end
|
|
527
1346
|
end
|