1#![allow(clippy::wildcard_imports, clippy::default_trait_access)]
2
3use lsp_types::*;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::{HashMap, VecDeque};
7use std::io::{BufRead, BufReader, Write};
8use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
9use std::sync::Arc;
10use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
11use std::sync::mpsc::{self, Receiver};
12use std::time::{Duration, Instant};
13
14use super::capabilities::{SemanticBackendInfo, SemanticBackendKind, SemanticCapabilities};
15use super::config::ResolvedServer;
16
17const INIT_TIMEOUT_SECS: u64 = 60;
18const REQUEST_TIMEOUT_SECS: u64 = 30;
19const SHUTDOWN_TIMEOUT_SECS: u64 = 5;
20
21pub fn file_path_to_uri(path: &str) -> Result<Uri, String> {
22 let abs = if path.starts_with('/') || (path.len() >= 2 && path.as_bytes()[1] == b':') {
23 path.to_string()
24 } else {
25 std::fs::canonicalize(path)
26 .map(|p| p.to_string_lossy().to_string())
27 .map_err(|e| format!("Cannot resolve path '{path}': {e}"))?
28 };
29 let normalized = percent_encode_path(&abs.replace('\\', "/"));
30 let uri_str = if normalized.starts_with('/') {
31 format!("file://{normalized}")
32 } else {
33 format!("file:///{normalized}")
34 };
35 uri_str
36 .parse::<Uri>()
37 .map_err(|e| format!("Invalid URI: {e}"))
38}
39
40fn percent_encode_path(path: &str) -> String {
44 let mut out = String::with_capacity(path.len());
45 for b in path.bytes() {
46 if b.is_ascii_alphanumeric() || b"-._~/!$&'()*+,;=:@".contains(&b) {
47 out.push(b as char);
48 } else {
49 out.push_str(&format!("%{b:02X}"));
50 }
51 }
52 out
53}
54
55pub fn uri_to_file_path(uri: &Uri) -> Option<String> {
56 let s = uri.as_str();
57 s.strip_prefix("file://")
58 .map(|p| urlencoding::decode(p).map_or_else(|_| p.to_string(), |d| d.to_string()))
59 .map(|p| {
60 let b = p.as_bytes();
62 if b.len() >= 3 && b[0] == b'/' && b[1].is_ascii_alphabetic() && b[2] == b':' {
63 p[1..].to_string()
64 } else {
65 p
66 }
67 })
68}
69
70pub struct LspClient {
71 child: Child,
72 stdin: ChildStdin,
73 response_rx: Receiver<Result<Value, String>>,
74 next_id: AtomicI64,
75 initialized: bool,
76 alive: Arc<AtomicBool>,
79 info: SemanticBackendInfo,
82 documents: DocumentStore,
83 request_timeout: Duration,
85 root_path: String,
87}
88
89#[derive(Default)]
92struct DocumentStore {
93 documents: HashMap<String, (i32, u64)>,
94 open_order: VecDeque<String>,
96}
97
98impl DocumentStore {
99 fn sync(&mut self, uri: &str, text: &str) -> (DocumentSync, Option<String>) {
102 let hash = content_hash(text);
103 if let Some((version, known)) = self.documents.get_mut(uri) {
104 if *known == hash {
105 return (DocumentSync::Unchanged, None);
106 }
107 *version += 1;
108 *known = hash;
109 return (DocumentSync::Change(*version), None);
110 }
111 self.documents.insert(uri.to_string(), (1, hash));
112 self.open_order.push_back(uri.to_string());
113 let evicted = (self.open_order.len() > MAX_OPEN_DOCUMENTS)
114 .then(|| self.open_order.pop_front())
115 .flatten();
116 if let Some(old) = &evicted {
117 self.documents.remove(old);
118 }
119 (DocumentSync::Open, evicted)
120 }
121}
122
123const MAX_OPEN_DOCUMENTS: usize = 128;
126
127#[derive(Debug, PartialEq, Eq)]
129enum DocumentSync {
130 Open,
131 Change(i32),
132 Unchanged,
133}
134
135fn content_hash(text: &str) -> u64 {
136 use std::hash::{Hash, Hasher};
137 let mut h = std::collections::hash_map::DefaultHasher::new();
138 text.hash(&mut h);
139 h.finish()
140}
141
142#[derive(Debug, PartialEq)]
144enum Incoming {
145 FromServer(Option<Value>),
147 Response(Result<Option<Value>, String>),
149 Other,
151}
152
153fn route_incoming(msg: Value, expected_id: i64) -> Incoming {
157 if msg.get("method").is_some() {
158 return Incoming::FromServer(server_request_reply(&msg));
159 }
160 if msg.get("id").and_then(Value::as_i64) != Some(expected_id) {
161 return Incoming::Other;
162 }
163 Incoming::Response(
164 serde_json::from_value::<JsonRpcResponse>(msg)
165 .map_err(|e| e.to_string())
166 .and_then(|resp| match resp.error {
167 Some(err) => Err(format!("LSP error: {}", err.message)),
168 None => Ok(resp.result),
169 }),
170 )
171}
172
173fn server_request_reply(msg: &Value) -> Option<Value> {
178 let id = msg.get("id")?.clone();
179 let method = msg.get("method")?.as_str()?;
180 let reply = match method {
181 "window/workDoneProgress/create"
182 | "client/registerCapability"
183 | "client/unregisterCapability"
184 | "window/showMessageRequest" => {
185 serde_json::json!({ "jsonrpc": "2.0", "id": id, "result": Value::Null })
186 }
187 "workspace/configuration" => {
188 let n = msg
189 .pointer("/params/items")
190 .and_then(Value::as_array)
191 .map_or(0, Vec::len);
192 serde_json::json!({ "jsonrpc": "2.0", "id": id, "result": vec![Value::Null; n] })
193 }
194 _ => serde_json::json!({
195 "jsonrpc": "2.0",
196 "id": id,
197 "error": { "code": -32601, "message": format!("lean-ctx does not handle {method}") }
198 }),
199 };
200 Some(reply)
201}
202
203fn is_credential_env(name: &str) -> bool {
218 const SECRET_MARKERS: &[&str] = &[
219 "TOKEN",
220 "SECRET",
221 "PASSWORD",
222 "PASSWD",
223 "API_KEY",
224 "APIKEY",
225 "ACCESS_KEY",
226 "SECRET_KEY",
227 "PRIVATE_KEY",
228 "CREDENTIALS",
229 "AUTH",
230 "AUTH_CONFIG",
231 ];
232 let upper = name.to_ascii_uppercase();
233 SECRET_MARKERS
234 .iter()
235 .any(|m| upper == *m || upper.ends_with(&format!("_{m}")))
236}
237
238#[derive(Serialize)]
239struct JsonRpcRequest {
240 jsonrpc: &'static str,
241 id: i64,
242 method: String,
243 params: Value,
244}
245
246#[derive(Deserialize)]
247struct JsonRpcResponse {
248 #[serde(rename = "id")]
249 _id: Option<i64>,
250 result: Option<Value>,
251 error: Option<JsonRpcError>,
252}
253
254#[derive(Deserialize)]
255struct JsonRpcError {
256 #[serde(rename = "code")]
257 _code: i64,
258 message: String,
259}
260
261fn read_one_message(reader: &mut BufReader<ChildStdout>) -> Result<Value, String> {
262 let mut content_length = 0usize;
263 loop {
264 let mut line = String::new();
265 let bytes_read = reader
266 .read_line(&mut line)
267 .map_err(|e| format!("Read header: {e}"))?;
268 if bytes_read == 0 {
269 return Err("LSP server closed connection (EOF)".into());
270 }
271 let trimmed = line.trim();
272 if trimmed.is_empty() {
273 break;
274 }
275 if let Some(val) = trimmed.strip_prefix("Content-Length: ") {
276 content_length = val.parse().map_err(|e| format!("Parse length: {e}"))?;
277 }
278 }
279 if content_length == 0 {
280 return Err("Zero content length from LSP server".into());
281 }
282 let mut body = vec![0u8; content_length];
283 std::io::Read::read_exact(reader, &mut body).map_err(|e| format!("Read body: {e}"))?;
284 let text = String::from_utf8_lossy(&body);
285 serde_json::from_str(&text).map_err(|e| format!("Parse response: {e}"))
286}
287
288fn spawn_reader(stdout: ChildStdout, alive: Arc<AtomicBool>) -> Receiver<Result<Value, String>> {
289 let (tx, rx) = mpsc::channel();
290 std::thread::Builder::new()
291 .name("lsp-reader".into())
292 .spawn(move || {
293 let mut reader = BufReader::new(stdout);
294 loop {
295 match read_one_message(&mut reader) {
296 Ok(msg) => {
297 if tx.send(Ok(msg)).is_err() {
298 break;
299 }
300 }
301 Err(e) => {
302 alive.store(false, Ordering::Release);
303 let _ = tx.send(Err(e));
304 break;
305 }
306 }
307 }
308 })
309 .ok();
310 rx
311}
312
313impl LspClient {
314 pub fn start(
318 server: &ResolvedServer,
319 root_uri: &Uri,
320 init_timeout: Option<Duration>,
321 ) -> Result<Self, String> {
322 let config = &server.config;
323 let mut cmd = Command::new(&config.command);
324 cmd.args(&config.args)
325 .stdin(Stdio::piped())
326 .stdout(Stdio::piped())
327 .stderr(Stdio::null());
328 for (name, _) in std::env::vars_os() {
329 if name.to_str().is_some_and(is_credential_env) {
330 cmd.env_remove(&name);
331 }
332 }
333 let mut child = cmd
334 .spawn()
335 .map_err(|e| format!("Failed to start LSP server '{}': {e}", config.command))?;
336
337 let stdin = child.stdin.take().ok_or("No stdin")?;
338 let stdout = child.stdout.take().ok_or("No stdout")?;
339 let alive = Arc::new(AtomicBool::new(true));
340 let response_rx = spawn_reader(stdout, Arc::clone(&alive));
341
342 let mut client = Self {
343 child,
344 stdin,
345 response_rx,
346 next_id: AtomicI64::new(1),
347 initialized: false,
348 alive,
349 info: SemanticBackendInfo {
350 kind: SemanticBackendKind::Lsp,
351 server_name: None,
352 server_version: None,
353 capabilities: SemanticCapabilities::default(),
354 utf8_positions: false,
355 },
356 documents: DocumentStore::default(),
357 request_timeout: Duration::from_secs(REQUEST_TIMEOUT_SECS),
358 root_path: String::new(),
359 };
360
361 client.initialize(
362 root_uri,
363 init_timeout.unwrap_or(Duration::from_secs(INIT_TIMEOUT_SECS)),
364 server.init_options.clone(),
365 )?;
366 if client.info.server_name.is_none() {
369 client.info.server_name = Some(server.binary_name());
370 }
371 Ok(client)
372 }
373
374 fn check_alive(&mut self) -> Result<(), String> {
375 match self.child.try_wait() {
376 Ok(Some(status)) => Err(format!("LSP server exited: {status}")),
377 Ok(None) => Ok(()),
378 Err(e) => Err(format!("Cannot check LSP server status: {e}")),
379 }
380 }
381
382 #[allow(deprecated)]
383 fn initialize(
384 &mut self,
385 root_uri: &Uri,
386 timeout: Duration,
387 init_options: Option<serde_json::Value>,
388 ) -> Result<(), String> {
389 let params = InitializeParams {
390 root_uri: Some(root_uri.clone()),
391 initialization_options: init_options,
392 capabilities: ClientCapabilities {
393 text_document: Some(TextDocumentClientCapabilities {
394 rename: Some(RenameClientCapabilities {
395 dynamic_registration: Some(false),
396 prepare_support: Some(true),
397 ..Default::default()
398 }),
399 references: Some(DynamicRegistrationClientCapabilities {
400 dynamic_registration: Some(false),
401 }),
402 definition: Some(GotoCapability {
403 dynamic_registration: Some(false),
404 link_support: Some(false),
405 }),
406 implementation: Some(GotoCapability {
407 dynamic_registration: Some(false),
408 link_support: Some(false),
409 }),
410 type_hierarchy: Some(DynamicRegistrationClientCapabilities {
411 dynamic_registration: Some(false),
412 }),
413 ..Default::default()
414 }),
415 general: Some(GeneralClientCapabilities {
418 position_encodings: Some(vec![
419 PositionEncodingKind::UTF8,
420 PositionEncodingKind::UTF16,
421 ]),
422 ..Default::default()
423 }),
424 ..Default::default()
425 },
426 ..Default::default()
427 };
428
429 let raw = self
431 .request_raw_with_timeout(
432 <request::Initialize as request::Request>::METHOD,
433 serde_json::to_value(params).map_err(|e| e.to_string())?,
434 timeout,
435 )?
436 .unwrap_or(Value::Null);
437 let result: InitializeResult =
438 serde_json::from_value(raw.clone()).map_err(|e| format!("Deserialize error: {e}"))?;
439 let mut capabilities = SemanticCapabilities::from_server(&result.capabilities);
440 capabilities.type_hierarchy = raw
441 .pointer("/capabilities/typeHierarchyProvider")
442 .is_some_and(|p| p.as_bool() != Some(false) && !p.is_null());
443 self.root_path = uri_to_file_path(root_uri).unwrap_or_default();
444 self.info = SemanticBackendInfo {
445 kind: SemanticBackendKind::Lsp,
446 server_name: result.server_info.as_ref().map(|s| s.name.clone()),
447 server_version: result
448 .server_info
449 .and_then(|s| s.version)
450 .map(|v| crate::lsp::capabilities::compact_server_version(&v)),
451 capabilities,
452 utf8_positions: result.capabilities.position_encoding
453 == Some(PositionEncodingKind::UTF8),
454 };
455 self.send_notification::<notification::Initialized>(InitializedParams {})?;
456 self.initialized = true;
457 Ok(())
458 }
459
460 pub fn info(&self) -> &SemanticBackendInfo {
461 &self.info
462 }
463
464 fn require(&self, supported: bool, method: &str) -> Result<(), String> {
467 if supported {
468 return Ok(());
469 }
470 Err(format!(
471 "UNSUPPORTED: language server '{}' does not provide {method}",
472 self.info.server_name.as_deref().unwrap_or("unknown")
473 ))
474 }
475
476 pub fn did_open(&mut self, uri: &Uri, language_id: &str, text: &str) -> Result<(), String> {
479 self.check_alive()?;
480 let (sync, evicted) = self.documents.sync(uri.as_str(), text);
481 if let Some(old) = evicted
482 && let Ok(old_uri) = old.parse::<Uri>()
483 {
484 self.send_notification::<notification::DidCloseTextDocument>(
485 DidCloseTextDocumentParams {
486 text_document: TextDocumentIdentifier { uri: old_uri },
487 },
488 )?;
489 }
490 match sync {
491 DocumentSync::Unchanged => Ok(()),
492 DocumentSync::Open => self.send_notification::<notification::DidOpenTextDocument>(
493 DidOpenTextDocumentParams {
494 text_document: TextDocumentItem {
495 uri: uri.clone(),
496 language_id: language_id.to_string(),
497 version: 1,
498 text: text.to_string(),
499 },
500 },
501 ),
502 DocumentSync::Change(version) => self
503 .send_notification::<notification::DidChangeTextDocument>(
504 DidChangeTextDocumentParams {
505 text_document: VersionedTextDocumentIdentifier {
506 uri: uri.clone(),
507 version,
508 },
509 content_changes: vec![TextDocumentContentChangeEvent {
510 range: None,
511 range_length: None,
512 text: text.to_string(),
513 }],
514 },
515 ),
516 }
517 }
518
519 pub fn references(&mut self, uri: &Uri, position: Position) -> Result<Vec<Location>, String> {
520 self.check_alive()?;
521 self.require(self.info.capabilities.references, "textDocument/references")?;
522 let params = ReferenceParams {
523 text_document_position: TextDocumentPositionParams {
524 text_document: TextDocumentIdentifier { uri: uri.clone() },
525 position,
526 },
527 context: ReferenceContext {
528 include_declaration: true,
529 },
530 work_done_progress_params: Default::default(),
531 partial_result_params: Default::default(),
532 };
533 let result =
534 self.request_with_timeout::<request::References>(params, self.request_timeout)?;
535 Ok(result.unwrap_or_default())
536 }
537
538 pub fn definition(
539 &mut self,
540 uri: &Uri,
541 position: Position,
542 ) -> Result<GotoDefinitionResponse, String> {
543 self.check_alive()?;
544 self.require(self.info.capabilities.definition, "textDocument/definition")?;
545 let params = GotoDefinitionParams {
546 text_document_position_params: TextDocumentPositionParams {
547 text_document: TextDocumentIdentifier { uri: uri.clone() },
548 position,
549 },
550 work_done_progress_params: Default::default(),
551 partial_result_params: Default::default(),
552 };
553 let result =
554 self.request_with_timeout::<request::GotoDefinition>(params, self.request_timeout)?;
555 Ok(result.unwrap_or(GotoDefinitionResponse::Array(vec![])))
556 }
557
558 pub fn rename(
559 &mut self,
560 uri: &Uri,
561 position: Position,
562 new_name: &str,
563 ) -> Result<Option<WorkspaceEdit>, String> {
564 self.check_alive()?;
565 self.require(self.info.capabilities.rename, "textDocument/rename")?;
566 let params = RenameParams {
567 text_document_position: TextDocumentPositionParams {
568 text_document: TextDocumentIdentifier { uri: uri.clone() },
569 position,
570 },
571 new_name: new_name.to_string(),
572 work_done_progress_params: Default::default(),
573 };
574 self.request_with_timeout::<request::Rename>(params, self.request_timeout)
575 }
576
577 pub fn type_hierarchy(
583 &mut self,
584 uri: &Uri,
585 position: Position,
586 direction: crate::lsp::backend::HierarchyDirection,
587 ) -> Result<crate::lsp::backend::TypeHierarchyNode, String> {
588 use crate::lsp::backend::{HierarchyDirection, TypeHierarchyNode};
589 self.check_alive()?;
590 self.require(
591 self.info.capabilities.type_hierarchy,
592 "textDocument/prepareTypeHierarchy",
593 )?;
594 let items = self
595 .request_with_timeout::<request::TypeHierarchyPrepare>(
596 TypeHierarchyPrepareParams {
597 text_document_position_params: TextDocumentPositionParams {
598 text_document: TextDocumentIdentifier { uri: uri.clone() },
599 position,
600 },
601 work_done_progress_params: Default::default(),
602 },
603 self.request_timeout,
604 )?
605 .unwrap_or_default();
606 let Some(item) = items.into_iter().next() else {
607 return Ok(TypeHierarchyNode {
608 name: String::new(),
609 path: String::new(),
610 line: 0,
611 children: vec![],
612 });
613 };
614 let related = match direction {
615 HierarchyDirection::Supertypes => self
616 .request_with_timeout::<request::TypeHierarchySupertypes>(
617 TypeHierarchySupertypesParams {
618 item: item.clone(),
619 work_done_progress_params: Default::default(),
620 partial_result_params: Default::default(),
621 },
622 self.request_timeout,
623 )?,
624 HierarchyDirection::Subtypes => self
625 .request_with_timeout::<request::TypeHierarchySubtypes>(
626 TypeHierarchySubtypesParams {
627 item: item.clone(),
628 work_done_progress_params: Default::default(),
629 partial_result_params: Default::default(),
630 },
631 self.request_timeout,
632 )?,
633 }
634 .unwrap_or_default();
635 let node = |i: &TypeHierarchyItem, children| TypeHierarchyNode {
636 name: i.name.clone(),
637 path: relative_to_root(&self.root_path, &i.uri),
638 line: i.selection_range.start.line + 1,
639 children,
640 };
641 let children = related.iter().map(|r| node(r, vec![])).collect();
642 Ok(node(&item, children))
643 }
644
645 pub fn implementations(
646 &mut self,
647 uri: &Uri,
648 position: Position,
649 ) -> Result<Vec<Location>, String> {
650 self.check_alive()?;
651 self.require(
652 self.info.capabilities.implementations,
653 "textDocument/implementation",
654 )?;
655 let params = GotoDefinitionParams {
656 text_document_position_params: TextDocumentPositionParams {
657 text_document: TextDocumentIdentifier { uri: uri.clone() },
658 position,
659 },
660 work_done_progress_params: Default::default(),
661 partial_result_params: Default::default(),
662 };
663 let value = self.request_raw_with_timeout(
664 "textDocument/implementation",
665 serde_json::to_value(params).unwrap_or_default(),
666 self.request_timeout,
667 )?;
668 match value {
669 Some(v) => {
670 let locations: Vec<Location> = serde_json::from_value(v).unwrap_or_default();
671 Ok(locations)
672 }
673 None => Ok(vec![]),
674 }
675 }
676
677 fn request_with_timeout<R: request::Request>(
678 &mut self,
679 params: R::Params,
680 timeout: Duration,
681 ) -> Result<R::Result, String>
682 where
683 R::Params: Serialize,
684 R::Result: for<'de> Deserialize<'de>,
685 {
686 let value = self.request_raw_with_timeout(
687 R::METHOD,
688 serde_json::to_value(params).map_err(|e| e.to_string())?,
689 timeout,
690 )?;
691 match value {
692 Some(v) => serde_json::from_value(v).map_err(|e| format!("Deserialize error: {e}")),
693 None => serde_json::from_value(Value::Null).map_err(|e| format!("Null result: {e}")),
694 }
695 }
696
697 fn request_raw_with_timeout(
698 &mut self,
699 method: &str,
700 params: Value,
701 timeout: Duration,
702 ) -> Result<Option<Value>, String> {
703 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
704 let req = JsonRpcRequest {
705 jsonrpc: "2.0",
706 id,
707 method: method.to_string(),
708 params,
709 };
710 self.send_message(&serde_json::to_value(req).map_err(|e| e.to_string())?)?;
711 self.read_response(id, timeout)
712 }
713
714 fn send_notification<N: notification::Notification>(
715 &mut self,
716 params: N::Params,
717 ) -> Result<(), String>
718 where
719 N::Params: Serialize,
720 {
721 let msg = serde_json::json!({
722 "jsonrpc": "2.0",
723 "method": N::METHOD,
724 "params": serde_json::to_value(params).map_err(|e| e.to_string())?
725 });
726 self.send_message(&msg)
727 }
728
729 fn send_message(&mut self, msg: &Value) -> Result<(), String> {
730 let body = serde_json::to_string(msg).map_err(|e| e.to_string())?;
731 let header = format!("Content-Length: {}\r\n\r\n", body.len());
732 self.stdin
733 .write_all(header.as_bytes())
734 .map_err(|e| format!("Write to LSP server: {e}"))?;
735 self.stdin
736 .write_all(body.as_bytes())
737 .map_err(|e| format!("Write to LSP server: {e}"))?;
738 self.stdin
739 .flush()
740 .map_err(|e| format!("Flush LSP server: {e}"))?;
741 Ok(())
742 }
743
744 fn read_response(
745 &mut self,
746 expected_id: i64,
747 timeout: Duration,
748 ) -> Result<Option<Value>, String> {
749 let deadline = Instant::now() + timeout;
750 loop {
751 let remaining = deadline.saturating_duration_since(Instant::now());
752 if remaining.is_zero() {
753 return Err(format!(
754 "LSP response timeout ({}s) for request id={expected_id}",
755 timeout.as_secs()
756 ));
757 }
758
759 match self.response_rx.recv_timeout(remaining) {
760 Ok(Ok(msg)) => match route_incoming(msg, expected_id) {
761 Incoming::FromServer(reply) => {
762 if let Some(reply) = reply {
763 self.send_message(&reply)?;
764 }
765 }
766 Incoming::Response(result) => return result,
767 Incoming::Other => {}
768 },
769 Ok(Err(e)) => return Err(format!("LSP reader error: {e}")),
770 Err(mpsc::RecvTimeoutError::Timeout) => {
771 return Err(format!("LSP response timeout ({}s)", timeout.as_secs()));
772 }
773 Err(mpsc::RecvTimeoutError::Disconnected) => {
774 return Err("LSP server connection lost".into());
775 }
776 }
777 }
778 }
779
780 pub fn shutdown(&mut self) {
781 if self.initialized {
782 let _ = self.request_raw_with_timeout(
783 "shutdown",
784 Value::Null,
785 Duration::from_secs(SHUTDOWN_TIMEOUT_SECS),
786 );
787 let _ = self.send_notification::<notification::Exit>(());
788 self.initialized = false;
789 self.reap(Duration::from_secs(SHUTDOWN_TIMEOUT_SECS));
790 } else {
791 self.reap(Duration::ZERO);
795 }
796 }
797
798 fn reap(&mut self, grace: Duration) {
802 let deadline = Instant::now() + grace;
803 while Instant::now() < deadline {
804 match self.child.try_wait() {
805 Ok(Some(_)) => return,
806 Ok(None) => std::thread::sleep(Duration::from_millis(20)),
807 Err(_) => break,
808 }
809 }
810 let _ = self.child.kill();
811 let _ = self.child.wait();
812 }
813}
814
815impl Drop for LspClient {
816 fn drop(&mut self) {
817 self.shutdown();
818 }
819}
820
821impl crate::lsp::backend::LspBackend for LspClient {
822 fn open_file(
823 &mut self,
824 uri: &lsp_types::Uri,
825 language_id: &str,
826 text: &str,
827 ) -> Result<(), String> {
828 LspClient::did_open(self, uri, language_id, text)
829 }
830 fn references(
831 &mut self,
832 uri: &lsp_types::Uri,
833 position: lsp_types::Position,
834 _scope: &str,
835 ) -> Result<Vec<lsp_types::Location>, String> {
836 LspClient::references(self, uri, position)
837 }
838 fn definition(
839 &mut self,
840 uri: &lsp_types::Uri,
841 position: lsp_types::Position,
842 ) -> Result<lsp_types::GotoDefinitionResponse, String> {
843 LspClient::definition(self, uri, position)
844 }
845 fn implementations(
846 &mut self,
847 uri: &lsp_types::Uri,
848 position: lsp_types::Position,
849 _scope: &str,
850 ) -> Result<Vec<lsp_types::Location>, String> {
851 LspClient::implementations(self, uri, position)
852 }
853 fn rename(
854 &mut self,
855 uri: &lsp_types::Uri,
856 position: lsp_types::Position,
857 new_name: &str,
858 ) -> Result<Option<lsp_types::WorkspaceEdit>, String> {
859 LspClient::rename(self, uri, position, new_name)
860 }
861 fn is_stale(&self, _project_root: &str) -> bool {
864 !self.alive.load(Ordering::Acquire)
865 }
866 fn set_request_timeout(&mut self, timeout: Option<Duration>) {
867 self.request_timeout = timeout.unwrap_or(Duration::from_secs(REQUEST_TIMEOUT_SECS));
868 }
869 fn backend_info(&self) -> SemanticBackendInfo {
870 self.info.clone()
871 }
872 fn type_hierarchy(
873 &mut self,
874 uri: &lsp_types::Uri,
875 position: lsp_types::Position,
876 direction: crate::lsp::backend::HierarchyDirection,
877 ) -> Result<crate::lsp::backend::TypeHierarchyNode, String> {
878 LspClient::type_hierarchy(self, uri, position, direction)
879 }
880 }
882
883fn relative_to_root(root: &str, uri: &Uri) -> String {
886 let path = uri_to_file_path(uri).unwrap_or_default().replace('\\', "/");
887 let root = root.replace('\\', "/");
888 path.strip_prefix(root.trim_end_matches('/'))
889 .and_then(|rest| rest.strip_prefix('/'))
890 .map_or_else(|| path.clone(), str::to_string)
891}
892
893#[cfg(test)]
894mod tests {
895 use super::{DocumentStore, DocumentSync, MAX_OPEN_DOCUMENTS, is_credential_env};
896 use super::{Incoming, route_incoming, server_request_reply};
897
898 #[test]
902 fn file_uris_round_trip_paths_with_special_characters() {
903 use super::{file_path_to_uri, uri_to_file_path};
904 for path in [
905 "/Applications/Visual Studio Code.app/lib.es5.d.ts",
906 "/Users/zoë/My Projects/a#1 (copy)%.rs",
907 "C:/Users/A B/src/main.rs",
908 ] {
909 let uri = file_path_to_uri(path).expect(path);
910 assert!(!uri.as_str().contains(' '), "{}", uri.as_str());
911 assert_eq!(uri_to_file_path(&uri).as_deref(), Some(path));
912 }
913 }
914
915 #[test]
916 fn document_store_opens_once_changes_on_edit_and_stays_bounded() {
917 let mut docs = DocumentStore::default();
918 assert_eq!(docs.sync("file:///a.rs", "v1"), (DocumentSync::Open, None));
919 assert_eq!(
920 docs.sync("file:///a.rs", "v1"),
921 (DocumentSync::Unchanged, None)
922 );
923 assert_eq!(
924 docs.sync("file:///a.rs", "v2"),
925 (DocumentSync::Change(2), None)
926 );
927 for i in 0..MAX_OPEN_DOCUMENTS - 1 {
928 docs.sync(&format!("file:///{i}.rs"), "x");
929 }
930 let (sync, evicted) = docs.sync("file:///overflow.rs", "x");
931 assert_eq!(sync, DocumentSync::Open);
932 assert_eq!(evicted.as_deref(), Some("file:///a.rs"), "oldest is closed");
933 assert_eq!(
934 docs.sync("file:///a.rs", "v2").0,
935 DocumentSync::Open,
936 "a closed document must be reopened, not changed"
937 );
938 }
939
940 #[test]
941 fn server_requests_get_a_reply_and_are_never_mistaken_for_responses() {
942 let config = serde_json::json!({
943 "jsonrpc": "2.0", "id": 1, "method": "workspace/configuration",
944 "params": { "items": [{}, {}] }
945 });
946 let Incoming::FromServer(Some(reply)) = route_incoming(config, 1) else {
949 panic!("server request must be answered, not returned as response");
950 };
951 assert_eq!(reply["id"], 1);
952 assert_eq!(reply["result"], serde_json::json!([null, null]));
953 assert_eq!(
954 route_incoming(
955 serde_json::json!({ "jsonrpc": "2.0", "id": 1, "result": 7 }),
956 1
957 ),
958 Incoming::Response(Ok(Some(serde_json::json!(7))))
959 );
960 assert_eq!(
961 route_incoming(
962 serde_json::json!({ "jsonrpc": "2.0", "id": 9, "result": 7 }),
963 1
964 ),
965 Incoming::Other,
966 "late response to another request is ignored"
967 );
968
969 let unknown = serde_json::json!({ "jsonrpc": "2.0", "id": "x", "method": "foo/bar" });
970 assert_eq!(
971 server_request_reply(&unknown).unwrap()["error"]["code"],
972 -32601
973 );
974
975 let notification = serde_json::json!({ "jsonrpc": "2.0", "method": "$/progress" });
976 assert!(server_request_reply(¬ification).is_none());
977 }
978
979 #[test]
980 fn credential_env_is_scrubbed_but_toolchain_config_is_kept() {
981 for scrubbed in [
982 "GITHUB_TOKEN",
983 "ANTHROPIC_API_KEY",
984 "AWS_SECRET_ACCESS_KEY",
985 "AWS_SESSION_TOKEN",
986 "DB_PASSWORD",
987 "CARGO_REGISTRIES_CORP_TOKEN",
988 "NPM_TOKEN",
989 "DOCKER_AUTH_CONFIG",
990 "token",
991 ] {
992 assert!(is_credential_env(scrubbed), "{scrubbed} must be scrubbed");
993 }
994 for kept in [
995 "PATH",
996 "HOME",
997 "CARGO_HOME",
998 "GOPROXY",
999 "GOPRIVATE",
1000 "GOAUTH",
1001 "SSH_AUTH_SOCK",
1002 "SSL_CERT_FILE",
1003 "VIRTUAL_ENV",
1004 "TOKENIZERS_PARALLELISM",
1005 ] {
1006 assert!(!is_credential_env(kept), "{kept} must be kept");
1007 }
1008 }
1009}