rubik 0.3.0 → 0.3.2

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.
@@ -49,7 +49,9 @@ module Rubik
49
49
  # `scope` is the enclosing declaration path (namespaces, then the
50
50
  # innermost class, outermost first); `member` marks a class body so
51
51
  # declarations are read as member declarations.
52
- def walk_children(node, symbols, scope, member:)
52
+ def walk_children(node, symbols, scope, member:, depth: 0)
53
+ return if depth > 50
54
+
53
55
  (0...node.named_child_count).each do |i|
54
56
  child = node.named_child(i)
55
57
  case child.kind
@@ -58,9 +60,9 @@ module Rubik
58
60
  when "type_definition"
59
61
  walk_typedef(child, symbols, scope)
60
62
  when *RECORD_KINDS.keys
61
- walk_record(child, symbols, scope)
63
+ walk_record(child, symbols, scope, depth: depth + 1)
62
64
  when "namespace_definition"
63
- walk_namespace(child, symbols, scope)
65
+ walk_namespace(child, symbols, scope, depth: depth + 1)
64
66
  when "function_definition"
65
67
  walk_function(child, symbols, scope, member: member)
66
68
  when "field_declaration", "declaration"
@@ -69,7 +71,7 @@ module Rubik
69
71
  # Plain recursion also unwraps template_declaration nodes (their
70
72
  # inner class/function is walked in place) and linkage
71
73
  # specifications.
72
- walk_children(child, symbols, scope, member: member)
74
+ walk_children(child, symbols, scope, member: member, depth: depth + 1)
73
75
  end
74
76
  end
75
77
  end
@@ -104,7 +106,7 @@ module Rubik
104
106
  )
105
107
  end
106
108
 
107
- def walk_record(node, symbols, scope)
109
+ def walk_record(node, symbols, scope, depth:)
108
110
  name_node = node.child_by_field_name("name")
109
111
  return unless name_node
110
112
 
@@ -122,16 +124,16 @@ module Rubik
122
124
  )
123
125
 
124
126
  body = node.child_by_field_name("body")
125
- walk_children(body, symbols, scope + [name], member: true) if body
127
+ walk_children(body, symbols, scope + [name], member: true, depth: depth + 1) if body
126
128
  end
127
129
 
128
130
  # Namespace names extend the qualification path; everything inside is
129
131
  # walked with the namespace prefix (app::run, app::Widget#run).
130
- def walk_namespace(node, symbols, scope)
132
+ def walk_namespace(node, symbols, scope, depth:)
131
133
  name_node = node.child_by_field_name("name")
132
134
  ns_scope = name_node ? scope + [text_for(name_node).strip] : scope
133
135
  body = node.child_by_field_name("body")
134
- walk_children(body, symbols, ns_scope, member: false) if body
136
+ walk_children(body, symbols, ns_scope, member: false, depth: depth + 1) if body
135
137
  end
136
138
 
137
139
  def walk_function(node, symbols, scope, member:)
@@ -16,7 +16,9 @@ module Rubik
16
16
 
17
17
  private
18
18
 
19
- def walk_children(node, symbols)
19
+ def walk_children(node, symbols, depth: 0)
20
+ return if depth > 50
21
+
20
22
  (0...node.named_child_count).each do |i|
21
23
  child = node.named_child(i)
22
24
  case child.kind
@@ -31,7 +33,7 @@ module Rubik
31
33
  when "import_declaration"
32
34
  walk_import(child, symbols)
33
35
  else
34
- walk_children(child, symbols)
36
+ walk_children(child, symbols, depth: depth + 1)
35
37
  end
36
38
  end
37
39
  end
@@ -16,7 +16,9 @@ module Rubik
16
16
 
17
17
  private
18
18
 
19
- def walk_children(node, symbols, impl_type)
19
+ def walk_children(node, symbols, impl_type, depth: 0)
20
+ return if depth > 50
21
+
20
22
  (0...node.named_child_count).each do |i|
21
23
  child = node.named_child(i)
22
24
  case child.kind
@@ -25,13 +27,13 @@ module Rubik
25
27
  when "enum_item"
26
28
  walk_enum(child, symbols)
27
29
  when "impl_item"
28
- walk_impl(child, symbols)
30
+ walk_impl(child, symbols, depth: depth + 1)
29
31
  when "function_item"
30
32
  walk_function(child, symbols, impl_type)
31
33
  when "use_declaration"
32
34
  walk_use(child, symbols)
33
35
  else
34
- walk_children(child, symbols, impl_type)
36
+ walk_children(child, symbols, impl_type, depth: depth + 1)
35
37
  end
36
38
  end
37
39
  end
@@ -64,12 +66,12 @@ module Rubik
64
66
  )
65
67
  end
66
68
 
67
- def walk_impl(node, symbols)
69
+ def walk_impl(node, symbols, depth: 0)
68
70
  type_node = node.child_by_field_name("type")
69
71
  impl_name = type_node ? text_for(type_node).strip : nil
70
72
 
71
73
  body = node.child_by_field_name("body")
72
- walk_children(body, symbols, impl_name) if body
74
+ walk_children(body, symbols, impl_name, depth: depth + 1) if body
73
75
  end
74
76
 
75
77
  def walk_function(node, symbols, impl_type)
@@ -110,13 +112,13 @@ module Rubik
110
112
  if n.kind == "call_expression"
111
113
  fn = n.child_by_field_name("function")
112
114
  if fn
113
- fn_text = text_for(fn).split("::").last&.strip
115
+ fn_text = text_for(fn).strip
114
116
  calls << fn_text if fn_text && !fn_text.empty?
115
117
  end
116
118
  elsif n.kind == "macro_invocation"
117
119
  macro = n.child_by_field_name("macro") || n.named_child(0)
118
120
  if macro
119
- macro_text = text_for(macro).sub(/!\z/, "").split("::").last&.strip
121
+ macro_text = text_for(macro).sub(/!\z/, "").strip
120
122
  calls << macro_text if macro_text && !macro_text.empty?
121
123
  end
122
124
  end
@@ -16,12 +16,14 @@ module Rubik
16
16
 
17
17
  private
18
18
 
19
- def walk_children(node, symbols, current_class)
19
+ def walk_children(node, symbols, current_class, depth: 0)
20
+ return if depth > 50
21
+
20
22
  (0...node.named_child_count).each do |i|
21
23
  child = node.named_child(i)
22
24
  case child.kind
23
25
  when "export_statement"
24
- walk_export(child, symbols, current_class)
26
+ walk_export(child, symbols, current_class, depth: depth + 1)
25
27
  when "interface_declaration"
26
28
  walk_interface(child, symbols)
27
29
  when "enum_declaration"
@@ -35,12 +37,12 @@ module Rubik
35
37
  when "import_statement"
36
38
  walk_import(child, symbols)
37
39
  else
38
- walk_children(child, symbols, current_class)
40
+ walk_children(child, symbols, current_class, depth: depth + 1)
39
41
  end
40
42
  end
41
43
  end
42
44
 
43
- def walk_export(node, symbols, current_class)
45
+ def walk_export(node, symbols, current_class, depth:)
44
46
  (0...node.named_child_count).each do |i|
45
47
  child = node.named_child(i)
46
48
  case child.kind
@@ -49,7 +51,7 @@ module Rubik
49
51
  when "class_declaration" then walk_class(child, symbols)
50
52
  when "function_declaration" then walk_function(child, symbols)
51
53
  when "lexical_declaration", "variable_declaration" then walk_variable_declaration(child, symbols)
52
- else walk_children(child, symbols, current_class)
54
+ else walk_children(child, symbols, current_class, depth: depth + 1)
53
55
  end
54
56
  end
55
57
  end
@@ -19,14 +19,50 @@ module Rubik
19
19
 
20
20
  private
21
21
 
22
- def walk_children(node, symbols)
22
+ # Zig represents a method receiver as the prefix of a SuffixExpr while
23
+ # FieldOrFnCall contains only the trailing `.method()` node. Preserve
24
+ # the full expression so unresolved receiver dispatch is not flattened
25
+ # into a call to every same-named function.
26
+ def extract_calls(node, call_kinds = ["call", "call_expression", "invocation"])
27
+ return super unless call_kinds.include?("FieldOrFnCall")
28
+
29
+ calls = []
30
+ stack = [node]
31
+ while (current = stack.pop)
32
+ if current.kind == "FieldOrFnCall"
33
+ args_node = (0...current.named_child_count).map { |i| current.named_child(i) }
34
+ .find { |child| child.kind == "FnCallArguments" }
35
+ if args_node
36
+ suffix = current.parent
37
+ start_byte = suffix&.kind == "SuffixExpr" ? suffix.start_byte : current.start_byte
38
+ fn_text = (@code.byteslice(start_byte...args_node.start_byte) || "").strip
39
+ calls << fn_text unless fn_text.empty?
40
+ end
41
+ elsif current.kind == "SuffixExpr" &&
42
+ current.named_child_count >= 2 &&
43
+ current.named_child(1)&.kind == "FnCallArguments"
44
+ fn_text = text_for(current.named_child(0)).strip
45
+ calls << fn_text unless fn_text.empty?
46
+ elsif call_kinds.include?(current.kind)
47
+ fn = current.child_by_field_name("function") || current.child_by_field_name("name") || current.named_child(0)
48
+ fn_text = text_for(fn).strip if fn
49
+ calls << fn_text if fn_text && !fn_text.empty?
50
+ end
51
+ (0...current.named_child_count).reverse_each { |i| stack.push(current.named_child(i)) }
52
+ end
53
+ calls.uniq
54
+ end
55
+
56
+ def walk_children(node, symbols, depth: 0)
57
+ return if depth > 50
58
+
23
59
  (0...node.named_child_count).each do |i|
24
60
  child = node.named_child(i)
25
61
  case child.kind
26
62
  when "Decl"
27
63
  walk_decl(child, symbols)
28
64
  else
29
- walk_children(child, symbols)
65
+ walk_children(child, symbols, depth: depth + 1)
30
66
  end
31
67
  end
32
68
  end
@@ -82,11 +118,12 @@ module Rubik
82
118
  # Locates the node that owns this Decl's function definition. Zig
83
119
  # exposes it under several kind names across grammar versions, so we
84
120
  # accept every one we know about rather than only "FnProto".
85
- def find_function(node)
121
+ def find_function(node, depth: 0)
122
+ return if depth > 50
86
123
  return node if FN_KINDS.include?(node.kind)
87
124
 
88
125
  (0...node.named_child_count).each do |i|
89
- found = find_function(node.named_child(i))
126
+ found = find_function(node.named_child(i), depth: depth + 1)
90
127
  return found if found
91
128
  end
92
129
  nil
@@ -94,11 +131,12 @@ module Rubik
94
131
 
95
132
  # Walks the subtree for the first identifier node — used as a fallback
96
133
  # when the grammar does not expose the function name as a named field.
97
- def find_identifier(node)
134
+ def find_identifier(node, depth: 0)
135
+ return if depth > 50
98
136
  return node if node.kind == "identifier"
99
137
 
100
138
  (0...node.named_child_count).each do |i|
101
- found = find_identifier(node.named_child(i))
139
+ found = find_identifier(node.named_child(i), depth: depth + 1)
102
140
  return found if found
103
141
  end
104
142
  nil
@@ -4,10 +4,20 @@ require "open3"
4
4
  require "json"
5
5
  require "uri"
6
6
  require_relative "../platform"
7
+ require_relative "../log"
7
8
 
8
9
  module Rubik
9
10
  module LSP
10
11
  class Client
12
+ class RequestError < StandardError
13
+ attr_reader :reason
14
+
15
+ def initialize(reason, message)
16
+ @reason = reason
17
+ super(message)
18
+ end
19
+ end
20
+
11
21
  SERVERS = {
12
22
  "go" => "gopls",
13
23
  "rust" => "rust-analyzer",
@@ -30,13 +40,14 @@ module Rubik
30
40
  @language = language.to_s.downcase
31
41
  @root_dir = Rubik::Platform.expand_path(root_dir)
32
42
  cmd = server_cmd || SERVERS[@language]
33
- if (cmd.to_s.start_with?("typeprof") || cmd.to_s.start_with?("typescript-language-server")) && !cmd.to_s.include?("--stdio")
43
+ if cmd.to_s.start_with?("typeprof", "typescript-language-server") && !cmd.to_s.include?("--stdio")
34
44
  cmd = "#{cmd} --stdio"
35
45
  elsif cmd.to_s.start_with?("solargraph") && !cmd.to_s.include?("stdio")
36
46
  cmd = "#{cmd} stdio"
37
47
  end
38
48
  @server_cmd = cmd
39
49
  @running = false
50
+ @crashed = false
40
51
  @req_id = 0
41
52
  @stdin = nil
42
53
  @stdout = nil
@@ -48,6 +59,7 @@ module Rubik
48
59
  @cv = ConditionVariable.new
49
60
  @responses = {}
50
61
  @pending_requests = {}
62
+ @reader_failure = nil
51
63
  @diagnostics = {}
52
64
  @open_documents = {}
53
65
  @version_counter = 0
@@ -72,6 +84,7 @@ module Rubik
72
84
  @req_id = 0
73
85
  @responses.clear
74
86
  @pending_requests.clear
87
+ @reader_failure = nil
75
88
  @open_documents.clear
76
89
  @diagnostics.clear
77
90
  @version_counter = 0
@@ -91,6 +104,7 @@ module Rubik
91
104
  @stdin.binmode
92
105
  @stdout.binmode
93
106
  @running = false
107
+ @crashed = false
94
108
  @reader_alive = false
95
109
 
96
110
  start_reader_thread
@@ -137,6 +151,10 @@ module Rubik
137
151
  true
138
152
  end
139
153
 
154
+ def crashed?
155
+ defined?(@crashed) && @crashed == true
156
+ end
157
+
140
158
  def reconnect!
141
159
  stop if @running
142
160
  start
@@ -153,6 +171,7 @@ module Rubik
153
171
  end
154
172
 
155
173
  @running = false
174
+ @crashed = false
156
175
  @reader_alive = false
157
176
  # Drain first: its thread may be blocked in gets on @stderr.
158
177
  @stderr_drain&.stop
@@ -189,9 +208,8 @@ module Rubik
189
208
  def definition(file_path, line, character)
190
209
  return { ok: false, error: "LSP server not running" } unless alive?
191
210
 
192
- abs_path = File.expand_path(file_path, @root_dir)
193
211
  res = send_request("textDocument/definition", {
194
- textDocument: { uri: Rubik::Platform.to_file_uri(abs_path) },
212
+ textDocument: { uri: document_uri(file_path) },
195
213
  position: { line: line, character: character }
196
214
  })
197
215
 
@@ -203,9 +221,8 @@ module Rubik
203
221
  def references(file_path, line, character)
204
222
  return { ok: false, error: "LSP server not running" } unless alive?
205
223
 
206
- abs_path = File.expand_path(file_path, @root_dir)
207
224
  res = send_request("textDocument/references", {
208
- textDocument: { uri: Rubik::Platform.to_file_uri(abs_path) },
225
+ textDocument: { uri: document_uri(file_path) },
209
226
  position: { line: line, character: character },
210
227
  context: { includeDeclaration: true }
211
228
  })
@@ -218,9 +235,8 @@ module Rubik
218
235
  def hover(file_path, line, character)
219
236
  return { ok: false, error: "LSP server not running" } unless alive?
220
237
 
221
- abs_path = File.expand_path(file_path, @root_dir)
222
238
  res = send_request("textDocument/hover", {
223
- textDocument: { uri: Rubik::Platform.to_file_uri(abs_path) },
239
+ textDocument: { uri: document_uri(file_path) },
224
240
  position: { line: line, character: character }
225
241
  })
226
242
 
@@ -232,7 +248,7 @@ module Rubik
232
248
  def did_open(file_path, content, language_id = @language)
233
249
  return false unless alive?
234
250
 
235
- abs_path = Rubik::Platform.normalize_path_raw(Rubik::Platform.expand_path(file_path, @root_dir))
251
+ abs_path = normalized_document_path(file_path)
236
252
  @mutex.synchronize do
237
253
  @version_counter += 1
238
254
  @open_documents[abs_path] = @version_counter
@@ -240,7 +256,7 @@ module Rubik
240
256
 
241
257
  send_notification("textDocument/didOpen", {
242
258
  textDocument: {
243
- uri: Rubik::Platform.to_file_uri(abs_path),
259
+ uri: document_uri(abs_path),
244
260
  languageId: language_id.to_s,
245
261
  version: @version_counter,
246
262
  text: content
@@ -254,7 +270,7 @@ module Rubik
254
270
  def did_change(file_path, content)
255
271
  return false unless alive?
256
272
 
257
- abs_path = Rubik::Platform.normalize_path_raw(Rubik::Platform.expand_path(file_path, @root_dir))
273
+ abs_path = normalized_document_path(file_path)
258
274
  version = @mutex.synchronize do
259
275
  @version_counter += 1
260
276
  @open_documents[abs_path] = @version_counter
@@ -266,7 +282,7 @@ module Rubik
266
282
 
267
283
  send_notification("textDocument/didChange", {
268
284
  textDocument: {
269
- uri: Rubik::Platform.to_file_uri(abs_path),
285
+ uri: document_uri(abs_path),
270
286
  version: version
271
287
  },
272
288
  contentChanges: [
@@ -281,11 +297,16 @@ module Rubik
281
297
  def did_close(file_path)
282
298
  return false unless @running
283
299
 
284
- abs_path = Rubik::Platform.normalize_path_raw(Rubik::Platform.expand_path(file_path, @root_dir))
285
- @mutex.synchronize { @open_documents.delete(abs_path) }
300
+ abs_path = normalized_document_path(file_path)
301
+ raw_abs = Rubik::Platform.expand_path(file_path, @root_dir)
302
+ @mutex.synchronize do
303
+ @open_documents.delete(abs_path)
304
+ @open_documents.delete(raw_abs)
305
+ @open_documents.delete_if { |k, _| Rubik::Platform.path_equal?(k, abs_path) }
306
+ end
286
307
 
287
308
  send_notification("textDocument/didClose", {
288
- textDocument: { uri: Rubik::Platform.to_file_uri(abs_path) }
309
+ textDocument: { uri: document_uri(abs_path) }
289
310
  })
290
311
  true
291
312
  rescue StandardError
@@ -295,18 +316,25 @@ module Rubik
295
316
  def document_open?(file_path)
296
317
  return false if file_path.nil? || file_path.to_s.strip.empty?
297
318
 
298
- abs_path = Rubik::Platform.normalize_path_raw(Rubik::Platform.expand_path(file_path, @root_dir))
299
- @mutex.synchronize { @open_documents.key?(abs_path) }
319
+ abs_path = normalized_document_path(file_path)
320
+ raw_abs = Rubik::Platform.expand_path(file_path, @root_dir)
321
+ @mutex.synchronize do
322
+ @open_documents.key?(abs_path) ||
323
+ @open_documents.key?(raw_abs) ||
324
+ @open_documents.keys.any? { |k| Rubik::Platform.path_equal?(k, abs_path) }
325
+ end
300
326
  rescue ArgumentError
301
327
  false
302
328
  end
303
329
 
304
330
  def diagnostics_for(file_path)
305
- abs_path = Rubik::Platform.normalize_path_raw(Rubik::Platform.expand_path(file_path, @root_dir))
331
+ abs_path = normalized_document_path(file_path)
332
+ raw_abs = Rubik::Platform.normalize_path_raw(Rubik::Platform.expand_path(file_path, @root_dir))
306
333
  root_norm = Rubik::Platform.normalize_path_raw(@root_dir)
307
- rel_path = abs_path.start_with?("#{root_norm}/") ? abs_path.delete_prefix("#{root_norm}/") : file_path.to_s.tr("\\", "/")
334
+ rel_path = raw_abs.start_with?("#{root_norm}/") ? raw_abs.delete_prefix("#{root_norm}/") : file_path.to_s.tr("\\", "/")
308
335
  @mutex.synchronize do
309
336
  @diagnostics[abs_path] ||
337
+ @diagnostics[raw_abs] ||
310
338
  @diagnostics[rel_path] ||
311
339
  @diagnostics[file_path] ||
312
340
  @diagnostics.find { |k, _| Rubik::Platform.path_equal?(k, abs_path) }&.last ||
@@ -347,23 +375,30 @@ module Rubik
347
375
  parsed = JSON.parse(body)
348
376
  handle_incoming_message(parsed)
349
377
  rescue StandardError => e
350
- warn "[LSP::Client] Failed to handle message: #{e.class}: #{e.message}"
378
+ Rubik::Log.warn(:lsp, "Failed to handle message: #{e.class}: #{e.message}")
351
379
  end
352
380
  rescue IOError, Errno::EPIPE, Errno::ECONNRESET
353
381
  break
354
382
  rescue StandardError => e
355
- warn "[LSP::Client] Reader loop error: #{e.class}: #{e.message}"
383
+ Rubik::Log.warn(:lsp, "Reader loop error: #{e.class}: #{e.message}")
356
384
  next
357
385
  end
358
386
  end
359
387
  ensure
360
- @reader_alive = false
361
- @running = false
388
+ @mutex.synchronize do
389
+ @crashed = true if @running
390
+ @reader_alive = false
391
+ @running = false
392
+ @reader_failure ||= "LSP response reader stopped before completing the request"
393
+ @cv.broadcast
394
+ end
362
395
  end
363
396
  end
364
397
 
365
398
  def handle_incoming_message(msg)
366
- if msg["id"]
399
+ if msg.key?("method") && msg.key?("id")
400
+ handle_server_request(msg)
401
+ elsif msg.key?("id")
367
402
  req_id = msg["id"]
368
403
  @mutex.synchronize do
369
404
  # Discard late response if request is neither pending nor already tracked
@@ -381,12 +416,14 @@ module Rubik
381
416
  params = msg["params"] || {}
382
417
  uri = params["uri"].to_s
383
418
  raw_path = Rubik::Platform.from_file_uri(uri)
419
+ norm_path = normalized_document_path(raw_path)
384
420
  abs_path = Rubik::Platform.normalize_path_raw(Rubik::Platform.expand_path(raw_path, @root_dir))
385
421
  root_norm = Rubik::Platform.normalize_path_raw(@root_dir)
386
422
  rel_path = abs_path.start_with?("#{root_norm}/") ? abs_path.delete_prefix("#{root_norm}/") : abs_path
387
423
  diags = params["diagnostics"] || []
388
424
 
389
425
  @mutex.synchronize do
426
+ @diagnostics[norm_path] = diags
390
427
  @diagnostics[abs_path] = diags
391
428
  @diagnostics[rel_path] = diags
392
429
  @diagnostics[raw_path] = diags
@@ -412,15 +449,28 @@ module Rubik
412
449
 
413
450
  def wait_for_response(req_id, timeout: 5)
414
451
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
415
- res = nil
416
452
  timed_out = false
453
+ reader_failure = nil
417
454
 
418
455
  @mutex.synchronize do
419
456
  loop do
420
457
  if @responses.key?(req_id)
421
- res = @responses.delete(req_id)["result"]
458
+ response = @responses.delete(req_id)
422
459
  @pending_requests.delete(req_id)
423
- return res
460
+ if response.key?("error")
461
+ error = response["error"] || {}
462
+ raise RequestError.new(:server_error, error["message"] || "LSP server returned an error")
463
+ end
464
+ return response["result"] if response.key?("result")
465
+
466
+ raise RequestError.new(:invalid_response, "LSP response did not contain a result")
467
+ end
468
+
469
+ if @reader_failure
470
+ reader_failure = @reader_failure
471
+ @responses.delete(req_id)
472
+ @pending_requests.delete(req_id)
473
+ break
424
474
  end
425
475
 
426
476
  remaining = timeout - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time)
@@ -442,11 +492,40 @@ module Rubik
442
492
  rescue StandardError
443
493
  nil
444
494
  end
495
+ raise RequestError.new(:timeout, "LSP request timed out after #{timeout} seconds")
445
496
  end
446
497
 
498
+ raise RequestError.new(:reader_failure, reader_failure) if reader_failure
499
+
447
500
  nil
448
501
  end
449
502
 
503
+ def normalized_document_path(file_path)
504
+ path = Rubik::Platform.expand_path(file_path, @root_dir)
505
+ Rubik::Platform.normalize_path(path)
506
+ end
507
+
508
+ def document_uri(file_path)
509
+ Rubik::Platform.to_file_uri(normalized_document_path(file_path))
510
+ end
511
+
512
+ def handle_server_request(msg)
513
+ case msg["method"]
514
+ when "workspace/configuration"
515
+ params = msg["params"]
516
+ items = params.is_a?(Hash) && params["items"].is_a?(Array) ? params["items"] : []
517
+ write_message({ jsonrpc: "2.0", id: msg["id"], result: items.map { {} } })
518
+ when "client/registerCapability", "client/unregisterCapability", "window/workDoneProgress/create"
519
+ write_message({ jsonrpc: "2.0", id: msg["id"], result: nil })
520
+ else
521
+ write_message({
522
+ jsonrpc: "2.0",
523
+ id: msg["id"],
524
+ error: { code: -32_601, message: "Method not found: #{msg['method']}" }
525
+ })
526
+ end
527
+ end
528
+
450
529
  def send_notification(method, params)
451
530
  msg = {
452
531
  jsonrpc: "2.0",