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.
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
- raise Error, "language server is already started" if %i[starting running].include?(@state)
75
+ connect(timeout: timeout, state: :starting)
76
+ @lock.synchronize do
77
+ raise Error, "language server closed during initialization" unless @state == :starting
40
78
 
41
- @closing = false
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
- result = request("initialize", initialize_params).await(timeout: timeout)
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
- @closing = true
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
- request("shutdown").await(timeout: 2) if @state == :running
68
- notify("exit") if @transport&.alive?
69
- rescue Error
70
- nil
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
- @epoch += 1
73
- @documents.clear
74
- @semantic.clear
75
- @diagnostics.clear
76
- @transport&.close
77
- fail_pending(Error.new("language server stopped"))
78
- @state = :stopped
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? = @state == :running && !!@transport&.alive?
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
- id = @lock.synchronize { @sequence += 1 }
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
- raise Error, "language server is not connected" unless @transport&.alive?
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: document.uri.dup.freeze, language_id: document.language_id.dup.freeze,
115
- version: document.version, text: document.text.dup.freeze)
116
- @documents[stored.uri] = stored
117
- notify_open(stored) if open_close?
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
- document = @documents.fetch(uri) { raise Error, "document is not open" }
123
- unless Protocol.uint?(version) && version > document.version
124
- raise Error, "document version must increase"
125
- end
126
- raise Error, "content changes must be a nonempty Array" unless changes.is_a?(Array) && !changes.empty?
127
-
128
- text = document.text
129
- wire_changes = changes.map do |change|
130
- validate_change(change)
131
- if change.range
132
- index = DocumentIndex.new(text)
133
- first = Protocol.offset(index, change.range.start)
134
- last = Protocol.offset(index, change.range.end)
135
- raise Error, "invalid content change range" if last < first
136
-
137
- text = text.byteslice(0, first) + change.text + text.byteslice(last, text.bytesize - last)
138
- {range: Protocol.range_hash(change.range), text: change.text}
139
- else
140
- text = change.text
141
- {text: change.text}
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
- document = @documents.fetch(uri) { raise Error, "document is not open" }
159
- sync = @capabilities["textDocumentSync"]
160
- save = sync.is_a?(Hash) ? sync["save"] : sync.is_a?(Integer) && sync.positive?
161
- return unless save
162
-
163
- if text
164
- raise Error, "saved text must be valid UTF-8" unless text.is_a?(String) && text.valid_encoding?
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
- raise Error, "document is not open" unless @documents.delete(uri)
175
-
176
- @diagnostics.delete(uri)
177
- @semantic.delete(uri)
178
- notify("textDocument/didClose", {textDocument: {uri: uri}}) if open_close?
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
- raise Error, "new name must be a String" unless new_name.is_a?(String) && new_name.valid_encoding?
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
- raise Error, "previous result id must be a String" unless previous_result_id.is_a?(String)
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 = @documents[uri]
226
- return [] unless document && document.version == version
227
-
228
- provider = @capabilities["semanticTokensProvider"]
229
- return [] unless provider.is_a?(Hash) && provider["full"]
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 = request(method, params).await
237
- current = @documents[uri]
238
- return [] unless result && current && current.version == version
348
+ result = checked_request(method, params, :semantic).await
239
349
 
240
- valid = result.is_a?(Hash) && (!result.key?("resultId") || result["resultId"].is_a?(String))
241
- raise Error, "invalid semantic token result" unless valid
242
- raise Error, "unexpected semantic token delta" if !delta && !result.key?("data")
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
- data = result["data"] || Protocol.semantic_delta(previous[1], result["edits"])
245
- tokens = Protocol.semantic_tokens(data, legend: provider["legend"])
246
- @semantic[uri] = [result["resultId"], data]
247
- tokens
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
- raise Error, "query must be a String" unless query.is_a?(String)
366
+ query = utf8_string(query, "query must be a valid UTF-8 String")
252
367
 
253
- request("workspace/symbol", {query: query})
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
- raise Error, "command must be a nonempty String" unless command.is_a?(String) && !command.empty?
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, workspaceEdit: {documentChanges: true, resourceOperations: %w[create rename delete], failureHandling: "abort"}}
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
- unless document.language_id.is_a?(String) && !document.language_id.empty?
315
- raise Error, "language id must be a nonempty String"
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
- unless document.text.is_a?(String) && document.text.valid_encoding?
319
- raise Error, "document text must be valid UTF-8"
320
- end
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
- raise Error, "URI must be a nonempty String" unless uri.is_a?(String) && !uri.empty? && !uri.include?("\0") && uri.valid_encoding?
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
- request("textDocument/#{method}", params.merge(core))
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
- request("textDocument/#{method}", params.merge(textDocument: {uri: valid_uri(uri)}))
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
- request(method, value)
1101
+ checked_request(method, value, kind)
349
1102
  end
350
1103
 
351
1104
  def notify_open(document)
352
- notify("textDocument/didOpen", {textDocument: {uri: document.uri, languageId: document.language_id, version: document.version, text: document.text}})
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 { @pending.delete(id) }
367
- notify("$/cancelRequest", {id: id})
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
- future = @lock.synchronize { @pending.delete(message["id"]) }
405
- future&.fulfill(message["result"], error: message["error"] && ServerError.new(message["error"]))
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
- running = @state == :running
418
- @state = :failed
419
- fail_pending(Error.new(error.message))
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 running && @restart && !@closing && @restarts < 3
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["uri"].is_a?(String) && params["diagnostics"].is_a?(Array)
1224
+ valid = params.is_a?(Hash) && params["diagnostics"].is_a?(Array)
449
1225
  raise Error, "invalid diagnostics notification" unless valid
450
1226
 
451
- document = @documents[params["uri"]]
1227
+ uri = valid_uri(params["uri"])
452
1228
  version = params["version"]
453
- raise Error, "invalid diagnostic version" if !version.nil? && !version.is_a?(Integer)
454
- return if document && version && version < document.version
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
- @diagnostics[params["uri"]] = Protocol.diagnostics(params["diagnostics"])
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 ? @configuration.dig(*section.split(".")) : @configuration
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
- return unless epoch == @epoch && !@closing
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
- @transport.write(response)
1286
+ transport.write(response)
503
1287
  rescue StandardError => failure
504
1288
  report_error(failure)
505
1289
  end
506
1290
 
507
- def restart_server
508
- return if @restart_thread&.alive?
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
- sleep(0.2 * @restarts)
516
- break if @closing
1308
+ [@restarts, epoch]
1309
+ end
1310
+ break unless attempt
517
1311
 
518
- begin
519
- start
520
- @documents.values.dup.each { |document| notify_open(document) if open_close? }
521
- break
522
- rescue StandardError => failure
523
- report_error(failure)
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