1use std::collections::HashMap;
13use std::io::{BufRead, BufReader, Read, Write};
14use std::path::{Path, PathBuf};
15use std::process::{Child, ChildStdin, Command, Stdio};
16use std::sync::mpsc::{self, Receiver, TryRecvError};
17use std::thread;
18
19use crate::config;
20use crate::highlight::{self, TokenKind};
21
22pub type SemanticToken = (TokenKind, usize, usize, usize);
24
25pub struct LspClient {
26 stdin: Option<ChildStdin>,
27 rx: Option<Receiver<RawMsg>>,
28 _child: Option<Child>,
29 next_id: u64,
30 pending: HashMap<u64, PendingReq>,
32 doc_version: i64,
33 pub diagnostics: Vec<Diagnostic>,
34 pub server_running: bool,
35 pub server_name: String,
36 pub server_lang: String,
37 pub initialized: bool,
38 pending_didopen: Option<(String, String, String)>, pub pending_definition: Option<Location>,
40 pub definition_as_peek: bool,
42 pub pending_completions: Vec<CompletionItem>,
43 pub pending_hover: Option<String>,
44 pub pending_references: Vec<Location>,
45 pub pending_workspace_edit: Option<String>,
47 pub pending_edits: Vec<FileEdit>,
49 pub pending_symbols: Vec<SymbolItem>,
50 pub pending_code_actions: Vec<CodeActionItem>,
51 pub pending_call_hierarchy: Vec<crate::call_hierarchy::CallItem>,
53 pub pending_call_direction: Option<crate::call_hierarchy::CallDirection>,
55 pub call_hierarchy_ready: bool,
56 pub inlay_hints: Vec<InlayHint>,
57 pub inlay_supported: bool,
58 inlay_dirty: bool,
59 pub code_lenses: Vec<CodeLens>,
60 pub code_lens_supported: bool,
61 code_lens_dirty: bool,
62 code_lens_gen: u64,
64 pub error: Option<String>,
66 pub soft_error: Option<String>,
68 pub stderr_tail: String,
70 current_uri: String,
71 opened_uri: String,
73 root_uri: String,
74 semantic_token_types: Vec<String>,
76 pub semantic_tokens_supported: bool,
77 pub semantic_tokens: Vec<SemanticToken>,
79 semantic_doc_text: String,
81 semantic_dirty: bool,
83 last_semantic_req_version: i64,
84 pub enabled: bool,
86 pub server_overrides: HashMap<String, String>,
88}
89
90#[derive(Debug, Clone)]
91pub struct SymbolItem {
92 pub name: String,
93 pub kind: String,
94 pub path: String,
95 pub row: usize,
96 pub col: usize,
97 pub detail: String,
98}
99
100#[derive(Debug, Clone)]
101pub struct InlayHint {
102 pub row: usize,
103 pub col: usize, pub label: String,
105}
106
107#[derive(Debug, Clone)]
109pub struct CodeLens {
110 pub row: usize,
111 pub col: usize,
112 pub title: String,
113}
114
115#[derive(Debug, Clone)]
117pub struct FileEdit {
118 pub path: String,
119 pub text: String,
120}
121
122#[derive(Debug, Clone)]
124pub struct CodeActionItem {
125 pub title: String,
126 pub kind: String,
127 pub edits: Vec<FileEdit>,
128 pub command: Option<String>,
130 pub command_args_json: Option<String>,
131}
132
133#[derive(Debug, Clone, Copy)]
134enum PendingReq {
135 Definition,
136 Completion,
137 Hover,
138 References,
139 Rename,
140 Initialize,
141 SemanticTokens,
142 DocumentSymbol,
143 WorkspaceSymbol,
144 InlayHint,
145 Formatting,
146 CodeAction,
147 ExecuteCommand,
148 PrepareCallHierarchy,
149 IncomingCalls,
150 OutgoingCalls,
151 CodeLens,
152 CodeLensResolve(u64),
155}
156
157struct RawMsg {
158 id: Option<u64>,
159 method: Option<String>,
160 body: String,
161}
162
163#[derive(Debug, Clone)]
164pub struct Diagnostic {
165 pub row: usize,
166 pub col_start: usize,
167 pub col_end: usize,
168 pub message: String,
169 pub severity: DiagnosticSeverity,
170}
171
172#[derive(Debug, Clone, PartialEq)]
173pub enum DiagnosticSeverity {
174 Error,
175 Warning,
176 Info,
177 Hint,
178}
179
180#[derive(Debug, Clone)]
181pub struct Location {
182 pub path: String,
183 pub row: usize,
184 pub col: usize,
185}
186
187#[derive(Debug, Clone)]
188pub struct CompletionItem {
189 pub label: String,
190 pub detail: Option<String>,
191}
192
193impl Default for LspClient {
194 fn default() -> Self {
195 Self {
196 stdin: None,
197 rx: None,
198 _child: None,
199 next_id: 1,
200 pending: HashMap::new(),
201 doc_version: 1,
202 diagnostics: Vec::new(),
203 server_running: false,
204 server_name: String::new(),
205 server_lang: String::new(),
206 initialized: false,
207 pending_didopen: None,
208 pending_definition: None,
209 definition_as_peek: false,
210 pending_completions: Vec::new(),
211 pending_hover: None,
212 pending_references: Vec::new(),
213 pending_workspace_edit: None,
214 pending_edits: Vec::new(),
215 pending_symbols: Vec::new(),
216 pending_code_actions: Vec::new(),
217 pending_call_hierarchy: Vec::new(),
218 pending_call_direction: None,
219 call_hierarchy_ready: false,
220 inlay_hints: Vec::new(),
221 inlay_supported: false,
222 inlay_dirty: false,
223 code_lenses: Vec::new(),
224 code_lens_supported: true, code_lens_dirty: false,
226 code_lens_gen: 0,
227 error: None,
228 soft_error: None,
229 stderr_tail: String::new(),
230 current_uri: String::new(),
231 opened_uri: String::new(),
232 root_uri: String::new(),
233 semantic_token_types: Vec::new(),
234 semantic_tokens_supported: false,
235 semantic_tokens: Vec::new(),
236 semantic_doc_text: String::new(),
237 semantic_dirty: false,
238 last_semantic_req_version: 0,
239 enabled: true,
240 server_overrides: HashMap::new(),
241 }
242 }
243}
244
245impl LspClient {
246 pub fn new() -> Self {
247 Self::default()
248 }
249
250 pub fn start(&mut self, cmd: &str, root: &str, file_path: &str) {
251 self.start_with_text(cmd, root, file_path, None);
252 }
253
254 pub fn start_with_text(
255 &mut self,
256 cmd: &str,
257 root: &str,
258 file_path: &str,
259 text_override: Option<&str>,
260 ) {
261 self.shutdown_quiet();
262
263 let parts: Vec<&str> = cmd.split_whitespace().collect();
264 if parts.is_empty() {
265 return;
266 }
267
268 if !command_exists(parts[0]) {
269 self.soft_error = Some(install_hint(parts[0]));
271 self.error = None;
272 self.server_running = false;
273 return;
274 }
275
276 let abs_file = abs_path(file_path);
277 let abs_root = abs_path(root);
278 let root = find_project_root(&abs_root, &abs_file);
279
280 let mut child = match Command::new(parts[0])
281 .args(&parts[1..])
282 .current_dir(&root)
283 .stdin(Stdio::piped())
284 .stdout(Stdio::piped())
285 .stderr(Stdio::null())
286 .spawn()
287 {
288 Ok(c) => c,
289 Err(e) => {
290 self.error = Some(format!("LSP failed to start `{}`: {}", parts[0], e));
291 return;
292 }
293 };
294
295 let stdin = match child.stdin.take() {
296 Some(s) => s,
297 None => {
298 self.error = Some("LSP stdin unavailable".into());
299 return;
300 }
301 };
302 let stdout = match child.stdout.take() {
303 Some(s) => s,
304 None => {
305 self.error = Some("LSP stdout unavailable".into());
306 return;
307 }
308 };
309
310 let (tx, rx) = mpsc::channel();
311 thread::spawn(move || read_loop(stdout, tx));
312
313 self.stdin = Some(stdin);
314 self.rx = Some(rx);
315 self._child = Some(child);
316 self.server_name = parts[0].to_string();
317 self.current_uri = path_to_uri(&abs_file);
318 self.root_uri = path_to_uri(&root);
319 self.doc_version = 1;
320 self.initialized = false;
321 self.server_running = false; self.error = None;
323 self.soft_error = None;
324 self.stderr_tail.clear();
325 self.diagnostics.clear();
326 self.pending.clear();
327 self.opened_uri.clear();
328
329 let id = self.alloc_id(PendingReq::Initialize);
330 let pid = std::process::id();
331 let folder_name = Path::new(&root)
332 .file_name()
333 .and_then(|n| n.to_str())
334 .unwrap_or("workspace");
335 let init = build_initialize_request(
338 id,
339 pid,
340 &self.root_uri,
341 folder_name,
342 );
343 self.send_raw(&init);
344
345 let text = text_override
346 .map(|s| s.to_string())
347 .unwrap_or_else(|| std::fs::read_to_string(&abs_file).unwrap_or_default());
348 let lang = lang_id(&abs_file);
349 self.semantic_doc_text = text.clone();
350 self.pending_didopen = Some((
351 abs_file.clone(),
352 lang.to_string(),
353 escape_json(&text),
354 ));
355 }
356
357 pub fn auto_start(&mut self, file_path: &str) {
358 self.auto_start_with_text(file_path, None);
359 }
360
361 pub fn apply_config(&mut self, enabled: bool, overrides: HashMap<String, String>) {
363 self.enabled = enabled;
364 self.server_overrides = overrides;
365 if !enabled && (self.server_running || self.stdin.is_some()) {
366 self.shutdown_quiet();
367 self.soft_error = Some("LSP disabled in settings".into());
368 }
369 }
370
371 pub fn resolve_server_cmd(&self, ext: &str) -> Option<String> {
373 if !self.enabled {
374 return None;
375 }
376 let lang = ext_to_lang_key(ext)?;
377 if let Some(cmd) = self.server_overrides.get(lang) {
378 if cmd.is_empty() {
379 return None; }
381 return Some(cmd.clone());
382 }
383 default_server_for_ext(ext).map(|s| s.to_string())
384 }
385
386 pub fn auto_start_with_text(&mut self, file_path: &str, text: Option<&str>) {
388 let abs = abs_path(file_path);
389 let ext = Path::new(&abs)
390 .extension()
391 .and_then(|e| e.to_str())
392 .unwrap_or("")
393 .to_lowercase();
394
395 let Some(cmd) = self.resolve_server_cmd(&ext) else {
396 self.diagnostics.clear();
398 self.semantic_tokens.clear();
399 self.inlay_hints.clear();
400 if !self.server_running {
402 self.error = None;
403 }
404 return;
405 };
406
407 if self.server_running && self.server_lang == ext {
409 self.open_document_with_text(&abs, text);
410 return;
411 }
412
413 if self.server_running || self.stdin.is_some() {
414 self.shutdown_quiet();
415 }
416
417 let root = Path::new(&abs)
418 .parent()
419 .map(|p| p.display().to_string())
420 .unwrap_or_else(|| ".".into());
421 self.start_with_text(&cmd, &root, &abs, text);
422 self.server_lang = ext;
423 }
424
425 pub fn open_document(&mut self, file_path: &str) {
427 self.open_document_with_text(file_path, None);
428 }
429
430 pub fn open_document_with_text(&mut self, file_path: &str, text: Option<&str>) {
432 if !self.server_running {
433 return;
434 }
435 let abs = abs_path(file_path);
436 let new_uri = path_to_uri(&abs);
437 if !self.opened_uri.is_empty() && self.opened_uri != new_uri {
439 let close = format!(
440 r#"{{"jsonrpc":"2.0","method":"textDocument/didClose","params":{{"textDocument":{{"uri":"{}"}}}}}}"#,
441 escape_json(&self.opened_uri)
442 );
443 self.send_raw(&close);
444 }
445 let text = text
446 .map(|s| s.to_string())
447 .unwrap_or_else(|| std::fs::read_to_string(&abs).unwrap_or_default());
448 let lang = lang_id(&abs);
449 self.current_uri = new_uri.clone();
450 self.opened_uri = new_uri;
451 self.doc_version = 1;
452 self.diagnostics.clear();
453 self.semantic_tokens.clear();
454 self.inlay_hints.clear();
455 self.code_lenses.clear();
456 self.semantic_doc_text = text.clone();
457 let msg = format!(
458 r#"{{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{{"textDocument":{{"uri":"{}","languageId":"{}","version":{},"text":"{}"}}}}}}"#,
459 escape_json(&self.current_uri),
460 lang,
461 self.doc_version,
462 escape_json(&text)
463 );
464 self.send_raw(&msg);
465 self.semantic_dirty = true;
466 self.maybe_request_semantic_tokens();
467 self.inlay_dirty = true;
468 self.code_lens_dirty = true;
469 }
470
471 pub fn notify_change(&mut self, path: &str, text: &str) {
472 if !self.server_running {
473 return;
474 }
475 let uri = path_to_uri(&abs_path(path));
476 self.current_uri = uri.clone();
477 self.doc_version = self.doc_version.saturating_add(1);
478 self.semantic_doc_text = text.to_string();
479 let msg = format!(
480 r#"{{"jsonrpc":"2.0","method":"textDocument/didChange","params":{{"textDocument":{{"uri":"{}","version":{}}},"contentChanges":[{{"text":"{}"}}]}}}}"#,
481 escape_json(&uri),
482 self.doc_version,
483 escape_json(text)
484 );
485 self.send_raw(&msg);
486 self.semantic_dirty = true;
487 self.maybe_request_semantic_tokens();
488 self.inlay_dirty = true;
489 self.code_lens_dirty = true;
490 }
491
492 pub fn request_semantic_tokens(&mut self) {
494 if !self.server_running || !self.semantic_tokens_supported {
495 return;
496 }
497 if self
499 .pending
500 .values()
501 .any(|k| matches!(k, PendingReq::SemanticTokens))
502 {
503 self.semantic_dirty = true;
504 return;
505 }
506 let id = self.alloc_id(PendingReq::SemanticTokens);
507 self.last_semantic_req_version = self.doc_version;
508 self.semantic_dirty = false;
509 let msg = format!(
510 r#"{{"jsonrpc":"2.0","id":{},"method":"textDocument/semanticTokens/full","params":{{"textDocument":{{"uri":"{}"}}}}}}"#,
511 id,
512 escape_json(&self.current_uri)
513 );
514 self.send_raw(&msg);
515 }
516
517 fn maybe_request_semantic_tokens(&mut self) {
518 if self.semantic_dirty {
519 self.request_semantic_tokens();
520 }
521 }
522
523 pub fn request_definition(&mut self, path: &str, row: usize, col: usize) {
524 self.definition_as_peek = false;
525 self.request_position(PendingReq::Definition, "textDocument/definition", path, row, col);
526 }
527
528 pub fn request_peek_definition(&mut self, path: &str, row: usize, col: usize) {
529 self.definition_as_peek = true;
530 self.request_position(PendingReq::Definition, "textDocument/definition", path, row, col);
531 }
532
533 pub fn request_document_symbols(&mut self, path: &str) {
534 if !self.server_running {
535 return;
536 }
537 let uri = path_to_uri(&abs_path(path));
538 let id = self.alloc_id(PendingReq::DocumentSymbol);
539 let msg = format!(
540 r#"{{"jsonrpc":"2.0","id":{},"method":"textDocument/documentSymbol","params":{{"textDocument":{{"uri":"{}"}}}}}}"#,
541 id,
542 escape_json(&uri)
543 );
544 self.send_raw(&msg);
545 }
546
547 pub fn request_workspace_symbols(&mut self, query: &str) {
548 if !self.server_running {
549 return;
550 }
551 let id = self.alloc_id(PendingReq::WorkspaceSymbol);
552 let msg = format!(
553 r#"{{"jsonrpc":"2.0","id":{},"method":"workspace/symbol","params":{{"query":"{}"}}}}"#,
554 id,
555 escape_json(query)
556 );
557 self.send_raw(&msg);
558 }
559
560 pub fn request_inlay_hints(&mut self, path: &str, end_row: usize) {
561 if !self.server_running || !self.inlay_supported {
562 return;
563 }
564 let uri = path_to_uri(&abs_path(path));
565 let id = self.alloc_id(PendingReq::InlayHint);
566 let msg = format!(
567 r#"{{"jsonrpc":"2.0","id":{},"method":"textDocument/inlayHint","params":{{"textDocument":{{"uri":"{}"}},"range":{{"start":{{"line":0,"character":0}},"end":{{"line":{},"character":0}}}}}}}}"#,
568 id,
569 escape_json(&uri),
570 end_row.saturating_add(1)
571 );
572 self.send_raw(&msg);
573 self.inlay_dirty = false;
574 }
575
576 pub fn mark_inlay_dirty(&mut self) {
577 self.inlay_dirty = true;
578 }
579
580 pub fn maybe_request_inlays(&mut self, path: &str, end_row: usize) {
581 if self.inlay_dirty && self.inlay_supported && self.server_running {
582 self.request_inlay_hints(path, end_row);
583 }
584 }
585
586 pub fn request_code_lens(&mut self, path: &str) {
587 if !self.server_running || !self.code_lens_supported {
588 return;
589 }
590 if self
592 .pending
593 .values()
594 .any(|k| matches!(k, PendingReq::CodeLens))
595 {
596 return;
597 }
598 let uri = path_to_uri(&abs_path(path));
599 let id = self.alloc_id(PendingReq::CodeLens);
600 let msg = format!(
601 r#"{{"jsonrpc":"2.0","id":{},"method":"textDocument/codeLens","params":{{"textDocument":{{"uri":"{}"}}}}}}"#,
602 id,
603 escape_json(&uri)
604 );
605 self.send_raw(&msg);
606 self.code_lens_dirty = false;
607 }
608
609 pub fn mark_code_lens_dirty(&mut self) {
610 self.code_lens_dirty = true;
611 }
612
613 pub fn maybe_request_code_lens(&mut self, path: &str) {
614 if self.code_lens_dirty && self.code_lens_supported && self.server_running {
615 self.request_code_lens(path);
616 }
617 }
618
619 pub fn request_completion(&mut self, path: &str, row: usize, col: usize) {
620 self.request_position(PendingReq::Completion, "textDocument/completion", path, row, col);
621 }
622
623 pub fn request_hover(&mut self, path: &str, row: usize, col: usize) {
624 self.request_position(PendingReq::Hover, "textDocument/hover", path, row, col);
625 }
626
627 pub fn request_references(&mut self, path: &str, row: usize, col: usize) {
628 if !self.server_running {
629 return;
630 }
631 let uri = path_to_uri(&abs_path(path));
632 let col16 = self.char_col_to_utf16(row, col);
633 let id = self.alloc_id(PendingReq::References);
634 let msg = format!(
635 r#"{{"jsonrpc":"2.0","id":{},"method":"textDocument/references","params":{{"textDocument":{{"uri":"{}"}},"position":{{"line":{},"character":{}}},"context":{{"includeDeclaration":true}}}}}}"#,
636 id,
637 escape_json(&uri),
638 row,
639 col16
640 );
641 self.send_raw(&msg);
642 }
643
644 pub fn request_rename(&mut self, path: &str, row: usize, col: usize, new_name: &str) {
645 if !self.server_running {
646 return;
647 }
648 let uri = path_to_uri(&abs_path(path));
649 let col16 = self.char_col_to_utf16(row, col);
650 let id = self.alloc_id(PendingReq::Rename);
651 let msg = format!(
652 r#"{{"jsonrpc":"2.0","id":{},"method":"textDocument/rename","params":{{"textDocument":{{"uri":"{}"}},"position":{{"line":{},"character":{}}},"newName":"{}"}}}}"#,
653 id,
654 escape_json(&uri),
655 row,
656 col16,
657 escape_json(new_name)
658 );
659 self.send_raw(&msg);
660 }
661
662 pub fn request_formatting(&mut self, path: &str) {
664 if !self.server_running {
665 return;
666 }
667 let uri = path_to_uri(&abs_path(path));
668 let id = self.alloc_id(PendingReq::Formatting);
669 let msg = format!(
670 r#"{{"jsonrpc":"2.0","id":{},"method":"textDocument/formatting","params":{{"textDocument":{{"uri":"{}"}},"options":{{"tabSize":4,"insertSpaces":true}}}}}}"#,
671 id,
672 escape_json(&uri)
673 );
674 self.send_raw(&msg);
675 }
676
677 pub fn request_call_hierarchy(
680 &mut self,
681 path: &str,
682 row: usize,
683 col: usize,
684 direction: crate::call_hierarchy::CallDirection,
685 ) {
686 if !self.server_running {
687 self.soft_error = Some("LSP not running".into());
688 return;
689 }
690 self.pending_call_hierarchy.clear();
691 self.call_hierarchy_ready = false;
692 self.pending_call_direction = Some(direction);
693 let abs = abs_path(path);
694 let uri = path_to_uri(&abs);
695 let col16 = self.char_col_to_utf16(row, col);
696 let id = self.alloc_id(PendingReq::PrepareCallHierarchy);
697 let msg = format!(
698 r#"{{"jsonrpc":"2.0","id":{},"method":"textDocument/prepareCallHierarchy","params":{{"textDocument":{{"uri":"{}"}},"position":{{"line":{},"character":{}}}}}}}"#,
699 id,
700 escape_json(&uri),
701 row,
702 col16
703 );
704 self.send_raw(&msg);
705 }
706
707 fn request_calls_for_item(
708 &mut self,
709 item_json: &str,
710 direction: crate::call_hierarchy::CallDirection,
711 ) {
712 let (method, kind) = match direction {
713 crate::call_hierarchy::CallDirection::Incoming => {
714 ("callHierarchy/incomingCalls", PendingReq::IncomingCalls)
715 }
716 crate::call_hierarchy::CallDirection::Outgoing => {
717 ("callHierarchy/outgoingCalls", PendingReq::OutgoingCalls)
718 }
719 };
720 let id = self.alloc_id(kind);
721 let msg = format!(
722 r#"{{"jsonrpc":"2.0","id":{},"method":"{}","params":{{"item":{}}}}}"#,
723 id, method, item_json
724 );
725 self.send_raw(&msg);
726 }
727
728 pub fn semantic_tokens_for_row(&self, row: usize) -> &[SemanticToken] {
732 let lo = self.semantic_tokens.partition_point(|t| t.3 < row);
733 let hi = self.semantic_tokens.partition_point(|t| t.3 <= row);
734 &self.semantic_tokens[lo..hi]
735 }
736
737 pub fn diagnostics_for_row(&self, row: usize) -> &[Diagnostic] {
739 let lo = self.diagnostics.partition_point(|d| d.row < row);
740 let hi = self.diagnostics.partition_point(|d| d.row <= row);
741 &self.diagnostics[lo..hi]
742 }
743
744 pub fn request_code_action(&mut self, path: &str, row: usize, col: usize) {
745 if !self.server_running {
746 return;
747 }
748 let uri = path_to_uri(&abs_path(path));
749 let col16 = self.char_col_to_utf16(row, col);
750 let mut diags_json = String::from("[");
752 let mut first = true;
753 for d in self.diagnostics.iter().filter(|d| d.row == row) {
754 if !first {
755 diags_json.push(',');
756 }
757 first = false;
758 let sev = match d.severity {
759 DiagnosticSeverity::Error => 1,
760 DiagnosticSeverity::Warning => 2,
761 DiagnosticSeverity::Info => 3,
762 DiagnosticSeverity::Hint => 4,
763 };
764 diags_json.push_str(&format!(
765 r#"{{"range":{{"start":{{"line":{},"character":{}}},"end":{{"line":{},"character":{}}}}},"severity":{},"message":"{}"}}"#,
766 d.row,
767 self.char_col_to_utf16(d.row, d.col_start),
768 d.row,
769 self.char_col_to_utf16(d.row, d.col_end),
770 sev,
771 escape_json(&d.message)
772 ));
773 }
774 diags_json.push(']');
775 let id = self.alloc_id(PendingReq::CodeAction);
776 let msg = format!(
777 r#"{{"jsonrpc":"2.0","id":{},"method":"textDocument/codeAction","params":{{"textDocument":{{"uri":"{}"}},"range":{{"start":{{"line":{},"character":{}}},"end":{{"line":{},"character":{}}}}},"context":{{"diagnostics":{},"only":["quickfix","refactor","source"]}}}}}}"#,
778 id,
779 escape_json(&uri),
780 row,
781 col16,
782 row,
783 col16,
784 diags_json
785 );
786 self.send_raw(&msg);
787 }
788
789 pub fn execute_command(&mut self, command: &str, args_json: Option<&str>) {
790 if !self.server_running {
791 return;
792 }
793 let id = self.alloc_id(PendingReq::ExecuteCommand);
794 let args = args_json.unwrap_or("[]");
795 let msg = format!(
796 r#"{{"jsonrpc":"2.0","id":{},"method":"workspace/executeCommand","params":{{"command":"{}","arguments":{}}}}}"#,
797 id,
798 escape_json(command),
799 args
800 );
801 self.send_raw(&msg);
802 }
803
804 fn request_position(
805 &mut self,
806 kind: PendingReq,
807 method: &str,
808 path: &str,
809 row: usize,
810 col: usize,
811 ) {
812 if !self.server_running {
813 return;
814 }
815 let uri = path_to_uri(&abs_path(path));
816 let col16 = self.char_col_to_utf16(row, col);
817 let id = self.alloc_id(kind);
818 let msg = format!(
819 r#"{{"jsonrpc":"2.0","id":{},"method":"{}","params":{{"textDocument":{{"uri":"{}"}},"position":{{"line":{},"character":{}}}}}}}"#,
820 id,
821 method,
822 escape_json(&uri),
823 row,
824 col16
825 );
826 self.send_raw(&msg);
827 }
828
829 fn char_col_to_utf16(&self, row: usize, col: usize) -> usize {
831 let line = self
832 .semantic_doc_text
833 .split('\n')
834 .nth(row)
835 .unwrap_or("");
836 char_to_utf16_col(line, col)
837 }
838
839 fn alloc_id(&mut self, kind: PendingReq) -> u64 {
840 let id = self.next_id;
841 self.next_id = self.next_id.saturating_add(1);
842 self.pending.insert(id, kind);
843 id
844 }
845
846 fn send_raw(&mut self, msg: &str) {
847 if let Some(ref mut stdin) = self.stdin {
848 let bytes = msg.as_bytes();
850 let header = format!("Content-Length: {}\r\n\r\n", bytes.len());
851 let _ = stdin.write_all(header.as_bytes());
852 let _ = stdin.write_all(bytes);
853 let _ = stdin.flush();
854 }
855 }
856
857 pub fn poll(&mut self) {
858 let mut batch = Vec::new();
859 if let Some(ref rx) = self.rx {
860 loop {
861 match rx.try_recv() {
862 Ok(m) => batch.push(m),
863 Err(TryRecvError::Empty) => break,
864 Err(TryRecvError::Disconnected) => {
865 self.server_running = false;
866 self.initialized = false;
867 self.error = Some("LSP server disconnected".into());
868 break;
869 }
870 }
871 }
872 }
873
874 for msg in batch {
875 self.handle_raw(msg);
876 }
877
878 if self.initialized {
880 if let Some((path, lang, text)) = self.pending_didopen.take() {
881 self.send_raw(r#"{"jsonrpc":"2.0","method":"initialized","params":{}}"#);
882 self.doc_version = 1;
883 self.current_uri = path_to_uri(&path);
884 self.opened_uri = self.current_uri.clone();
885 if self.semantic_doc_text.is_empty() {
887 self.semantic_doc_text = std::fs::read_to_string(&path).unwrap_or_default();
888 }
889 let msg = format!(
890 r#"{{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{{"textDocument":{{"uri":"{}","languageId":"{}","version":{},"text":"{}"}}}}}}"#,
891 escape_json(&self.current_uri),
892 lang,
893 self.doc_version,
894 text );
896 self.send_raw(&msg);
897 self.semantic_dirty = true;
898 self.maybe_request_semantic_tokens();
899 self.inlay_dirty = true;
900 self.code_lens_dirty = true;
901 }
902 }
903
904 if self.semantic_dirty && self.server_running {
906 self.maybe_request_semantic_tokens();
907 }
908 }
909
910 fn handle_raw(&mut self, msg: RawMsg) {
911 if let Some(method) = msg.method.as_deref() {
913 if method == "textDocument/publishDiagnostics" {
914 let uri = extract_str(&msg.body, "\"uri\":\"").unwrap_or_default();
915 let mut diags = parse_diagnostics(&msg.body);
916 diag_cols_utf16_to_chars(&mut diags, &self.semantic_doc_text);
918 if uri.is_empty() || uris_match(&uri, &self.current_uri) {
920 diags.sort_by_key(|d| d.row);
922 self.diagnostics = diags; }
924 return;
925 }
926 if msg.id.is_none() {
928 return;
929 }
930 }
931
932 let Some(id) = msg.id else {
933 return;
934 };
935 let Some(kind) = self.pending.remove(&id) else {
936 if msg.body.contains("\"capabilities\"") {
938 self.finish_initialize(&msg.body);
939 }
940 return;
941 };
942
943 if is_jsonrpc_error(&msg.body) {
945 let m = extract_str(&msg.body, "\"message\":\"")
946 .unwrap_or_else(|| "request failed".into());
947 match kind {
948 PendingReq::Initialize => {
949 self.error = Some(format!("LSP init: {m}"));
950 self.server_running = false;
951 self.initialized = false;
952 }
953 PendingReq::SemanticTokens | PendingReq::InlayHint | PendingReq::CodeLens => {
954 self.soft_error = Some(format!("LSP: {m}"));
956 if matches!(kind, PendingReq::SemanticTokens) {
957 self.semantic_tokens_supported = false;
958 }
959 if matches!(kind, PendingReq::InlayHint) {
960 self.inlay_supported = false;
961 }
962 if matches!(kind, PendingReq::CodeLens) {
963 self.code_lens_supported = false;
964 self.code_lenses.clear();
965 }
966 }
967 PendingReq::CodeLensResolve(_) => {
968 }
970 _ => {
971 self.soft_error = Some(format!("LSP: {m}"));
972 }
973 }
974 return;
975 }
976
977 match kind {
978 PendingReq::Initialize => {
979 self.finish_initialize(&msg.body);
980 }
981 PendingReq::Definition => {
982 if let Some(loc) = parse_single_location(&msg.body) {
983 self.pending_definition = Some(loc);
984 } else {
985 self.soft_error = Some("No definition found".into());
986 }
987 }
988 PendingReq::Completion => {
989 self.pending_completions = parse_completions(&msg.body);
990 }
991 PendingReq::Hover => {
992 if let Some(h) = parse_hover(&msg.body) {
993 self.pending_hover = Some(h);
994 }
995 }
996 PendingReq::References => {
997 self.pending_references = parse_locations(&msg.body);
998 }
999 PendingReq::Rename => {
1000 let edits = parse_workspace_edit_ctx(
1001 &msg.body,
1002 &self.current_uri,
1003 &self.semantic_doc_text,
1004 );
1005 if edits.is_empty() {
1006 self.pending_workspace_edit =
1007 Some(parse_rename_message(&msg.body).unwrap_or_else(|| {
1008 "Rename: no changes".into()
1009 }));
1010 } else {
1011 self.pending_edits = edits;
1012 }
1013 }
1014 PendingReq::Formatting => {
1015 if let Some(edit) =
1016 parse_text_edits_as_full_replace(&msg.body, &self.semantic_doc_text)
1017 {
1018 let path = uri_to_path(&self.current_uri);
1019 self.pending_edits = vec![FileEdit { path, text: edit }];
1020 } else if msg.body.contains("\"result\":null")
1021 || msg.body.contains("\"result\":[]")
1022 {
1023 self.soft_error = Some("Format: nothing to change".into());
1024 }
1025 }
1026 PendingReq::CodeAction => {
1027 self.pending_code_actions = parse_code_actions_ctx(
1028 &msg.body,
1029 &self.current_uri,
1030 &self.semantic_doc_text,
1031 );
1032 if self.pending_code_actions.is_empty() {
1033 self.soft_error = Some("No code actions".into());
1034 }
1035 }
1036 PendingReq::ExecuteCommand => {
1037 let edits = parse_workspace_edit_ctx(
1038 &msg.body,
1039 &self.current_uri,
1040 &self.semantic_doc_text,
1041 );
1042 if !edits.is_empty() {
1043 self.pending_edits = edits;
1044 } else {
1045 self.soft_error = Some("Command executed".into());
1046 }
1047 }
1048 PendingReq::SemanticTokens => {
1049 let data = parse_semantic_data(&msg.body);
1050 let lines: Vec<&str> = self.semantic_doc_text.split('\n').collect();
1051 let mut toks = decode_semantic_tokens(&data, &self.semantic_token_types, &lines);
1052 toks.sort_by_key(|t| t.3);
1055 self.semantic_tokens = toks;
1056 if self.doc_version != self.last_semantic_req_version {
1058 self.semantic_dirty = true;
1059 }
1060 }
1061 PendingReq::DocumentSymbol | PendingReq::WorkspaceSymbol => {
1062 self.pending_symbols = parse_symbols(&msg.body);
1063 }
1064 PendingReq::InlayHint => {
1065 let lines: Vec<&str> = self.semantic_doc_text.split('\n').collect();
1066 self.inlay_hints = parse_inlay_hints(&msg.body, &lines);
1067 }
1068 PendingReq::PrepareCallHierarchy => {
1069 let items = parse_call_hierarchy_items(&msg.body);
1070 if items.is_empty() {
1071 self.soft_error = Some("No call hierarchy at cursor".into());
1072 self.call_hierarchy_ready = true;
1073 self.pending_call_hierarchy.clear();
1074 } else {
1075 let dir = self
1077 .pending_call_direction
1078 .unwrap_or(crate::call_hierarchy::CallDirection::Incoming);
1079 let raw = items[0].raw_json.clone();
1080 self.pending_call_hierarchy = items;
1082 self.request_calls_for_item(&raw, dir);
1083 }
1084 }
1085 PendingReq::IncomingCalls | PendingReq::OutgoingCalls => {
1086 let calls = parse_call_hierarchy_calls(&msg.body);
1087 self.pending_call_hierarchy = calls;
1088 self.call_hierarchy_ready = true;
1089 }
1090 PendingReq::CodeLens => {
1091 self.code_lens_gen = self.code_lens_gen.wrapping_add(1);
1092 let (resolved, unresolved) = parse_code_lenses(&msg.body);
1093 self.code_lenses = resolved;
1094 let generation = self.code_lens_gen;
1097 for lens in unresolved.into_iter().take(40) {
1098 let id = self.alloc_id(PendingReq::CodeLensResolve(generation));
1099 let req = format!(
1100 r#"{{"jsonrpc":"2.0","id":{id},"method":"codeLens/resolve","params":{lens}}}"#
1101 );
1102 self.send_raw(&req);
1103 }
1104 }
1105 PendingReq::CodeLensResolve(generation) => {
1106 if generation == self.code_lens_gen {
1107 if let Some(lens) = parse_resolved_code_lens(&msg.body) {
1108 merge_code_lens(&mut self.code_lenses, lens);
1109 }
1110 }
1111 }
1112 }
1113 }
1114
1115 fn finish_initialize(&mut self, body: &str) {
1116 self.initialized = true;
1117 self.server_running = true;
1118 self.error = None;
1119 let (types, supported) = parse_semantic_legend(body);
1121 self.semantic_token_types = types;
1122 self.semantic_tokens_supported = supported && !self.semantic_token_types.is_empty();
1123 if supported && self.semantic_token_types.is_empty() {
1125 self.semantic_token_types = default_semantic_types();
1126 self.semantic_tokens_supported = true;
1127 }
1128 self.inlay_supported = body.contains("inlayHintProvider") || body.contains("\"inlayHint\"");
1129 if self.inlay_supported {
1130 self.inlay_dirty = true;
1131 }
1132 }
1133
1134 pub fn shutdown(&mut self) {
1135 if self.stdin.is_some() {
1136 self.send_raw(r#"{"jsonrpc":"2.0","id":999999,"method":"shutdown","params":null}"#);
1137 self.send_raw(r#"{"jsonrpc":"2.0","method":"exit","params":null}"#);
1138 }
1139 self.shutdown_quiet();
1140 }
1141
1142 fn shutdown_quiet(&mut self) {
1143 if let Some(mut child) = self._child.take() {
1144 let _ = child.kill();
1145 let _ = child.wait();
1146 }
1147 self.stdin = None;
1148 self.rx = None;
1149 self.server_running = false;
1150 self.initialized = false;
1151 self.pending.clear();
1152 self.pending_didopen = None;
1153 self.diagnostics.clear();
1154 self.semantic_tokens.clear();
1155 self.semantic_token_types.clear();
1156 self.semantic_tokens_supported = false;
1157 self.semantic_doc_text.clear();
1158 self.semantic_dirty = false;
1159 self.inlay_hints.clear();
1160 self.inlay_supported = false;
1161 self.inlay_dirty = false;
1162 self.pending_symbols.clear();
1163 self.pending_code_actions.clear();
1164 self.pending_edits.clear();
1165 self.definition_as_peek = false;
1166 self.opened_uri.clear();
1167 self.soft_error = None;
1168 }
1169
1170 pub fn status_label(&self) -> LspStatus {
1172 if self.server_running {
1173 LspStatus::Running {
1174 name: self.server_name.clone(),
1175 diags: self.diagnostics.len(),
1176 }
1177 } else if self.error.is_some() {
1178 LspStatus::HardError
1179 } else if self.soft_error.is_some() {
1180 LspStatus::Soft {
1181 msg: self.soft_error.clone().unwrap_or_default(),
1182 }
1183 } else {
1184 LspStatus::Idle
1185 }
1186 }
1187}
1188
1189#[derive(Debug, Clone)]
1190pub enum LspStatus {
1191 Idle,
1192 Running { name: String, diags: usize },
1193 Soft { msg: String },
1194 HardError,
1195}
1196
1197fn read_loop(stdout: impl Read + Send + 'static, tx: mpsc::Sender<RawMsg>) {
1200 let mut reader = BufReader::new(stdout);
1201 loop {
1202 let mut content_len: Option<usize> = None;
1203 loop {
1204 let mut line = String::new();
1205 match reader.read_line(&mut line) {
1206 Ok(0) => return,
1207 Err(_) => return,
1208 Ok(_) => {
1209 let t = line.trim_end_matches(['\r', '\n']);
1210 if t.is_empty() {
1211 break;
1212 }
1213 if let Some(rest) = t
1214 .strip_prefix("Content-Length:")
1215 .or_else(|| t.strip_prefix("content-length:"))
1216 {
1217 content_len = rest.trim().parse().ok();
1218 }
1219 }
1220 }
1221 }
1222 let Some(len) = content_len else {
1223 continue;
1224 };
1225 if len == 0 || len > 50_000_000 {
1226 continue;
1227 }
1228 let mut body = vec![0u8; len];
1229 if reader.read_exact(&mut body).is_err() {
1230 return;
1231 }
1232 let text = String::from_utf8_lossy(&body).into_owned();
1233 let id = extract_json_id(&text);
1234 let method = extract_str(&text, "\"method\":\"");
1235 if tx
1236 .send(RawMsg {
1237 id,
1238 method,
1239 body: text,
1240 })
1241 .is_err()
1242 {
1243 return;
1244 }
1245 }
1246}
1247
1248fn parse_diagnostics(text: &str) -> Vec<Diagnostic> {
1251 let mut diags = Vec::new();
1252 let Some(start) = text.find("\"diagnostics\":") else {
1253 return diags;
1254 };
1255 let rest = &text[start..];
1256 for item in rest.split("\"range\"").skip(1) {
1258 let row = extract_int(item, "\"line\":").unwrap_or(0).max(0) as usize;
1259 let chars: Vec<i64> = {
1261 let mut v = Vec::new();
1262 let mut search = item;
1263 while let Some(pos) = search.find("\"character\":") {
1264 let after = &search[pos + "\"character\":".len()..];
1265 if let Some(n) = after
1266 .chars()
1267 .take_while(|c| c.is_ascii_digit())
1268 .collect::<String>()
1269 .parse()
1270 .ok()
1271 {
1272 v.push(n);
1273 }
1274 search = &search[pos + 12..];
1275 if v.len() >= 2 {
1276 break;
1277 }
1278 }
1279 v
1280 };
1281 let col_start = chars.first().copied().unwrap_or(0).max(0) as usize;
1282 let col_end = chars
1283 .get(1)
1284 .copied()
1285 .unwrap_or((col_start as i64) + 1)
1286 .max(0) as usize;
1287 let msg = extract_str(item, "\"message\":\"")
1288 .unwrap_or_default()
1289 .replace('\n', " ");
1290 let severity = match extract_int(item, "\"severity\":") {
1291 Some(1) => DiagnosticSeverity::Error,
1292 Some(2) => DiagnosticSeverity::Warning,
1293 Some(3) => DiagnosticSeverity::Info,
1294 _ => DiagnosticSeverity::Hint,
1295 };
1296 if !msg.is_empty() {
1297 diags.push(Diagnostic {
1298 row,
1299 col_start,
1300 col_end: col_end.max(col_start + 1),
1301 message: msg,
1302 severity,
1303 });
1304 }
1305 }
1306 diags
1307}
1308
1309fn diag_cols_utf16_to_chars(diags: &mut [Diagnostic], doc: &str) {
1312 if diags.is_empty() || doc.is_empty() {
1313 return;
1314 }
1315 let lines: Vec<&str> = doc.split('\n').collect();
1316 for d in diags {
1317 let Some(line) = lines.get(d.row) else {
1318 continue;
1319 };
1320 if line.is_ascii() {
1321 continue;
1322 }
1323 d.col_start = utf16_to_char_col(line, d.col_start);
1324 d.col_end = utf16_to_char_col(line, d.col_end).max(d.col_start + 1);
1325 }
1326}
1327
1328fn parse_single_location(text: &str) -> Option<Location> {
1329 let locs = parse_locations(text);
1330 locs.into_iter().next()
1331}
1332
1333fn symbol_kind_name(k: i64) -> &'static str {
1334 match k {
1335 1 => "File",
1336 2 => "Module",
1337 3 => "Namespace",
1338 4 => "Package",
1339 5 => "Class",
1340 6 => "Method",
1341 7 => "Property",
1342 8 => "Field",
1343 9 => "Constructor",
1344 10 => "Enum",
1345 11 => "Interface",
1346 12 => "Function",
1347 13 => "Variable",
1348 14 => "Constant",
1349 15 => "String",
1350 16 => "Number",
1351 17 => "Boolean",
1352 18 => "Array",
1353 19 => "Object",
1354 20 => "Key",
1355 21 => "Null",
1356 22 => "EnumMember",
1357 23 => "Struct",
1358 24 => "Event",
1359 25 => "Operator",
1360 26 => "TypeParameter",
1361 _ => "Symbol",
1362 }
1363}
1364
1365fn parse_symbols(text: &str) -> Vec<SymbolItem> {
1366 let mut out = Vec::new();
1367 if text.contains("\"result\":null") {
1368 return out;
1369 }
1370 for chunk in text.split("\"name\":\"").skip(1) {
1372 let name = chunk.split('"').next().unwrap_or("").to_string();
1373 if name.is_empty() {
1374 continue;
1375 }
1376 let kind_n = extract_int(chunk, "\"kind\":").unwrap_or(0);
1377 let kind = symbol_kind_name(kind_n).to_string();
1378 let path = if let Some(uri) = extract_str(chunk, "\"uri\":\"") {
1379 uri_to_path(&uri)
1380 } else {
1381 String::new()
1382 };
1383 let row = extract_int(chunk, "\"line\":").unwrap_or(0).max(0) as usize;
1385 let col = extract_int(chunk, "\"character\":").unwrap_or(0).max(0) as usize;
1386 let detail = extract_str(chunk, "\"detail\":\"").unwrap_or_default();
1387 out.push(SymbolItem {
1388 name,
1389 kind,
1390 path,
1391 row,
1392 col,
1393 detail,
1394 });
1395 if out.len() >= 500 {
1396 break;
1397 }
1398 }
1399 out
1400}
1401
1402fn parse_inlay_hints(text: &str, lines: &[&str]) -> Vec<InlayHint> {
1403 let mut out = Vec::new();
1404 if text.contains("\"result\":null") || text.contains("\"result\":[]") {
1405 return out;
1406 }
1407 for chunk in text.split("\"position\"").skip(1) {
1409 let row = extract_int(chunk, "\"line\":").unwrap_or(0).max(0) as usize;
1410 let col_u16 = extract_int(chunk, "\"character\":").unwrap_or(0).max(0) as usize;
1411 let col = if let Some(line) = lines.get(row) {
1412 utf16_to_char_col(line, col_u16)
1413 } else {
1414 col_u16
1415 };
1416 let label = if let Some(s) = extract_str(chunk, "\"label\":\"") {
1417 s
1418 } else {
1419 let mut parts = Vec::new();
1421 for part in chunk.split("\"value\":\"").skip(1).take(6) {
1422 let v = part.split('"').next().unwrap_or("");
1423 if !v.is_empty() {
1424 parts.push(v.to_string());
1425 }
1426 }
1427 parts.join("")
1428 };
1429 let label = label.trim().to_string();
1430 if label.is_empty() {
1431 continue;
1432 }
1433 out.push(InlayHint { row, col, label });
1434 if out.len() >= 2000 {
1435 break;
1436 }
1437 }
1438 out.sort_by_key(|h| (h.row, h.col));
1439 out
1440}
1441
1442fn parse_locations(text: &str) -> Vec<Location> {
1443 let mut locs = Vec::new();
1444 if text.contains("\"result\":null") {
1446 return locs;
1447 }
1448 for chunk in text.split("\"uri\":\"").skip(1) {
1449 let uri = chunk.split('"').next().unwrap_or("");
1450 let path = uri_to_path(uri);
1451 if path.is_empty() {
1452 continue;
1453 }
1454 let row = extract_int(chunk, "\"line\":").unwrap_or(0).max(0) as usize;
1456 let col = extract_int(chunk, "\"character\":").unwrap_or(0).max(0) as usize;
1457 locs.push(Location { path, row, col });
1458 }
1459 locs
1460}
1461
1462fn parse_completions(text: &str) -> Vec<CompletionItem> {
1463 let mut items = Vec::new();
1464 for chunk in text.split("\"label\":\"").skip(1) {
1465 let label = chunk.split('"').next().unwrap_or("").to_string();
1466 if label.is_empty() {
1467 continue;
1468 }
1469 let detail = extract_str(chunk, "\"detail\":\"").map(|s| s.to_string());
1470 items.push(CompletionItem { label, detail });
1471 if items.len() >= 200 {
1472 break;
1473 }
1474 }
1475 items
1476}
1477
1478fn parse_hover(text: &str) -> Option<String> {
1479 if text.contains("\"result\":null") {
1480 return None;
1481 }
1482 if let Some(v) = extract_str(text, "\"value\":\"") {
1484 if !v.trim().is_empty() {
1485 return Some(v);
1486 }
1487 }
1488 extract_str(text, "\"contents\":\"").filter(|s| !s.trim().is_empty())
1489}
1490
1491fn parse_semantic_legend(body: &str) -> (Vec<String>, bool) {
1493 let supported = body.contains("semanticTokensProvider")
1494 || body.contains("\"semanticTokens\"");
1495 let search_from = body
1497 .find("semanticTokensProvider")
1498 .or_else(|| body.find("\"tokenTypes\""))
1499 .unwrap_or(0);
1500 let region = &body[search_from..];
1501 let Some(arr_start_rel) = region.find("\"tokenTypes\"") else {
1502 return (if supported { default_semantic_types() } else { Vec::new() }, supported);
1503 };
1504 let after = ®ion[arr_start_rel..];
1505 let Some(bracket) = after.find('[') else {
1506 return (if supported { default_semantic_types() } else { Vec::new() }, supported);
1507 };
1508 let rest = &after[bracket + 1..];
1509 let Some(end) = rest.find(']') else {
1510 return (Vec::new(), supported);
1511 };
1512 let arr = &rest[..end];
1513 let mut types = Vec::new();
1514 for part in arr.split(',') {
1515 let t = part
1516 .trim()
1517 .trim_matches('"')
1518 .trim()
1519 .to_string();
1520 if !t.is_empty() && t.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
1521 types.push(t);
1522 }
1523 }
1524 if types.is_empty() && supported {
1525 types = default_semantic_types();
1526 }
1527 let nonempty = !types.is_empty();
1528 (types, supported || nonempty)
1529}
1530
1531fn default_semantic_types() -> Vec<String> {
1532 [
1533 "namespace",
1534 "type",
1535 "class",
1536 "enum",
1537 "interface",
1538 "struct",
1539 "typeParameter",
1540 "parameter",
1541 "variable",
1542 "property",
1543 "enumMember",
1544 "event",
1545 "function",
1546 "method",
1547 "macro",
1548 "keyword",
1549 "modifier",
1550 "comment",
1551 "string",
1552 "number",
1553 "regexp",
1554 "operator",
1555 "decorator",
1556 ]
1557 .into_iter()
1558 .map(String::from)
1559 .collect()
1560}
1561
1562fn parse_semantic_data(body: &str) -> Vec<u32> {
1564 let Some(pos) = body.find("\"data\"") else {
1565 return Vec::new();
1566 };
1567 let after = &body[pos..];
1568 let Some(bracket) = after.find('[') else {
1569 return Vec::new();
1570 };
1571 let rest = &after[bracket + 1..];
1572 let Some(end) = rest.find(']') else {
1573 return Vec::new();
1574 };
1575 let arr = &rest[..end];
1576 let mut out = Vec::new();
1577 for part in arr.split(',') {
1578 let t = part.trim();
1579 if t.is_empty() {
1580 continue;
1581 }
1582 if let Ok(n) = t.parse::<u32>() {
1583 out.push(n);
1584 } else if let Ok(n) = t.parse::<i64>() {
1585 out.push(n.max(0) as u32);
1586 }
1587 }
1588 out
1589}
1590
1591fn decode_semantic_tokens(
1593 data: &[u32],
1594 legend: &[String],
1595 lines: &[&str],
1596) -> Vec<SemanticToken> {
1597 let mut tokens = Vec::new();
1598 if data.len() < 5 || legend.is_empty() {
1599 return tokens;
1600 }
1601 let mut line: u32 = 0;
1602 let mut start_utf16: u32 = 0;
1603 let mut i = 0;
1604 while i + 4 < data.len() {
1605 let delta_line = data[i];
1606 let delta_start = data[i + 1];
1607 let length = data[i + 2];
1608 let token_type = data[i + 3] as usize;
1609 i += 5;
1611
1612 if delta_line > 0 {
1613 line = line.saturating_add(delta_line);
1614 start_utf16 = delta_start;
1615 } else {
1616 start_utf16 = start_utf16.saturating_add(delta_start);
1617 }
1618
1619 let row = line as usize;
1620 let Some(line_text) = lines.get(row) else {
1621 continue;
1622 };
1623 let scol = utf16_to_char_col(line_text, start_utf16 as usize);
1624 let ecol = utf16_to_char_col(line_text, start_utf16 as usize + length as usize);
1625 if scol >= ecol {
1626 continue;
1627 }
1628 let type_name = legend.get(token_type).map(|s| s.as_str()).unwrap_or("");
1629 if type_name.is_empty() {
1630 continue;
1631 }
1632 let kind = highlight::from_semantic_type(type_name);
1633 tokens.push((kind, scol, ecol, row));
1634 }
1635 tokens.sort_by_key(|(_, st, ed, row)| (*row, ed.saturating_sub(*st), *st));
1636 tokens
1637}
1638
1639pub fn utf16_to_char_col(line: &str, utf16_col: usize) -> usize {
1641 if utf16_col == 0 {
1642 return 0;
1643 }
1644 let mut u16s = 0usize;
1645 for (i, c) in line.chars().enumerate() {
1646 if u16s >= utf16_col {
1647 return i;
1648 }
1649 u16s += c.len_utf16();
1650 }
1651 line.chars().count()
1652}
1653
1654pub fn char_to_utf16_col(line: &str, char_col: usize) -> usize {
1656 line.chars().take(char_col).map(|c| c.len_utf16()).sum()
1657}
1658
1659fn is_jsonrpc_error(body: &str) -> bool {
1662 if let Some(pos) = body.find("\"error\"") {
1664 let after = &body[pos..];
1666 if after.contains('{') {
1667 if body.contains("\"result\":") {
1669 return body.contains("\"result\":null")
1671 || body.find("\"error\"").unwrap_or(usize::MAX)
1672 < body.find("\"result\"").unwrap_or(usize::MAX);
1673 }
1674 return true;
1675 }
1676 }
1677 false
1678}
1679
1680fn parse_text_edits_as_full_replace(body: &str, original: &str) -> Option<String> {
1682 if body.contains("\"result\":null") || body.contains("\"result\":[]") {
1683 return None;
1684 }
1685 let mut edits: Vec<(usize, usize, usize, usize, String)> = Vec::new();
1687 for chunk in body.split("\"range\"").skip(1) {
1688 let lines: Vec<i64> = {
1689 let mut v = Vec::new();
1690 let mut search = chunk;
1691 while let Some(pos) = search.find("\"line\":") {
1692 let after = &search[pos + 7..];
1693 if let Ok(n) = after
1694 .chars()
1695 .take_while(|c| c.is_ascii_digit() || *c == '-')
1696 .collect::<String>()
1697 .parse()
1698 {
1699 v.push(n);
1700 }
1701 search = &search[pos + 7..];
1702 if v.len() >= 2 {
1703 break;
1704 }
1705 }
1706 v
1707 };
1708 let chars: Vec<i64> = {
1709 let mut v = Vec::new();
1710 let mut search = chunk;
1711 while let Some(pos) = search.find("\"character\":") {
1712 let after = &search[pos + 12..];
1713 if let Ok(n) = after
1714 .chars()
1715 .take_while(|c| c.is_ascii_digit() || *c == '-')
1716 .collect::<String>()
1717 .parse()
1718 {
1719 v.push(n);
1720 }
1721 search = &search[pos + 12..];
1722 if v.len() >= 2 {
1723 break;
1724 }
1725 }
1726 v
1727 };
1728 let new_text = extract_str(chunk, "\"newText\":\"").unwrap_or_default();
1729 if lines.len() >= 2 && chars.len() >= 2 {
1730 edits.push((
1731 lines[0].max(0) as usize,
1732 chars[0].max(0) as usize,
1733 lines[1].max(0) as usize,
1734 chars[1].max(0) as usize,
1735 new_text,
1736 ));
1737 }
1738 if edits.len() > 50 {
1739 break;
1740 }
1741 }
1742 if edits.is_empty() {
1743 return None;
1744 }
1745 if edits.len() == 1 {
1747 let (r0, c0, r1, _c1, ref t) = edits[0];
1748 let line_count = original.lines().count().max(1);
1749 if r0 == 0 && c0 == 0 && r1 + 1 >= line_count {
1750 return Some(t.clone());
1751 }
1752 if t.lines().count() >= line_count.saturating_sub(1) && r0 == 0 {
1754 return Some(t.clone());
1755 }
1756 }
1757 let mut lines: Vec<String> = original.lines().map(|l| l.to_string()).collect();
1759 if lines.is_empty() {
1760 lines.push(String::new());
1761 }
1762 edits.sort_by(|a, b| b.0.cmp(&a.0).then(b.1.cmp(&a.1)));
1763 for (r0, c0, r1, c1, new_t) in edits {
1764 let r0 = r0.min(lines.len().saturating_sub(1));
1765 let r1 = r1.min(lines.len().saturating_sub(1));
1766 if r0 == r1 {
1767 let line = &lines[r0];
1768 let chars: Vec<char> = line.chars().collect();
1769 let c0 = c0.min(chars.len());
1770 let c1 = c1.min(chars.len()).max(c0);
1771 let mut s: String = chars[..c0].iter().collect();
1772 s.push_str(&new_t);
1773 s.push_str(&chars[c1..].iter().collect::<String>());
1774 if s.contains('\n') {
1776 let parts: Vec<String> = s.split('\n').map(|x| x.to_string()).collect();
1777 lines.splice(r0..=r0, parts);
1778 } else {
1779 lines[r0] = s;
1780 }
1781 } else {
1782 let first: Vec<char> = lines[r0].chars().collect();
1784 let last: Vec<char> = lines[r1].chars().collect();
1785 let c0 = c0.min(first.len());
1786 let c1 = c1.min(last.len());
1787 let mut s: String = first[..c0].iter().collect();
1788 s.push_str(&new_t);
1789 s.push_str(&last[c1..].iter().collect::<String>());
1790 let parts: Vec<String> = s.split('\n').map(|x| x.to_string()).collect();
1791 lines.splice(r0..=r1, parts);
1792 }
1793 }
1794 let trailing = original.ends_with('\n');
1795 let mut out = lines.join("\n");
1796 if trailing && !out.ends_with('\n') {
1797 out.push('\n');
1798 }
1799 Some(out)
1800}
1801
1802fn parse_rename_message(text: &str) -> Option<String> {
1803 if text.contains("\"result\":null") {
1804 return Some("Rename: no changes".into());
1805 }
1806 let n = text.matches("\"newText\":\"").count();
1807 if n == 0 {
1808 return Some("Rename: no changes".into());
1809 }
1810 Some(format!("Rename: {n} edit(s)"))
1811}
1812
1813pub fn parse_workspace_edit(body: &str) -> Vec<FileEdit> {
1815 parse_workspace_edit_ctx(body, "", "")
1816}
1817
1818fn parse_workspace_edit_ctx(body: &str, current_uri: &str, current_text: &str) -> Vec<FileEdit> {
1819 if body.contains("\"result\":null") {
1820 return Vec::new();
1821 }
1822 let mut by_path: HashMap<String, Vec<RawTextEdit>> = HashMap::new();
1823
1824 for chunk in body.split("\"textDocument\"").skip(1) {
1826 let uri = extract_str(chunk, "\"uri\":\"").unwrap_or_default();
1827 if uri.is_empty() {
1828 continue;
1829 }
1830 let path = uri_to_path(&uri);
1831 let edits = parse_text_edit_list(chunk);
1832 if !edits.is_empty() {
1833 by_path.entry(path).or_default().extend(edits);
1834 }
1835 }
1836
1837 if by_path.is_empty() {
1839 if let Some(start) = body.find("\"changes\"") {
1840 let rest = &body[start..];
1841 for chunk in rest.split("\"file:").skip(1) {
1842 let uri_body = chunk.split('"').next().unwrap_or("");
1843 let uri = format!("file:{uri_body}");
1844 let path = uri_to_path(&uri);
1845 let edits = parse_text_edit_list(chunk);
1846 if !edits.is_empty() {
1847 by_path.entry(path).or_default().extend(edits);
1848 }
1849 }
1850 }
1851 }
1852
1853 if by_path.is_empty() {
1854 if let Some(uri) = extract_str(body, "\"uri\":\"") {
1855 let path = uri_to_path(&uri);
1856 let edits = parse_text_edit_list(body);
1857 if !edits.is_empty() {
1858 by_path.insert(path, edits);
1859 }
1860 }
1861 }
1862
1863 let current_path = if current_uri.is_empty() {
1864 String::new()
1865 } else {
1866 uri_to_path(current_uri)
1867 };
1868
1869 let mut out = Vec::new();
1870 for (path, mut edits) in by_path {
1871 if path.is_empty() {
1872 continue;
1873 }
1874 let original = if !current_path.is_empty() && path == current_path && !current_text.is_empty()
1875 {
1876 current_text.to_string()
1877 } else {
1878 std::fs::read_to_string(&path).unwrap_or_default()
1879 };
1880 let text = apply_raw_text_edits(&original, &mut edits);
1881 out.push(FileEdit { path, text });
1882 }
1883 out
1884}
1885
1886#[derive(Debug, Clone)]
1887struct RawTextEdit {
1888 r0: usize,
1889 c0: usize, r1: usize,
1891 c1: usize, new_text: String,
1893}
1894
1895fn parse_text_edit_list(chunk: &str) -> Vec<RawTextEdit> {
1896 let mut edits = Vec::new();
1897 for range_chunk in chunk.split("\"range\"").skip(1) {
1898 let (r0, c0, r1, c1) = extract_range_lines_chars(range_chunk);
1899 let new_text = extract_str(range_chunk, "\"newText\":\"").unwrap_or_else(|| {
1900 unescape_json_string_prefix(
1902 range_chunk
1903 .split("\"newText\":\"")
1904 .nth(1)
1905 .unwrap_or(""),
1906 )
1907 });
1908 if new_text.is_empty() && r0 == r1 && c0 == c1 {
1909 }
1911 edits.push(RawTextEdit {
1912 r0,
1913 c0,
1914 r1,
1915 c1,
1916 new_text,
1917 });
1918 if edits.len() > 200 {
1919 break;
1920 }
1921 }
1922 edits
1923}
1924
1925fn extract_range_lines_chars(chunk: &str) -> (usize, usize, usize, usize) {
1926 let mut lines = Vec::new();
1927 let mut chars = Vec::new();
1928 let mut search = chunk;
1929 while let Some(pos) = search.find("\"line\":") {
1930 let after = &search[pos + 7..];
1931 if let Ok(n) = after
1932 .chars()
1933 .take_while(|c| c.is_ascii_digit())
1934 .collect::<String>()
1935 .parse::<usize>()
1936 {
1937 lines.push(n);
1938 }
1939 search = &search[pos + 7..];
1940 if lines.len() >= 2 {
1941 break;
1942 }
1943 }
1944 search = chunk;
1945 while let Some(pos) = search.find("\"character\":") {
1946 let after = &search[pos + 12..];
1947 if let Ok(n) = after
1948 .chars()
1949 .take_while(|c| c.is_ascii_digit())
1950 .collect::<String>()
1951 .parse::<usize>()
1952 {
1953 chars.push(n);
1954 }
1955 search = &search[pos + 12..];
1956 if chars.len() >= 2 {
1957 break;
1958 }
1959 }
1960 (
1961 lines.first().copied().unwrap_or(0),
1962 chars.first().copied().unwrap_or(0),
1963 lines.get(1).copied().unwrap_or(0),
1964 chars.get(1).copied().unwrap_or(0),
1965 )
1966}
1967
1968fn unescape_json_string_prefix(part: &str) -> String {
1969 let mut raw = String::new();
1970 let mut chars = part.chars().peekable();
1971 while let Some(c) = chars.next() {
1972 if c == '\\' {
1973 if let Some(n) = chars.next() {
1974 match n {
1975 'n' => raw.push('\n'),
1976 't' => raw.push('\t'),
1977 'r' => raw.push('\r'),
1978 '"' => raw.push('"'),
1979 '\\' => raw.push('\\'),
1980 other => {
1981 raw.push('\\');
1982 raw.push(other);
1983 }
1984 }
1985 }
1986 } else if c == '"' {
1987 break;
1988 } else {
1989 raw.push(c);
1990 }
1991 }
1992 raw
1993}
1994
1995fn apply_raw_text_edits(original: &str, edits: &mut [RawTextEdit]) -> String {
1996 if edits.is_empty() {
1997 return original.to_string();
1998 }
1999 let mut lines: Vec<String> = original.lines().map(|l| l.to_string()).collect();
2001 if lines.is_empty() {
2002 lines.push(String::new());
2003 }
2004 edits.sort_by(|a, b| b.r1.cmp(&a.r1).then(b.c1.cmp(&a.c1)));
2006 for e in edits.iter() {
2007 let r0 = e.r0.min(lines.len().saturating_sub(1));
2008 let r1 = e.r1.min(lines.len().saturating_sub(1));
2009 let c0 = utf16_to_char_col(&lines[r0], e.c0);
2010 let c1 = utf16_to_char_col(&lines[r1], e.c1);
2011 if r0 == r1 {
2012 let chs: Vec<char> = lines[r0].chars().collect();
2013 let c0 = c0.min(chs.len());
2014 let c1 = c1.min(chs.len()).max(c0);
2015 let mut s: String = chs[..c0].iter().collect();
2016 s.push_str(&e.new_text);
2017 s.push_str(&chs[c1..].iter().collect::<String>());
2018 if s.contains('\n') {
2019 let parts: Vec<String> = s.split('\n').map(|x| x.to_string()).collect();
2020 lines.splice(r0..=r0, parts);
2021 } else {
2022 lines[r0] = s;
2023 }
2024 } else {
2025 let first: Vec<char> = lines[r0].chars().collect();
2026 let last: Vec<char> = lines[r1].chars().collect();
2027 let c0 = c0.min(first.len());
2028 let c1 = c1.min(last.len());
2029 let mut s: String = first[..c0].iter().collect();
2030 s.push_str(&e.new_text);
2031 s.push_str(&last[c1..].iter().collect::<String>());
2032 let parts: Vec<String> = s.split('\n').map(|x| x.to_string()).collect();
2033 lines.splice(r0..=r1, parts);
2034 }
2035 }
2036 let trailing = original.ends_with('\n');
2037 let mut out = lines.join("\n");
2038 if trailing && !out.ends_with('\n') {
2039 out.push('\n');
2040 }
2041 out
2042}
2043
2044fn parse_code_actions_ctx(
2045 body: &str,
2046 current_uri: &str,
2047 current_text: &str,
2048) -> Vec<CodeActionItem> {
2049 let mut out = Vec::new();
2050 if body.contains("\"result\":null") || body.contains("\"result\":[]") {
2051 return out;
2052 }
2053 for chunk in body.split("\"title\":\"").skip(1) {
2054 let title = chunk.split('"').next().unwrap_or("").to_string();
2055 if title.is_empty() {
2056 continue;
2057 }
2058 let kind = extract_str(chunk, "\"kind\":\"").unwrap_or_default();
2059 let edits = if let Some(pos) = chunk.find("\"edit\"") {
2060 parse_workspace_edit_ctx(&chunk[pos..], current_uri, current_text)
2061 } else {
2062 Vec::new()
2063 };
2064 let (command, command_args_json) = if let Some(pos) = chunk.find("\"command\":{") {
2065 let sub = &chunk[pos..];
2066 let cmd = extract_str(sub, "\"command\":\"");
2067 let args = sub.find("\"arguments\":").map(|i| {
2068 let rest = &sub[i + 12..];
2069 extract_json_array(rest).unwrap_or_else(|| "[]".into())
2070 });
2071 (cmd, args)
2072 } else {
2073 let cmd = extract_str(chunk, "\"command\":\"").filter(|s| {
2074 !s.is_empty() && !s.contains('{')
2076 });
2077 (cmd, None)
2078 };
2079 out.push(CodeActionItem {
2080 title,
2081 kind,
2082 edits,
2083 command,
2084 command_args_json,
2085 });
2086 if out.len() >= 40 {
2087 break;
2088 }
2089 }
2090 out
2091}
2092
2093fn extract_json_array(s: &str) -> Option<String> {
2094 let start = s.find('[')?;
2095 let mut depth = 0i32;
2096 for (i, c) in s[start..].chars().enumerate() {
2097 match c {
2098 '[' => depth += 1,
2099 ']' => {
2100 depth -= 1;
2101 if depth == 0 {
2102 return Some(s[start..start + i + 1].to_string());
2103 }
2104 }
2105 _ => {}
2106 }
2107 }
2108 None
2109}
2110
2111fn install_hint(bin: &str) -> String {
2112 let hint = match bin {
2113 "rust-analyzer" => "install: rustup component add rust-analyzer",
2114 "pyright-langserver" | "pyright" => "install: npm i -g pyright",
2115 "typescript-language-server" => "install: npm i -g typescript-language-server typescript",
2116 "clangd" => "install: brew install llvm (or apt install clangd)",
2117 "gopls" => "install: go install golang.org/x/tools/gopls@latest",
2118 "lua-language-server" => "install: brew install lua-language-server",
2119 "marksman" => "install: brew install marksman",
2120 "yaml-language-server" => "install: npm i -g yaml-language-server",
2121 "taplo" => "install: cargo install taplo-cli --locked",
2122 "bash-language-server" => "install: npm i -g bash-language-server",
2123 "zls" => "install: see https://github.com/zigtools/zls",
2124 "jdtls" => "install: brew install jdtls",
2125 _ => "install the language server or :LspStart <cmd>",
2126 };
2127 format!("LSP `{bin}` not found — {hint}")
2128}
2129
2130fn build_initialize_request(id: u64, pid: u32, root_uri: &str, folder_name: &str) -> String {
2132 let root = escape_json(root_uri);
2133 let folder = escape_json(folder_name);
2134 let token_types = r#"["namespace","type","class","enum","interface","struct","typeParameter","parameter","variable","property","enumMember","event","function","method","macro","keyword","modifier","comment","string","number","regexp","operator","decorator"]"#;
2136 let token_mods = r#"["declaration","definition","readonly","static","deprecated","abstract","async","modification","documentation","defaultLibrary"]"#;
2137 format!(
2138 concat!(
2139 r#"{{"jsonrpc":"2.0","id":{id},"method":"initialize","params":{{"#,
2140 r#""processId":{pid},"rootUri":"{root}","#,
2141 r#""workspaceFolders":[{{"uri":"{root}","name":"{folder}"}}],"#,
2142 r#""capabilities":{{"#,
2143 r#""general":{{"positionEncodings":["utf-16"]}},"#,
2144 r#""textDocument":{{"#,
2145 r#""synchronization":{{"didSave":true,"dynamicRegistration":false}},"#,
2146 r#""publishDiagnostics":{{"relatedInformation":true}},"#,
2147 r#""hover":{{"contentFormat":["markdown","plaintext"]}},"#,
2148 r#""completion":{{"completionItem":{{"snippetSupport":false,"documentationFormat":["markdown","plaintext"]}}}},"#,
2149 r#""definition":{{"linkSupport":true}},"#,
2150 r#""references":{{}},"#,
2151 r#""rename":{{"prepareSupport":false}},"#,
2152 r#""formatting":{{}},"#,
2153 r#""codeAction":{{"codeActionLiteralSupport":{{"codeActionKind":{{"valueSet":["quickfix","refactor","source"]}}}}}},"#,
2154 r#""documentSymbol":{{"hierarchicalDocumentSymbolSupport":true}},"#,
2155 r#""inlayHint":{{"resolveSupport":{{"properties":["label.tooltip"]}}}},"#,
2156 r#""semanticTokens":{{"requests":{{"full":true}},"tokenTypes":{token_types},"tokenModifiers":{token_mods},"formats":["relative"],"overlappingTokenSupport":false,"multilineTokenSupport":true}}"#,
2157 r#"}},"#, r#""workspace":{{"workspaceFolders":true,"symbol":{{}},"applyEdit":true}}"#,
2159 r#"}}}}}}"#
2161 ),
2162 id = id,
2163 pid = pid,
2164 root = root,
2165 folder = folder,
2166 token_types = token_types,
2167 token_mods = token_mods,
2168 )
2169}
2170
2171fn extract_json_id(text: &str) -> Option<u64> {
2172 let key = "\"id\":";
2174 let mut search = text;
2175 while let Some(pos) = search.find(key) {
2176 let after = search[pos + key.len()..].trim_start();
2177 if after.starts_with('n') {
2179 search = &search[pos + key.len()..];
2181 continue;
2182 }
2183 if let Some(n) = after
2184 .chars()
2185 .take_while(|c| c.is_ascii_digit())
2186 .collect::<String>()
2187 .parse()
2188 .ok()
2189 {
2190 return Some(n);
2191 }
2192 search = &search[pos + key.len()..];
2193 }
2194 None
2195}
2196
2197fn extract_str(text: &str, prefix: &str) -> Option<String> {
2201 let start = text.find(prefix)? + prefix.len();
2202 let mut out = String::new();
2203 let mut chars = text[start..].chars();
2204 while let Some(c) = chars.next() {
2205 match c {
2206 '\\' => match chars.next() {
2207 Some('n') => out.push('\n'),
2208 Some('t') => out.push('\t'),
2209 Some('r') => out.push('\r'),
2210 Some('"') => out.push('"'),
2211 Some('\\') => out.push('\\'),
2212 Some('/') => out.push('/'),
2213 Some('u') => {
2214 let hex: String = chars.by_ref().take(4).collect();
2215 if let Some(ch) = u32::from_str_radix(&hex, 16)
2216 .ok()
2217 .and_then(char::from_u32)
2218 {
2219 out.push(ch);
2220 }
2221 }
2222 Some(other) => {
2223 out.push('\\');
2224 out.push(other);
2225 }
2226 None => break,
2227 },
2228 '"' => break,
2229 c => out.push(c),
2230 }
2231 }
2232 Some(out)
2233}
2234
2235fn extract_int(text: &str, prefix: &str) -> Option<i64> {
2236 text.find(prefix).and_then(|i| {
2237 let s = text[i + prefix.len()..].trim_start();
2238 s.chars()
2239 .take_while(|c| c.is_ascii_digit())
2240 .collect::<String>()
2241 .parse()
2242 .ok()
2243 })
2244}
2245
2246fn escape_json(s: &str) -> String {
2247 let mut out = String::with_capacity(s.len() + 8);
2248 for ch in s.chars() {
2249 match ch {
2250 '\\' => out.push_str("\\\\"),
2251 '"' => out.push_str("\\\""),
2252 '\n' => out.push_str("\\n"),
2253 '\r' => out.push_str("\\r"),
2254 '\t' => out.push_str("\\t"),
2255 '\u{08}' => out.push_str("\\b"),
2256 '\u{0C}' => out.push_str("\\f"),
2257 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
2258 c => out.push(c),
2259 }
2260 }
2261 out
2262}
2263
2264fn abs_path(path: &str) -> String {
2265 let p = PathBuf::from(path);
2266 if p.is_absolute() {
2267 return p.display().to_string();
2268 }
2269 std::env::current_dir()
2270 .map(|c| c.join(p))
2271 .unwrap_or_else(|_| PathBuf::from(path))
2272 .display()
2273 .to_string()
2274}
2275
2276fn path_to_uri(path: &str) -> String {
2277 let abs = abs_path(path);
2278 let mut encoded = String::from("file://");
2279 #[cfg(unix)]
2281 {
2282 if !abs.starts_with('/') {
2283 encoded.push('/');
2284 }
2285 }
2286 for c in abs.chars() {
2287 match c {
2288 ' ' => encoded.push_str("%20"),
2289 '\\' => encoded.push('/'),
2290 c if c.is_ascii_alphanumeric()
2291 || matches!(c, '/' | ':' | '-' | '_' | '.' | '~') =>
2292 {
2293 encoded.push(c);
2294 }
2295 c => {
2296 for b in c.to_string().as_bytes() {
2297 encoded.push_str(&format!("%{:02X}", b));
2298 }
2299 }
2300 }
2301 }
2302 encoded
2303}
2304
2305fn code_lens_fields(lens: &serde_json::Value) -> (usize, usize, Option<String>) {
2307 let row = lens
2308 .get("range")
2309 .and_then(|r| r.get("start"))
2310 .and_then(|s| s.get("line"))
2311 .and_then(|l| l.as_u64())
2312 .unwrap_or(0) as usize;
2313 let col = lens
2314 .get("range")
2315 .and_then(|r| r.get("start"))
2316 .and_then(|s| s.get("character"))
2317 .and_then(|c| c.as_u64())
2318 .unwrap_or(0) as usize;
2319 let title = lens
2320 .get("command")
2321 .and_then(|c| c.get("title"))
2322 .and_then(|t| t.as_str())
2323 .filter(|t| !t.is_empty())
2324 .map(|t| t.to_string());
2325 (row, col, title)
2326}
2327
2328fn parse_code_lenses(body: &str) -> (Vec<CodeLens>, Vec<serde_json::Value>) {
2331 let Ok(v) = serde_json::from_str::<serde_json::Value>(body) else {
2332 return (Vec::new(), Vec::new());
2333 };
2334 let Some(arr) = v.get("result").and_then(|r| r.as_array()) else {
2335 return (Vec::new(), Vec::new());
2336 };
2337 let mut out = Vec::new();
2338 let mut unresolved = Vec::new();
2339 for lens in arr {
2340 let (row, col, title) = code_lens_fields(lens);
2341 match title {
2342 Some(title) => out.push(CodeLens { row, col, title }),
2343 None => unresolved.push(lens.clone()),
2344 }
2345 }
2346 out.sort_by_key(|l| (l.row, l.col));
2348 let mut merged: Vec<CodeLens> = Vec::new();
2349 for lens in out {
2350 if let Some(last) = merged.last_mut() {
2351 if last.row == lens.row {
2352 last.title = format!("{} · {}", last.title, lens.title);
2353 continue;
2354 }
2355 }
2356 merged.push(lens);
2357 }
2358 (merged, unresolved)
2359}
2360
2361fn parse_resolved_code_lens(body: &str) -> Option<CodeLens> {
2363 let v = serde_json::from_str::<serde_json::Value>(body).ok()?;
2364 let lens = v.get("result")?;
2365 let (row, col, title) = code_lens_fields(lens);
2366 title.map(|title| CodeLens { row, col, title })
2367}
2368
2369fn merge_code_lens(list: &mut Vec<CodeLens>, lens: CodeLens) {
2371 if let Some(existing) = list.iter_mut().find(|l| l.row == lens.row) {
2372 existing.title = format!("{} · {}", existing.title, lens.title);
2373 return;
2374 }
2375 let pos = list.partition_point(|l| l.row < lens.row);
2376 list.insert(pos, lens);
2377}
2378
2379fn parse_call_hierarchy_items(body: &str) -> Vec<crate::call_hierarchy::CallItem> {
2380 let Ok(v) = serde_json::from_str::<serde_json::Value>(body) else {
2381 return Vec::new();
2382 };
2383 let result = v.get("result");
2384 let arr = match result {
2385 Some(serde_json::Value::Array(a)) => a.clone(),
2386 Some(obj) if obj.is_object() => vec![obj.clone()],
2387 _ => return Vec::new(),
2388 };
2389 arr.into_iter()
2390 .filter_map(|item| call_item_from_json(&item))
2391 .collect()
2392}
2393
2394fn parse_call_hierarchy_calls(body: &str) -> Vec<crate::call_hierarchy::CallItem> {
2395 let Ok(v) = serde_json::from_str::<serde_json::Value>(body) else {
2396 return Vec::new();
2397 };
2398 let Some(arr) = v.get("result").and_then(|r| r.as_array()) else {
2399 return Vec::new();
2400 };
2401 let mut out = Vec::new();
2402 for call in arr {
2403 let item = call
2405 .get("from")
2406 .or_else(|| call.get("to"))
2407 .cloned()
2408 .unwrap_or(call.clone());
2409 if let Some(ci) = call_item_from_json(&item) {
2410 out.push(ci);
2411 }
2412 }
2413 out
2414}
2415
2416fn call_item_from_json(item: &serde_json::Value) -> Option<crate::call_hierarchy::CallItem> {
2417 let name = item
2418 .get("name")
2419 .and_then(|n| n.as_str())
2420 .unwrap_or("?")
2421 .to_string();
2422 let detail = item
2423 .get("detail")
2424 .and_then(|d| d.as_str())
2425 .unwrap_or("")
2426 .to_string();
2427 let kind_n = item.get("kind").and_then(|k| k.as_u64()).unwrap_or(12);
2428 let kind = symbol_kind_name(kind_n as i64).to_string();
2429 let uri = item
2430 .get("uri")
2431 .and_then(|u| u.as_str())
2432 .unwrap_or("");
2433 let path = uri_to_path(uri);
2434 let range = item
2435 .get("selectionRange")
2436 .or_else(|| item.get("range"));
2437 let row = range
2438 .and_then(|r| r.get("start"))
2439 .and_then(|s| s.get("line"))
2440 .and_then(|l| l.as_u64())
2441 .unwrap_or(0) as usize;
2442 let col = range
2443 .and_then(|r| r.get("start"))
2444 .and_then(|s| s.get("character"))
2445 .and_then(|c| c.as_u64())
2446 .unwrap_or(0) as usize;
2447 let raw_json = item.to_string();
2448 Some(crate::call_hierarchy::CallItem {
2449 name,
2450 detail,
2451 kind,
2452 path,
2453 row,
2454 col,
2455 raw_json,
2456 })
2457}
2458
2459fn uri_to_path(uri: &str) -> String {
2460 let rest = uri.strip_prefix("file://").unwrap_or(uri);
2461 let mut out = String::new();
2463 let bytes = rest.as_bytes();
2464 let mut i = 0;
2465 while i < bytes.len() {
2466 if bytes[i] == b'%' && i + 2 < bytes.len() {
2467 if let Ok(v) = u8::from_str_radix(
2468 std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or("00"),
2469 16,
2470 ) {
2471 out.push(v as char);
2472 i += 3;
2473 continue;
2474 }
2475 }
2476 out.push(bytes[i] as char);
2477 i += 1;
2478 }
2479 out
2481}
2482
2483fn uris_match(a: &str, b: &str) -> bool {
2484 if a == b {
2485 return true;
2486 }
2487 let pa = uri_to_path(a);
2488 let pb = uri_to_path(b);
2489 pa == pb || abs_path(&pa) == abs_path(&pb)
2490}
2491
2492fn find_project_root(hint: &str, file: &str) -> String {
2493 let start = Path::new(file)
2494 .parent()
2495 .map(|p| p.to_path_buf())
2496 .unwrap_or_else(|| PathBuf::from(hint));
2497 let markers = [
2498 "Cargo.toml",
2499 "package.json",
2500 "go.mod",
2501 "pyproject.toml",
2502 "setup.py",
2503 "compile_commands.json",
2504 "CMakeLists.txt",
2505 "Gemfile",
2506 ".git",
2507 ];
2508 let mut cur = start;
2509 for _ in 0..12 {
2510 for m in &markers {
2511 if cur.join(m).exists() {
2512 return cur.display().to_string();
2513 }
2514 }
2515 if !cur.pop() {
2516 break;
2517 }
2518 }
2519 Path::new(file)
2520 .parent()
2521 .map(|p| p.display().to_string())
2522 .unwrap_or_else(|| hint.to_string())
2523}
2524
2525fn command_exists(bin: &str) -> bool {
2526 if bin.contains('/') && Path::new(bin).exists() {
2528 return true;
2529 }
2530 Command::new("which")
2531 .arg(bin)
2532 .stdout(Stdio::null())
2533 .stderr(Stdio::null())
2534 .status()
2535 .map(|s| s.success())
2536 .unwrap_or(false)
2537}
2538
2539pub fn has_server_for(path: &str) -> bool {
2541 let ext = Path::new(path)
2542 .extension()
2543 .and_then(|e| e.to_str())
2544 .unwrap_or("")
2545 .to_lowercase();
2546 default_server_for_ext(&ext).is_some()
2547}
2548
2549pub fn ext_to_lang_key(ext: &str) -> Option<&'static str> {
2551 Some(match ext {
2552 "rs" => "rust",
2553 "py" | "pyi" => "python",
2554 "ts" | "tsx" | "mts" | "cts" => "typescript",
2555 "js" | "jsx" | "mjs" | "cjs" => "javascript",
2556 "c" | "h" | "cpp" | "hpp" | "cc" | "cxx" | "hh" | "hxx" => "c",
2557 "go" => "go",
2558 "java" => "java",
2559 "lua" => "lua",
2560 "json" | "jsonc" => "json",
2561 "yaml" | "yml" => "yaml",
2562 "toml" => "toml",
2563 "md" | "mdx" => "markdown",
2564 "sh" | "bash" | "zsh" => "bash",
2565 "zig" => "zig",
2566 "php" => "php",
2567 "rb" => "ruby",
2568 "swift" => "swift",
2569 "kt" | "kts" => "kotlin",
2570 "cs" => "csharp",
2571 "html" | "htm" => "html",
2572 "css" | "scss" | "less" => "css",
2573 "vue" => "vue",
2574 "svelte" => "svelte",
2575 "dart" => "dart",
2576 "hs" => "haskell",
2577 "ex" | "exs" => "elixir",
2578 "scala" => "scala",
2579 "nim" => "nim",
2580 _ => return None,
2581 })
2582}
2583
2584fn default_server_for_ext(ext: &str) -> Option<&'static str> {
2585 if let Some(lang) = ext_to_lang_key(ext) {
2587 for (key, _, cmd) in config::lsp_lang_catalog() {
2588 if *key == lang {
2589 return Some(*cmd);
2590 }
2591 }
2592 }
2593 Some(match ext {
2594 "rs" => "rust-analyzer",
2595 "py" | "pyi" => "pyright-langserver --stdio",
2596 "ts" | "tsx" | "mts" | "cts" => "typescript-language-server --stdio",
2597 "js" | "jsx" | "mjs" | "cjs" => "typescript-language-server --stdio",
2598 "c" | "h" => "clangd",
2599 "cpp" | "hpp" | "cc" | "cxx" | "hh" | "hxx" => "clangd",
2600 "go" => "gopls",
2601 "java" => "jdtls",
2602 "lua" => "lua-language-server",
2603 "php" => "intelephense --stdio",
2604 "rb" => "solargraph stdio",
2605 "swift" => "sourcekit-lsp",
2606 "kt" | "kts" => "kotlin-language-server",
2607 "cs" => "csharp-ls",
2608 "html" | "htm" => "vscode-html-language-server --stdio",
2609 "css" | "scss" | "less" => "vscode-css-language-server --stdio",
2610 "json" | "jsonc" => "vscode-json-language-server --stdio",
2611 "yaml" | "yml" => "yaml-language-server --stdio",
2612 "toml" => "taplo lsp stdio",
2613 "md" | "mdx" => "marksman server",
2614 "sh" | "bash" | "zsh" => "bash-language-server start",
2615 "zig" => "zls",
2616 "nim" => "nimlsp",
2617 "ex" | "exs" => "elixir-ls",
2618 "hs" => "haskell-language-server-wrapper --lsp",
2619 "scala" => "metals",
2620 "vue" => "vue-language-server --stdio",
2621 "svelte" => "svelteserver --stdio",
2622 "dart" => "dart language-server",
2623 "r" | "R" => "r-languageserver",
2624 _ => return None,
2625 })
2626}
2627
2628#[cfg(test)]
2630fn server_for_ext(ext: &str) -> Option<&'static str> {
2631 default_server_for_ext(ext)
2632}
2633
2634fn lang_id(path: &str) -> &'static str {
2635 let ext = Path::new(path)
2636 .extension()
2637 .and_then(|e| e.to_str())
2638 .unwrap_or("")
2639 .to_lowercase();
2640 match ext.as_str() {
2641 "rs" => "rust",
2642 "py" | "pyi" => "python",
2643 "ts" | "mts" | "cts" => "typescript",
2644 "tsx" => "typescriptreact",
2645 "js" | "mjs" | "cjs" => "javascript",
2646 "jsx" => "javascriptreact",
2647 "go" => "go",
2648 "c" => "c",
2649 "h" => "c",
2650 "cpp" | "cc" | "cxx" | "hpp" | "hh" | "hxx" => "cpp",
2651 "java" => "java",
2652 "lua" => "lua",
2653 "php" => "php",
2654 "rb" => "ruby",
2655 "swift" => "swift",
2656 "kt" | "kts" => "kotlin",
2657 "cs" => "csharp",
2658 "html" | "htm" => "html",
2659 "css" => "css",
2660 "scss" => "scss",
2661 "less" => "less",
2662 "json" | "jsonc" => "json",
2663 "yaml" | "yml" => "yaml",
2664 "toml" => "toml",
2665 "md" | "mdx" => "markdown",
2666 "sh" | "bash" | "zsh" => "shellscript",
2667 "zig" => "zig",
2668 "vue" => "vue",
2669 "svelte" => "svelte",
2670 "dart" => "dart",
2671 "hs" => "haskell",
2672 "ex" | "exs" => "elixir",
2673 "scala" => "scala",
2674 "nim" => "nim",
2675 _ => "plaintext",
2676 }
2677}
2678
2679#[cfg(test)]
2680mod tests {
2681 use super::*;
2682
2683 #[test]
2684 fn code_lens_split_resolved_unresolved() {
2685 let body = r#"{"jsonrpc":"2.0","id":1,"result":[
2686 {"range":{"start":{"line":3,"character":0},"end":{"line":3,"character":2}},
2687 "command":{"title":"2 references","command":"x"}},
2688 {"range":{"start":{"line":3,"character":4},"end":{"line":3,"character":6}},
2689 "command":{"title":"run test","command":"y"}},
2690 {"range":{"start":{"line":9,"character":0},"end":{"line":9,"character":1}},
2691 "data":{"kind":"references"}}
2692 ]}"#;
2693 let (resolved, unresolved) = parse_code_lenses(body);
2694 assert_eq!(resolved.len(), 1);
2696 assert_eq!(resolved[0].row, 3);
2697 assert_eq!(resolved[0].title, "2 references · run test");
2698 assert_eq!(unresolved.len(), 1);
2699 assert_eq!(
2700 unresolved[0]["range"]["start"]["line"].as_u64(),
2701 Some(9)
2702 );
2703 }
2704
2705 #[test]
2706 fn code_lens_resolve_response_parses_and_merges() {
2707 let body = r#"{"jsonrpc":"2.0","id":7,"result":
2708 {"range":{"start":{"line":9,"character":0},"end":{"line":9,"character":1}},
2709 "command":{"title":"5 references","command":"x"}}}"#;
2710 let lens = parse_resolved_code_lens(body).unwrap();
2711 assert_eq!(lens.row, 9);
2712 assert_eq!(lens.title, "5 references");
2713
2714 let mut list = vec![CodeLens {
2715 row: 3,
2716 col: 0,
2717 title: "run test".into(),
2718 }];
2719 merge_code_lens(&mut list, lens);
2720 assert_eq!(list.len(), 2);
2721 assert_eq!(list[1].row, 9);
2722 merge_code_lens(
2724 &mut list,
2725 CodeLens {
2726 row: 3,
2727 col: 5,
2728 title: "debug".into(),
2729 },
2730 );
2731 assert_eq!(list[0].title, "run test · debug");
2732 }
2733
2734 #[test]
2735 fn code_lens_resolve_without_command_is_dropped() {
2736 let body = r#"{"jsonrpc":"2.0","id":7,"result":
2737 {"range":{"start":{"line":1,"character":0},"end":{"line":1,"character":1}}}}"#;
2738 assert!(parse_resolved_code_lens(body).is_none());
2739 }
2740
2741 #[test]
2742 fn escape_and_uri() {
2743 let e = escape_json("a\"b\nc");
2744 assert!(e.contains("\\\""));
2745 assert!(e.contains("\\n"));
2746 let u = path_to_uri("/tmp/foo bar.rs");
2747 assert!(u.starts_with("file://"));
2748 assert!(u.contains("%20"));
2749 }
2750
2751 #[test]
2752 fn parse_diag_empty_array() {
2753 let body = r#"{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{"uri":"file:///a.rs","diagnostics":[]}}"#;
2754 let d = parse_diagnostics(body);
2755 assert!(d.is_empty());
2756 }
2757
2758 #[test]
2759 fn server_map_known() {
2760 assert!(server_for_ext("rs").is_some());
2761 assert!(server_for_ext("zig").is_some());
2762 assert!(server_for_ext("xyz").is_none());
2763 }
2764
2765 #[test]
2766 fn content_length_bytes() {
2767 let msg = "{\"a\":\"한글\"}";
2768 assert_ne!(msg.len(), msg.chars().count());
2769 assert_eq!(msg.as_bytes().len(), msg.len());
2771 }
2772
2773 #[test]
2774 fn decode_semantic_relative_tokens() {
2775 let legend = default_semantic_types();
2777 let function_idx = legend.iter().position(|t| t == "function").unwrap() as u32;
2778 let keyword_idx = legend.iter().position(|t| t == "keyword").unwrap() as u32;
2779 let data = vec![
2782 0, 0, 2, keyword_idx, 0, 0, 3, 4, function_idx, 0, ];
2785 let lines = ["fn main() {}"];
2786 let toks = decode_semantic_tokens(&data, &legend, &lines);
2787 assert_eq!(toks.len(), 2);
2788 assert_eq!(toks[0].1, 0);
2789 assert_eq!(toks[0].2, 2);
2790 assert!(matches!(toks[0].0, TokenKind::Keyword));
2791 assert_eq!(toks[1].1, 3);
2792 assert_eq!(toks[1].2, 7);
2793 assert!(matches!(toks[1].0, TokenKind::Function));
2794 }
2795
2796 #[test]
2797 fn workspace_edit_changes_map() {
2798 let body = r#"{"jsonrpc":"2.0","id":1,"result":{"changes":{"file:///tmp/xei_we_test.rs":[{"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":5}},"newText":"hello"}]}}}"#;
2799 let path = "/tmp/xei_we_test.rs";
2801 let _ = std::fs::write(path, "world\n");
2802 let edits = parse_workspace_edit(body);
2803 assert!(!edits.is_empty(), "expected edits");
2804 assert!(edits[0].text.contains("hello"));
2805 let _ = std::fs::remove_file(path);
2806 }
2807
2808 #[test]
2809 fn install_hint_mentions_binary() {
2810 let h = install_hint("rust-analyzer");
2811 assert!(h.contains("rust-analyzer"));
2812 assert!(h.contains("install"));
2813 }
2814
2815 #[test]
2816 fn initialize_request_is_valid_json() {
2817 let s = build_initialize_request(1, 42, "file:///tmp/xei-proj", "xei");
2818 let mut bal = 0i32;
2820 for c in s.chars() {
2821 match c {
2822 '{' => bal += 1,
2823 '}' => bal -= 1,
2824 _ => {}
2825 }
2826 assert!(bal >= 0, "negative brace balance in {s}");
2827 }
2828 assert_eq!(bal, 0, "unbalanced braces: {s}");
2829 assert!(s.contains("\"method\":\"initialize\""));
2831 assert!(s.contains("\"processId\":42"));
2832 assert!(s.contains("semanticTokens"));
2833 assert!(s.contains("workspaceFolders"));
2834 }
2835
2836 #[test]
2837 fn initialize_survives_rust_analyzer() {
2838 if !command_exists("rust-analyzer") {
2839 return;
2840 }
2841 use std::io::{Read, Write};
2843 use std::process::{Command, Stdio};
2844 use std::time::Duration;
2845 let mut child = Command::new("rust-analyzer")
2846 .stdin(Stdio::piped())
2847 .stdout(Stdio::piped())
2848 .stderr(Stdio::null())
2849 .spawn()
2850 .expect("spawn rust-analyzer");
2851 let mut stdin = child.stdin.take().unwrap();
2852 let mut stdout = child.stdout.take().unwrap();
2853 let body = build_initialize_request(1, std::process::id(), "file:///tmp", "tmp");
2854 let header = format!("Content-Length: {}\r\n\r\n", body.len());
2855 stdin.write_all(header.as_bytes()).unwrap();
2856 stdin.write_all(body.as_bytes()).unwrap();
2857 stdin.flush().unwrap();
2858 let start = std::time::Instant::now();
2860 let mut buf = Vec::new();
2861 let mut tmp = [0u8; 4096];
2862 while start.elapsed() < Duration::from_secs(3) {
2863 if let Ok(n) = stdout.read(&mut tmp) {
2864 if n == 0 {
2865 break;
2866 }
2867 buf.extend_from_slice(&tmp[..n]);
2868 if buf.windows(4).any(|w| w == b"\r\n\r\n") {
2869 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
2871 let header = std::str::from_utf8(&buf[..pos]).unwrap_or("");
2872 let mut len = 0usize;
2873 for line in header.lines() {
2874 if let Some(rest) = line
2875 .to_ascii_lowercase()
2876 .strip_prefix("content-length:")
2877 {
2878 len = rest.trim().parse().unwrap_or(0);
2879 }
2880 }
2881 let body_start = pos + 4;
2882 if buf.len() >= body_start + len {
2883 let body = std::str::from_utf8(&buf[body_start..body_start + len])
2884 .unwrap_or("");
2885 assert!(
2886 body.contains("\"result\"") || body.contains("capabilities"),
2887 "unexpected RA response: {body}"
2888 );
2889 assert!(
2890 !is_jsonrpc_error(body),
2891 "init should not be error: {body}"
2892 );
2893 let _ = child.kill();
2894 return;
2895 }
2896 }
2897 }
2898 } else {
2899 std::thread::sleep(Duration::from_millis(20));
2900 }
2901 }
2902 let _ = child.kill();
2903 panic!("timed out waiting for rust-analyzer initialize response");
2904 }
2905
2906 #[test]
2907 fn char_utf16_roundtrip() {
2908 let line = "a𝕏b"; assert_eq!(char_to_utf16_col(line, 0), 0);
2910 assert_eq!(char_to_utf16_col(line, 1), 1);
2911 assert_eq!(char_to_utf16_col(line, 2), 3); assert_eq!(utf16_to_char_col(line, 3), 2);
2913 }
2914
2915 #[test]
2916 fn jsonrpc_error_detect() {
2917 assert!(is_jsonrpc_error(
2918 r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}"#
2919 ));
2920 assert!(!is_jsonrpc_error(
2921 r#"{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"errorCodes":[]}}}"#
2922 ));
2923 }
2924
2925 #[test]
2926 fn utf16_cjk_column() {
2927 let line = "a한b"; assert_eq!(utf16_to_char_col(line, 0), 0);
2929 assert_eq!(utf16_to_char_col(line, 1), 1);
2930 assert_eq!(utf16_to_char_col(line, 2), 2);
2931 assert_eq!(utf16_to_char_col(line, 3), 3);
2932 }
2933
2934 #[test]
2935 fn extract_str_handles_escaped_quotes() {
2936 let body = r#"{"message":"expected \"foo\", found bar\nhere"}"#;
2937 let m = extract_str(body, "\"message\":\"").unwrap();
2938 assert_eq!(m, "expected \"foo\", found bar\nhere");
2939 }
2940
2941 #[test]
2942 fn diag_cols_convert_utf16_surrogate_pairs() {
2943 let doc = "𝕏ab";
2945 let mut diags = vec![Diagnostic {
2946 row: 0,
2947 col_start: 2, col_end: 3,
2949 message: "m".into(),
2950 severity: DiagnosticSeverity::Error,
2951 }];
2952 diag_cols_utf16_to_chars(&mut diags, doc);
2953 assert_eq!(diags[0].col_start, 1); assert_eq!(diags[0].col_end, 2);
2955 }
2956
2957 #[test]
2958 fn parse_semantic_data_array() {
2959 let body = r#"{"jsonrpc":"2.0","id":1,"result":{"data":[0,0,2,15,0,0,3,4,12,0]}}"#;
2960 let d = parse_semantic_data(body);
2961 assert_eq!(d, vec![0, 0, 2, 15, 0, 0, 3, 4, 12, 0]);
2962 }
2963}