1use std::collections::{HashMap, HashSet, VecDeque};
2use std::io::{self, BufRead, BufReader, BufWriter};
3use std::path::{Path, PathBuf};
4#[cfg(windows)]
5use std::process::Command;
6use std::process::{Child, Stdio};
7use std::str::FromStr;
8use std::sync::atomic::{AtomicI64, Ordering};
9use std::sync::{Arc, Mutex};
10use std::thread;
11use std::time::{Duration, Instant};
12
13use crossbeam_channel::{bounded, RecvTimeoutError, Sender};
14use serde::de::DeserializeOwned;
15use serde_json::{json, Value};
16
17use crate::lsp::child_registry::LspChildRegistry;
18use crate::lsp::jsonrpc::{
19 Notification, Request, RequestId, Response as JsonRpcResponse, ServerMessage,
20};
21use crate::lsp::position::path_to_uri;
22use crate::lsp::registry::ServerKind;
23use crate::lsp::{transport, LspError};
24
25const INTERACTIVE_REQUEST_TIMEOUT: Duration = Duration::from_secs(8);
27const HANDSHAKE_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
29const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
30const EXIT_POLL_INTERVAL: Duration = Duration::from_millis(25);
31const STDERR_TAIL_LINES: usize = 64;
32const STDERR_LINE_BYTES: usize = 4 * 1024;
33
34type PendingMap = HashMap<RequestId, Sender<JsonRpcResponse>>;
35type WatchedFileRegistrations = Arc<Mutex<HashSet<String>>>;
36
37#[cfg(windows)]
38fn is_windows_batch_file(path: &Path) -> bool {
39 path.extension()
40 .and_then(|ext| ext.to_str())
41 .is_some_and(|ext| ext.eq_ignore_ascii_case("cmd") || ext.eq_ignore_ascii_case("bat"))
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum ServerState {
47 Starting,
48 Initializing,
49 Ready,
50 ShuttingDown,
51 Exited,
52}
53
54#[derive(Debug)]
56pub enum LspEvent {
57 Notification {
59 server_kind: ServerKind,
60 root: PathBuf,
61 method: String,
62 params: Option<Value>,
63 },
64 ServerRequest {
66 server_kind: ServerKind,
67 root: PathBuf,
68 id: RequestId,
69 method: String,
70 params: Option<Value>,
71 },
72 ServerExited {
74 server_kind: ServerKind,
75 root: PathBuf,
76 },
77}
78
79#[derive(Debug, Clone, Default)]
87pub struct ServerDiagnosticCapabilities {
88 pub pull_diagnostics: bool,
90 pub workspace_diagnostics: bool,
92 pub identifier: Option<String>,
95 pub refresh_support: bool,
99}
100
101pub struct LspClient {
103 kind: ServerKind,
104 root: PathBuf,
105 state: ServerState,
106 child: Child,
107 child_pid: u32,
111 writer: Arc<Mutex<BufWriter<std::process::ChildStdin>>>,
112
113 pending: Arc<Mutex<PendingMap>>,
115 next_id: AtomicI64,
117 diagnostic_caps: Option<ServerDiagnosticCapabilities>,
121 rust_analyzer_quiescent: bool,
125 supports_watched_files: bool,
130 watched_file_registrations: WatchedFileRegistrations,
135 child_registry: LspChildRegistry,
139 stderr_tail: Arc<Mutex<VecDeque<String>>>,
140}
141
142impl LspClient {
143 pub fn spawn(
149 kind: ServerKind,
150 root: PathBuf,
151 binary: &Path,
152 args: &[String],
153 env: &HashMap<String, String>,
154 event_tx: Sender<LspEvent>,
155 child_registry: LspChildRegistry,
156 ) -> io::Result<Self> {
157 Self::spawn_with_reclaim_root(
158 kind,
159 root,
160 binary,
161 args,
162 env,
163 event_tx,
164 child_registry,
165 None,
166 )
167 }
168
169 pub(crate) fn spawn_with_reclaim_root(
171 kind: ServerKind,
172 root: PathBuf,
173 binary: &Path,
174 args: &[String],
175 env: &HashMap<String, String>,
176 event_tx: Sender<LspEvent>,
177 child_registry: LspChildRegistry,
178 reclaim_root: Option<&Path>,
179 ) -> io::Result<Self> {
180 #[cfg(windows)]
181 let mut command = if is_windows_batch_file(binary) {
182 let mut command = Command::new("cmd.exe");
183 command.arg("/C").arg(binary.as_os_str());
184 command
185 } else {
186 Command::new(binary)
187 };
188 #[cfg(not(windows))]
189 let mut command = crate::effective_path::new_command(binary);
190 command
191 .args(args)
192 .current_dir(&root)
193 .stdin(Stdio::piped())
194 .stdout(Stdio::piped())
195 .stderr(Stdio::piped());
198 for (key, value) in env {
199 command.env(key, value);
200 }
201
202 #[cfg(unix)]
208 unsafe {
209 use std::os::unix::process::CommandExt;
210 command.pre_exec(|| {
211 #[cfg(target_os = "linux")]
212 {
213 if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) == -1 {
219 return Err(io::Error::last_os_error());
220 }
221 if libc::getppid() == 1 {
222 return Err(io::Error::other("parent died before LSP spawn completed"));
223 }
224 }
225 if libc::setsid() == -1 {
226 return Err(io::Error::last_os_error());
227 }
228 Ok(())
229 });
230 }
231
232 let mut child = child_registry.spawn_tracked_in_root(&mut command, reclaim_root)?;
233 let child_pid = child.id();
234
235 let stdout = child
236 .stdout
237 .take()
238 .ok_or_else(|| io::Error::other("language server missing stdout pipe"))?;
239 let stdin = child
240 .stdin
241 .take()
242 .ok_or_else(|| io::Error::other("language server missing stdin pipe"))?;
243 let stderr = child
244 .stderr
245 .take()
246 .ok_or_else(|| io::Error::other("language server missing stderr pipe"))?;
247 let stderr_tail = Arc::new(Mutex::new(VecDeque::with_capacity(STDERR_TAIL_LINES)));
248 spawn_stderr_drain_thread(stderr, Arc::clone(&stderr_tail));
249
250 let writer = Arc::new(Mutex::new(BufWriter::new(stdin)));
251 let pending = Arc::new(Mutex::new(PendingMap::new()));
252 let watched_file_registrations = Arc::new(Mutex::new(HashSet::new()));
253 let reader_pending = Arc::clone(&pending);
254 let reader_writer = Arc::clone(&writer);
255 let reader_watched_file_registrations = Arc::clone(&watched_file_registrations);
256 let reader_kind = kind.clone();
257 let reader_root = root.clone();
258
259 thread::spawn(move || {
260 let mut reader = BufReader::new(stdout);
261 loop {
262 match transport::read_message(&mut reader) {
263 Ok(Some(ServerMessage::Response(response))) => {
264 if let Ok(mut guard) = reader_pending.lock() {
265 if let Some(tx) = guard.remove(&response.id) {
266 if tx.send(response).is_err() {
267 log::debug!("response channel closed");
268 }
269 }
270 } else {
271 let _ = event_tx.send(LspEvent::ServerExited {
272 server_kind: reader_kind.clone(),
273 root: reader_root.clone(),
274 });
275 break;
276 }
277 }
278 Ok(Some(ServerMessage::Notification { method, params })) => {
279 let _ = event_tx.send(LspEvent::Notification {
280 server_kind: reader_kind.clone(),
281 root: reader_root.clone(),
282 method,
283 params,
284 });
285 }
286 Ok(Some(ServerMessage::Request { id, method, params })) => {
287 record_watched_file_registration(
288 &reader_watched_file_registrations,
289 &method,
290 params.as_ref(),
291 );
292 let response_value = if method == "workspace/configuration" {
302 let item_count = params
305 .as_ref()
306 .and_then(|p| p.get("items"))
307 .and_then(|items| items.as_array())
308 .map_or(1, |arr| arr.len());
309 serde_json::Value::Array(vec![serde_json::Value::Null; item_count])
310 } else {
311 serde_json::Value::Null
312 };
313 if let Ok(mut w) = reader_writer.lock() {
314 let response = super::jsonrpc::OutgoingResponse::success(
315 id.clone(),
316 response_value,
317 );
318 let _ = transport::write_response(&mut *w, &response);
319 }
320 let _ = event_tx.send(LspEvent::ServerRequest {
322 server_kind: reader_kind.clone(),
323 root: reader_root.clone(),
324 id,
325 method,
326 params,
327 });
328 }
329 Ok(None) | Err(_) => {
330 if let Ok(mut guard) = reader_pending.lock() {
331 guard.clear();
332 }
333 let _ = event_tx.send(LspEvent::ServerExited {
334 server_kind: reader_kind.clone(),
335 root: reader_root.clone(),
336 });
337 break;
338 }
339 }
340 }
341 });
342
343 let rust_analyzer_quiescent = !matches!(&kind, ServerKind::Rust);
344 Ok(Self {
345 kind,
346 root,
347 state: ServerState::Starting,
348 child,
349 child_pid,
350 writer,
351 pending,
352 next_id: AtomicI64::new(1),
353 diagnostic_caps: None,
354 rust_analyzer_quiescent,
355 supports_watched_files: false,
356 watched_file_registrations,
357 child_registry,
358 stderr_tail,
359 })
360 }
361
362 pub fn initialize(
364 &mut self,
365 workspace_root: &Path,
366 initialization_options: Option<serde_json::Value>,
367 ) -> Result<lsp_types::InitializeResult, LspError> {
368 self.ensure_can_send()?;
369 self.state = ServerState::Initializing;
370
371 let root_url = path_to_uri(workspace_root)?;
372 let root_uri = lsp_types::Uri::from_str(root_url.as_str()).map_err(|_| {
373 LspError::NotFound(format!(
374 "failed to convert workspace root '{}' to file URI",
375 workspace_root.display()
376 ))
377 })?;
378
379 let mut params_value = json!({
380 "processId": std::process::id(),
381 "rootUri": root_uri,
382 "capabilities": {
383 "experimental": {
384 "serverStatusNotification": true
385 },
386 "workspace": {
387 "workspaceFolders": true,
388 "configuration": true,
389 "didChangeWatchedFiles": {
390 "dynamicRegistration": true
391 },
392 "diagnostic": {
397 "refreshSupport": false
398 }
399 },
400 "textDocument": {
401 "synchronization": {
402 "dynamicRegistration": false,
403 "didSave": true,
404 "willSave": false,
405 "willSaveWaitUntil": false
406 },
407 "publishDiagnostics": {
408 "relatedInformation": true,
409 "versionSupport": true,
410 "codeDescriptionSupport": true,
411 "dataSupport": true
412 },
413 "diagnostic": {
418 "dynamicRegistration": false,
419 "relatedDocumentSupport": true
420 }
421 }
422 },
423 "clientInfo": {
424 "name": "aft",
425 "version": env!("CARGO_PKG_VERSION")
426 },
427 "workspaceFolders": [
428 {
429 "uri": root_uri,
430 "name": workspace_root
431 .file_name()
432 .and_then(|name| name.to_str())
433 .unwrap_or("workspace")
434 }
435 ]
436 });
437 if let Some(initialization_options) = initialization_options {
438 params_value["initializationOptions"] = initialization_options;
439 }
440
441 let params = serde_json::from_value::<lsp_types::InitializeParams>(params_value)?;
442
443 let result_value = self.send_request_value_with_timeout(
444 <lsp_types::request::Initialize as lsp_types::request::Request>::METHOD,
445 params,
446 HANDSHAKE_REQUEST_TIMEOUT,
447 )?;
448 let result: lsp_types::InitializeResult = serde_json::from_value(result_value.clone())?;
449
450 let caps_value = result_value
455 .get("capabilities")
456 .cloned()
457 .unwrap_or_else(|| serde_json::to_value(&result.capabilities).unwrap_or(Value::Null));
458 self.diagnostic_caps = Some(parse_diagnostic_capabilities(&caps_value));
459
460 self.supports_watched_files = caps_value
466 .pointer("/workspace/didChangeWatchedFiles/dynamicRegistration")
467 .and_then(|v| v.as_bool())
468 .unwrap_or(false)
469 || caps_value
470 .pointer("/workspace/didChangeWatchedFiles")
471 .map(|v| v.is_object() || v.as_bool() == Some(true))
472 .unwrap_or(false);
473
474 self.send_notification::<lsp_types::notification::Initialized>(serde_json::from_value(
475 json!({}),
476 )?)?;
477 self.state = ServerState::Ready;
478 Ok(result)
479 }
480
481 pub fn diagnostic_capabilities(&self) -> Option<&ServerDiagnosticCapabilities> {
485 self.diagnostic_caps.as_ref()
486 }
487
488 pub fn diagnostics_are_provisional(&self) -> bool {
491 matches!(&self.kind, ServerKind::Rust) && !self.rust_analyzer_quiescent
492 }
493
494 pub fn set_rust_analyzer_quiescent(&mut self, quiescent: bool) -> bool {
498 if !matches!(&self.kind, ServerKind::Rust) || !quiescent || self.rust_analyzer_quiescent {
499 return false;
500 }
501 self.rust_analyzer_quiescent = true;
502 true
503 }
504
505 pub fn supports_watched_files(&self) -> bool {
509 self.supports_watched_files
510 }
511
512 pub fn has_watched_file_registration(&self) -> bool {
516 self.watched_file_registrations
517 .lock()
518 .map(|registrations| !registrations.is_empty())
519 .unwrap_or(false)
520 }
521
522 pub fn send_request<R>(&mut self, params: R::Params) -> Result<R::Result, LspError>
524 where
525 R: lsp_types::request::Request,
526 R::Params: serde::Serialize,
527 R::Result: DeserializeOwned,
528 {
529 self.ensure_can_send()?;
530
531 let value = self.send_request_value(R::METHOD, params)?;
532 serde_json::from_value(value).map_err(Into::into)
533 }
534
535 pub fn send_request_with_timeout<R>(
539 &mut self,
540 params: R::Params,
541 timeout: Duration,
542 ) -> Result<R::Result, LspError>
543 where
544 R: lsp_types::request::Request,
545 R::Params: serde::Serialize,
546 R::Result: DeserializeOwned,
547 {
548 self.ensure_can_send()?;
549
550 let value = self.send_request_value_with_timeout(R::METHOD, params, timeout)?;
551 serde_json::from_value(value).map_err(Into::into)
552 }
553
554 fn send_request_value<P>(&mut self, method: &'static str, params: P) -> Result<Value, LspError>
555 where
556 P: serde::Serialize,
557 {
558 self.send_request_value_with_timeout(method, params, INTERACTIVE_REQUEST_TIMEOUT)
559 }
560
561 fn send_request_value_with_timeout<P>(
562 &mut self,
563 method: &'static str,
564 params: P,
565 timeout: Duration,
566 ) -> Result<Value, LspError>
567 where
568 P: serde::Serialize,
569 {
570 self.ensure_can_send()?;
571
572 let id = RequestId::Int(self.next_id.fetch_add(1, Ordering::Relaxed));
573 let (tx, rx) = bounded(1);
574 {
575 let mut pending = self.lock_pending()?;
576 pending.insert(id.clone(), tx);
577 }
578
579 let request = Request::new(id.clone(), method, Some(serde_json::to_value(params)?));
580 {
581 let mut writer = self
582 .writer
583 .lock()
584 .map_err(|_| LspError::ServerNotReady("writer lock poisoned".to_string()))?;
585 if let Err(err) = transport::write_request(&mut *writer, &request) {
586 self.remove_pending(&id);
587 return Err(err.into());
588 }
589 }
590
591 let response = match rx.recv_timeout(timeout) {
592 Ok(response) => response,
593 Err(RecvTimeoutError::Timeout) => {
594 self.remove_pending(&id);
595 self.send_cancel_request(&id)?;
596 return Err(LspError::Timeout(format!(
597 "timed out waiting for '{}' response from {:?}",
598 method, self.kind
599 )));
600 }
601 Err(RecvTimeoutError::Disconnected) => {
602 self.remove_pending(&id);
603 return Err(LspError::ServerNotReady(format!(
604 "language server {:?} disconnected while waiting for '{}'",
605 self.kind, method
606 )));
607 }
608 };
609
610 if let Some(error) = response.error {
611 return Err(LspError::ServerError {
612 code: error.code,
613 message: error.message,
614 });
615 }
616
617 Ok(response.result.unwrap_or(Value::Null))
618 }
619
620 pub fn send_notification<N>(&mut self, params: N::Params) -> Result<(), LspError>
622 where
623 N: lsp_types::notification::Notification,
624 N::Params: serde::Serialize,
625 {
626 self.ensure_can_send()?;
627 let notification = Notification::new(N::METHOD, Some(serde_json::to_value(params)?));
628 let mut writer = self
629 .writer
630 .lock()
631 .map_err(|_| LspError::ServerNotReady("writer lock poisoned".to_string()))?;
632 transport::write_notification(&mut *writer, ¬ification)?;
633 Ok(())
634 }
635
636 pub fn shutdown(&mut self) -> Result<(), LspError> {
638 if self.state == ServerState::Exited {
639 self.child_registry.untrack(self.child_pid);
640 return Ok(());
641 }
642
643 if self.child.try_wait()?.is_some() {
644 self.state = ServerState::Exited;
645 self.child_registry.untrack(self.child_pid);
646 return Ok(());
647 }
648
649 if let Err(err) = self.send_request_with_timeout::<lsp_types::request::Shutdown>(
650 (),
651 HANDSHAKE_REQUEST_TIMEOUT,
652 ) {
653 self.state = ServerState::ShuttingDown;
654 if self.child.try_wait()?.is_some() {
655 self.state = ServerState::Exited;
656 return Ok(());
657 }
658 return Err(err);
659 }
660
661 self.state = ServerState::ShuttingDown;
662
663 if let Err(err) = self.send_notification::<lsp_types::notification::Exit>(()) {
664 if self.child.try_wait()?.is_some() {
665 self.state = ServerState::Exited;
666 return Ok(());
667 }
668 return Err(err);
669 }
670
671 let deadline = Instant::now() + SHUTDOWN_TIMEOUT;
672 loop {
673 if self.child.try_wait()?.is_some() {
674 self.state = ServerState::Exited;
675 return Ok(());
676 }
677 if Instant::now() >= deadline {
678 kill_lsp_child_group(&mut self.child);
682 self.state = ServerState::Exited;
683 return Err(LspError::Timeout(format!(
684 "timed out waiting for {:?} to exit",
685 self.kind
686 )));
687 }
688 thread::sleep(EXIT_POLL_INTERVAL);
689 }
690 }
691
692 pub fn stderr_tail(&self) -> String {
693 self.stderr_tail
694 .lock()
695 .map(|tail| stderr_tail_to_string(&tail))
696 .unwrap_or_default()
697 }
698
699 pub fn child_exited(&mut self) -> bool {
700 self.child.try_wait().ok().flatten().is_some()
701 }
702
703 pub fn child_exit_status(&mut self) -> Option<std::process::ExitStatus> {
704 self.child.try_wait().ok().flatten()
705 }
706
707 pub fn state(&self) -> ServerState {
708 self.state
709 }
710
711 pub fn kind(&self) -> ServerKind {
712 self.kind.clone()
713 }
714
715 pub fn root(&self) -> &Path {
716 &self.root
717 }
718
719 fn ensure_can_send(&self) -> Result<(), LspError> {
720 if matches!(self.state, ServerState::ShuttingDown | ServerState::Exited) {
721 return Err(LspError::ServerNotReady(format!(
722 "language server {:?} is not ready (state: {:?})",
723 self.kind, self.state
724 )));
725 }
726 Ok(())
727 }
728
729 fn lock_pending(&self) -> Result<std::sync::MutexGuard<'_, PendingMap>, LspError> {
730 self.pending
731 .lock()
732 .map_err(|_| io::Error::other("pending response map poisoned").into())
733 }
734
735 fn remove_pending(&self, id: &RequestId) {
736 if let Ok(mut pending) = self.pending.lock() {
737 pending.remove(id);
738 }
739 }
740
741 fn send_cancel_request(&mut self, id: &RequestId) -> Result<(), LspError> {
742 let notification = Notification::new("$/cancelRequest", Some(json!({ "id": id })));
743 let mut writer = self
744 .writer
745 .lock()
746 .map_err(|_| LspError::ServerNotReady("writer lock poisoned".to_string()))?;
747 transport::write_notification(&mut *writer, ¬ification)?;
748 Ok(())
749 }
750}
751
752impl Drop for LspClient {
753 fn drop(&mut self) {
754 self.child_registry.untrack(self.child_pid);
757 kill_lsp_child_group(&mut self.child);
758 }
759}
760
761fn spawn_stderr_drain_thread(
762 stderr: std::process::ChildStderr,
763 stderr_tail: Arc<Mutex<VecDeque<String>>>,
764) {
765 thread::spawn(move || {
766 let mut reader = BufReader::new(stderr);
767 let mut line = String::new();
768
769 loop {
770 line.clear();
771 match reader.read_line(&mut line) {
772 Ok(0) => break,
773 Ok(_) => {
774 if let Ok(mut tail) = stderr_tail.lock() {
775 append_stderr_tail(&mut tail, &line);
776 } else {
777 break;
778 }
779 }
780 Err(_) => break,
781 }
782 }
783 });
784}
785
786fn append_stderr_tail(tail: &mut VecDeque<String>, line: &str) {
787 if tail.len() == STDERR_TAIL_LINES {
788 tail.pop_front();
789 }
790 tail.push_back(trim_stderr_line(line));
791}
792
793fn trim_stderr_line(line: &str) -> String {
794 let line = line.trim_end_matches(|ch| ch == '\r' || ch == '\n');
795 if line.len() <= STDERR_LINE_BYTES {
796 return line.to_string();
797 }
798
799 let mut start = line.len() - STDERR_LINE_BYTES;
800 while start < line.len() && !line.is_char_boundary(start) {
801 start += 1;
802 }
803 format!("...{}", &line[start..])
804}
805
806fn stderr_tail_to_string(tail: &VecDeque<String>) -> String {
807 tail.iter()
808 .map(String::as_str)
809 .collect::<Vec<_>>()
810 .join("\n")
811}
812
813fn kill_lsp_child_group(child: &mut std::process::Child) {
820 #[cfg(unix)]
821 {
822 let pgid = child.id() as i32;
823 crate::bash_background::process::terminate_pgid(pgid, Some(child));
824 let _ = child.wait();
825 }
826 #[cfg(not(unix))]
827 {
828 crate::bash_background::process::terminate_process(child);
829 let _ = child.wait();
830 }
831}
832
833fn record_watched_file_registration(
834 registrations: &WatchedFileRegistrations,
835 method: &str,
836 params: Option<&Value>,
837) {
838 match method {
839 "client/registerCapability" => {
840 let Some(items) = params
841 .and_then(|params| params.get("registrations"))
842 .and_then(|registrations| registrations.as_array())
843 else {
844 return;
845 };
846 if let Ok(mut guard) = registrations.lock() {
847 for item in items {
848 if item.get("method").and_then(Value::as_str)
849 == Some("workspace/didChangeWatchedFiles")
850 {
851 if let Some(id) = item.get("id").and_then(Value::as_str) {
852 guard.insert(id.to_string());
853 }
854 }
855 }
856 }
857 }
858 "client/unregisterCapability" => {
859 let Some(items) = params
860 .and_then(|params| params.get("unregisterations"))
861 .and_then(|registrations| registrations.as_array())
862 else {
863 return;
864 };
865 if let Ok(mut guard) = registrations.lock() {
866 for item in items {
867 if item.get("method").and_then(Value::as_str)
868 == Some("workspace/didChangeWatchedFiles")
869 {
870 if let Some(id) = item.get("id").and_then(Value::as_str) {
871 guard.remove(id);
872 }
873 }
874 }
875 }
876 }
877 _ => {}
878 }
879}
880
881fn parse_diagnostic_capabilities(value: &Value) -> ServerDiagnosticCapabilities {
896 let mut caps = ServerDiagnosticCapabilities::default();
897
898 if let Some(provider) = value.get("diagnosticProvider") {
899 if provider.is_object() || provider.as_bool() == Some(true) {
902 caps.pull_diagnostics = true;
903 }
904
905 if let Some(obj) = provider.as_object() {
906 if obj
907 .get("workspaceDiagnostics")
908 .and_then(|v| v.as_bool())
909 .unwrap_or(false)
910 {
911 caps.workspace_diagnostics = true;
912 }
913 if let Some(identifier) = obj.get("identifier").and_then(|v| v.as_str()) {
914 caps.identifier = Some(identifier.to_string());
915 }
916 }
917 }
918
919 if let Some(refresh) = value
922 .get("workspace")
923 .and_then(|w| w.get("diagnostic"))
924 .and_then(|d| d.get("refreshSupport"))
925 .and_then(|r| r.as_bool())
926 {
927 caps.refresh_support = refresh;
928 }
929
930 caps
931}
932
933#[cfg(test)]
934mod tests {
935 use super::*;
936
937 #[test]
938 fn parse_caps_no_diagnostic_provider() {
939 let value = json!({});
940 let caps = parse_diagnostic_capabilities(&value);
941 assert!(!caps.pull_diagnostics);
942 assert!(!caps.workspace_diagnostics);
943 assert!(caps.identifier.is_none());
944 }
945
946 #[test]
947 fn parse_caps_basic_pull_only() {
948 let value = json!({
949 "diagnosticProvider": {
950 "interFileDependencies": false,
951 "workspaceDiagnostics": false
952 }
953 });
954 let caps = parse_diagnostic_capabilities(&value);
955 assert!(caps.pull_diagnostics);
956 assert!(!caps.workspace_diagnostics);
957 }
958
959 #[test]
960 fn parse_caps_full_pull_with_workspace() {
961 let value = json!({
962 "diagnosticProvider": {
963 "interFileDependencies": true,
964 "workspaceDiagnostics": true,
965 "identifier": "rust-analyzer"
966 }
967 });
968 let caps = parse_diagnostic_capabilities(&value);
969 assert!(caps.pull_diagnostics);
970 assert!(caps.workspace_diagnostics);
971 assert_eq!(caps.identifier.as_deref(), Some("rust-analyzer"));
972 }
973
974 #[test]
975 fn parse_caps_provider_as_bare_true() {
976 let value = json!({
978 "diagnosticProvider": true
979 });
980 let caps = parse_diagnostic_capabilities(&value);
981 assert!(caps.pull_diagnostics);
982 assert!(!caps.workspace_diagnostics);
983 }
984
985 #[test]
986 fn interactive_request_timeout_is_eight_seconds() {
987 assert_eq!(INTERACTIVE_REQUEST_TIMEOUT, Duration::from_secs(8));
988 }
989
990 #[test]
991 fn handshake_request_timeout_remains_thirty_seconds() {
992 assert_eq!(HANDSHAKE_REQUEST_TIMEOUT, Duration::from_secs(30));
993 }
994
995 #[test]
996 fn parse_caps_workspace_refresh_support() {
997 let value = json!({
998 "workspace": {
999 "diagnostic": {
1000 "refreshSupport": true
1001 }
1002 }
1003 });
1004 let caps = parse_diagnostic_capabilities(&value);
1005 assert!(caps.refresh_support);
1006 assert!(!caps.pull_diagnostics);
1008 }
1009}