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 #[cfg(windows)]
158 let mut command = if is_windows_batch_file(binary) {
159 let mut command = Command::new("cmd.exe");
160 command.arg("/C").arg(binary.as_os_str());
161 command
162 } else {
163 Command::new(binary)
164 };
165 #[cfg(not(windows))]
166 let mut command = crate::effective_path::new_command(binary);
167 command
168 .args(args)
169 .current_dir(&root)
170 .stdin(Stdio::piped())
171 .stdout(Stdio::piped())
172 .stderr(Stdio::piped());
175 for (key, value) in env {
176 command.env(key, value);
177 }
178
179 #[cfg(unix)]
185 unsafe {
186 use std::os::unix::process::CommandExt;
187 command.pre_exec(|| {
188 #[cfg(target_os = "linux")]
189 {
190 if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) == -1 {
196 return Err(io::Error::last_os_error());
197 }
198 if libc::getppid() == 1 {
199 return Err(io::Error::other("parent died before LSP spawn completed"));
200 }
201 }
202 if libc::setsid() == -1 {
203 return Err(io::Error::last_os_error());
204 }
205 Ok(())
206 });
207 }
208
209 let mut child = child_registry.spawn_tracked(&mut command)?;
210 let child_pid = child.id();
211
212 let stdout = child
213 .stdout
214 .take()
215 .ok_or_else(|| io::Error::other("language server missing stdout pipe"))?;
216 let stdin = child
217 .stdin
218 .take()
219 .ok_or_else(|| io::Error::other("language server missing stdin pipe"))?;
220 let stderr = child
221 .stderr
222 .take()
223 .ok_or_else(|| io::Error::other("language server missing stderr pipe"))?;
224 let stderr_tail = Arc::new(Mutex::new(VecDeque::with_capacity(STDERR_TAIL_LINES)));
225 spawn_stderr_drain_thread(stderr, Arc::clone(&stderr_tail));
226
227 let writer = Arc::new(Mutex::new(BufWriter::new(stdin)));
228 let pending = Arc::new(Mutex::new(PendingMap::new()));
229 let watched_file_registrations = Arc::new(Mutex::new(HashSet::new()));
230 let reader_pending = Arc::clone(&pending);
231 let reader_writer = Arc::clone(&writer);
232 let reader_watched_file_registrations = Arc::clone(&watched_file_registrations);
233 let reader_kind = kind.clone();
234 let reader_root = root.clone();
235
236 thread::spawn(move || {
237 let mut reader = BufReader::new(stdout);
238 loop {
239 match transport::read_message(&mut reader) {
240 Ok(Some(ServerMessage::Response(response))) => {
241 if let Ok(mut guard) = reader_pending.lock() {
242 if let Some(tx) = guard.remove(&response.id) {
243 if tx.send(response).is_err() {
244 log::debug!("response channel closed");
245 }
246 }
247 } else {
248 let _ = event_tx.send(LspEvent::ServerExited {
249 server_kind: reader_kind.clone(),
250 root: reader_root.clone(),
251 });
252 break;
253 }
254 }
255 Ok(Some(ServerMessage::Notification { method, params })) => {
256 let _ = event_tx.send(LspEvent::Notification {
257 server_kind: reader_kind.clone(),
258 root: reader_root.clone(),
259 method,
260 params,
261 });
262 }
263 Ok(Some(ServerMessage::Request { id, method, params })) => {
264 record_watched_file_registration(
265 &reader_watched_file_registrations,
266 &method,
267 params.as_ref(),
268 );
269 let response_value = if method == "workspace/configuration" {
279 let item_count = params
282 .as_ref()
283 .and_then(|p| p.get("items"))
284 .and_then(|items| items.as_array())
285 .map_or(1, |arr| arr.len());
286 serde_json::Value::Array(vec![serde_json::Value::Null; item_count])
287 } else {
288 serde_json::Value::Null
289 };
290 if let Ok(mut w) = reader_writer.lock() {
291 let response = super::jsonrpc::OutgoingResponse::success(
292 id.clone(),
293 response_value,
294 );
295 let _ = transport::write_response(&mut *w, &response);
296 }
297 let _ = event_tx.send(LspEvent::ServerRequest {
299 server_kind: reader_kind.clone(),
300 root: reader_root.clone(),
301 id,
302 method,
303 params,
304 });
305 }
306 Ok(None) | Err(_) => {
307 if let Ok(mut guard) = reader_pending.lock() {
308 guard.clear();
309 }
310 let _ = event_tx.send(LspEvent::ServerExited {
311 server_kind: reader_kind.clone(),
312 root: reader_root.clone(),
313 });
314 break;
315 }
316 }
317 }
318 });
319
320 let rust_analyzer_quiescent = !matches!(&kind, ServerKind::Rust);
321 Ok(Self {
322 kind,
323 root,
324 state: ServerState::Starting,
325 child,
326 child_pid,
327 writer,
328 pending,
329 next_id: AtomicI64::new(1),
330 diagnostic_caps: None,
331 rust_analyzer_quiescent,
332 supports_watched_files: false,
333 watched_file_registrations,
334 child_registry,
335 stderr_tail,
336 })
337 }
338
339 pub fn initialize(
341 &mut self,
342 workspace_root: &Path,
343 initialization_options: Option<serde_json::Value>,
344 ) -> Result<lsp_types::InitializeResult, LspError> {
345 self.ensure_can_send()?;
346 self.state = ServerState::Initializing;
347
348 let root_url = path_to_uri(workspace_root)?;
349 let root_uri = lsp_types::Uri::from_str(root_url.as_str()).map_err(|_| {
350 LspError::NotFound(format!(
351 "failed to convert workspace root '{}' to file URI",
352 workspace_root.display()
353 ))
354 })?;
355
356 let mut params_value = json!({
357 "processId": std::process::id(),
358 "rootUri": root_uri,
359 "capabilities": {
360 "experimental": {
361 "serverStatusNotification": true
362 },
363 "workspace": {
364 "workspaceFolders": true,
365 "configuration": true,
366 "didChangeWatchedFiles": {
367 "dynamicRegistration": true
368 },
369 "diagnostic": {
374 "refreshSupport": false
375 }
376 },
377 "textDocument": {
378 "synchronization": {
379 "dynamicRegistration": false,
380 "didSave": true,
381 "willSave": false,
382 "willSaveWaitUntil": false
383 },
384 "publishDiagnostics": {
385 "relatedInformation": true,
386 "versionSupport": true,
387 "codeDescriptionSupport": true,
388 "dataSupport": true
389 },
390 "diagnostic": {
395 "dynamicRegistration": false,
396 "relatedDocumentSupport": true
397 }
398 }
399 },
400 "clientInfo": {
401 "name": "aft",
402 "version": env!("CARGO_PKG_VERSION")
403 },
404 "workspaceFolders": [
405 {
406 "uri": root_uri,
407 "name": workspace_root
408 .file_name()
409 .and_then(|name| name.to_str())
410 .unwrap_or("workspace")
411 }
412 ]
413 });
414 if let Some(initialization_options) = initialization_options {
415 params_value["initializationOptions"] = initialization_options;
416 }
417
418 let params = serde_json::from_value::<lsp_types::InitializeParams>(params_value)?;
419
420 let result_value = self.send_request_value_with_timeout(
421 <lsp_types::request::Initialize as lsp_types::request::Request>::METHOD,
422 params,
423 HANDSHAKE_REQUEST_TIMEOUT,
424 )?;
425 let result: lsp_types::InitializeResult = serde_json::from_value(result_value.clone())?;
426
427 let caps_value = result_value
432 .get("capabilities")
433 .cloned()
434 .unwrap_or_else(|| serde_json::to_value(&result.capabilities).unwrap_or(Value::Null));
435 self.diagnostic_caps = Some(parse_diagnostic_capabilities(&caps_value));
436
437 self.supports_watched_files = caps_value
443 .pointer("/workspace/didChangeWatchedFiles/dynamicRegistration")
444 .and_then(|v| v.as_bool())
445 .unwrap_or(false)
446 || caps_value
447 .pointer("/workspace/didChangeWatchedFiles")
448 .map(|v| v.is_object() || v.as_bool() == Some(true))
449 .unwrap_or(false);
450
451 self.send_notification::<lsp_types::notification::Initialized>(serde_json::from_value(
452 json!({}),
453 )?)?;
454 self.state = ServerState::Ready;
455 Ok(result)
456 }
457
458 pub fn diagnostic_capabilities(&self) -> Option<&ServerDiagnosticCapabilities> {
462 self.diagnostic_caps.as_ref()
463 }
464
465 pub fn diagnostics_are_provisional(&self) -> bool {
468 matches!(&self.kind, ServerKind::Rust) && !self.rust_analyzer_quiescent
469 }
470
471 pub fn set_rust_analyzer_quiescent(&mut self, quiescent: bool) -> bool {
475 if !matches!(&self.kind, ServerKind::Rust) || !quiescent || self.rust_analyzer_quiescent {
476 return false;
477 }
478 self.rust_analyzer_quiescent = true;
479 true
480 }
481
482 pub fn supports_watched_files(&self) -> bool {
486 self.supports_watched_files
487 }
488
489 pub fn has_watched_file_registration(&self) -> bool {
493 self.watched_file_registrations
494 .lock()
495 .map(|registrations| !registrations.is_empty())
496 .unwrap_or(false)
497 }
498
499 pub fn send_request<R>(&mut self, params: R::Params) -> Result<R::Result, LspError>
501 where
502 R: lsp_types::request::Request,
503 R::Params: serde::Serialize,
504 R::Result: DeserializeOwned,
505 {
506 self.ensure_can_send()?;
507
508 let value = self.send_request_value(R::METHOD, params)?;
509 serde_json::from_value(value).map_err(Into::into)
510 }
511
512 pub fn send_request_with_timeout<R>(
516 &mut self,
517 params: R::Params,
518 timeout: Duration,
519 ) -> Result<R::Result, LspError>
520 where
521 R: lsp_types::request::Request,
522 R::Params: serde::Serialize,
523 R::Result: DeserializeOwned,
524 {
525 self.ensure_can_send()?;
526
527 let value = self.send_request_value_with_timeout(R::METHOD, params, timeout)?;
528 serde_json::from_value(value).map_err(Into::into)
529 }
530
531 fn send_request_value<P>(&mut self, method: &'static str, params: P) -> Result<Value, LspError>
532 where
533 P: serde::Serialize,
534 {
535 self.send_request_value_with_timeout(method, params, INTERACTIVE_REQUEST_TIMEOUT)
536 }
537
538 fn send_request_value_with_timeout<P>(
539 &mut self,
540 method: &'static str,
541 params: P,
542 timeout: Duration,
543 ) -> Result<Value, LspError>
544 where
545 P: serde::Serialize,
546 {
547 self.ensure_can_send()?;
548
549 let id = RequestId::Int(self.next_id.fetch_add(1, Ordering::Relaxed));
550 let (tx, rx) = bounded(1);
551 {
552 let mut pending = self.lock_pending()?;
553 pending.insert(id.clone(), tx);
554 }
555
556 let request = Request::new(id.clone(), method, Some(serde_json::to_value(params)?));
557 {
558 let mut writer = self
559 .writer
560 .lock()
561 .map_err(|_| LspError::ServerNotReady("writer lock poisoned".to_string()))?;
562 if let Err(err) = transport::write_request(&mut *writer, &request) {
563 self.remove_pending(&id);
564 return Err(err.into());
565 }
566 }
567
568 let response = match rx.recv_timeout(timeout) {
569 Ok(response) => response,
570 Err(RecvTimeoutError::Timeout) => {
571 self.remove_pending(&id);
572 self.send_cancel_request(&id)?;
573 return Err(LspError::Timeout(format!(
574 "timed out waiting for '{}' response from {:?}",
575 method, self.kind
576 )));
577 }
578 Err(RecvTimeoutError::Disconnected) => {
579 self.remove_pending(&id);
580 return Err(LspError::ServerNotReady(format!(
581 "language server {:?} disconnected while waiting for '{}'",
582 self.kind, method
583 )));
584 }
585 };
586
587 if let Some(error) = response.error {
588 return Err(LspError::ServerError {
589 code: error.code,
590 message: error.message,
591 });
592 }
593
594 Ok(response.result.unwrap_or(Value::Null))
595 }
596
597 pub fn send_notification<N>(&mut self, params: N::Params) -> Result<(), LspError>
599 where
600 N: lsp_types::notification::Notification,
601 N::Params: serde::Serialize,
602 {
603 self.ensure_can_send()?;
604 let notification = Notification::new(N::METHOD, Some(serde_json::to_value(params)?));
605 let mut writer = self
606 .writer
607 .lock()
608 .map_err(|_| LspError::ServerNotReady("writer lock poisoned".to_string()))?;
609 transport::write_notification(&mut *writer, ¬ification)?;
610 Ok(())
611 }
612
613 pub fn shutdown(&mut self) -> Result<(), LspError> {
615 if self.state == ServerState::Exited {
616 self.child_registry.untrack(self.child_pid);
617 return Ok(());
618 }
619
620 if self.child.try_wait()?.is_some() {
621 self.state = ServerState::Exited;
622 self.child_registry.untrack(self.child_pid);
623 return Ok(());
624 }
625
626 if let Err(err) = self.send_request_with_timeout::<lsp_types::request::Shutdown>(
627 (),
628 HANDSHAKE_REQUEST_TIMEOUT,
629 ) {
630 self.state = ServerState::ShuttingDown;
631 if self.child.try_wait()?.is_some() {
632 self.state = ServerState::Exited;
633 return Ok(());
634 }
635 return Err(err);
636 }
637
638 self.state = ServerState::ShuttingDown;
639
640 if let Err(err) = self.send_notification::<lsp_types::notification::Exit>(()) {
641 if self.child.try_wait()?.is_some() {
642 self.state = ServerState::Exited;
643 return Ok(());
644 }
645 return Err(err);
646 }
647
648 let deadline = Instant::now() + SHUTDOWN_TIMEOUT;
649 loop {
650 if self.child.try_wait()?.is_some() {
651 self.state = ServerState::Exited;
652 return Ok(());
653 }
654 if Instant::now() >= deadline {
655 kill_lsp_child_group(&mut self.child);
659 self.state = ServerState::Exited;
660 return Err(LspError::Timeout(format!(
661 "timed out waiting for {:?} to exit",
662 self.kind
663 )));
664 }
665 thread::sleep(EXIT_POLL_INTERVAL);
666 }
667 }
668
669 pub fn stderr_tail(&self) -> String {
670 self.stderr_tail
671 .lock()
672 .map(|tail| stderr_tail_to_string(&tail))
673 .unwrap_or_default()
674 }
675
676 pub fn child_exited(&mut self) -> bool {
677 self.child.try_wait().ok().flatten().is_some()
678 }
679
680 pub fn child_exit_status(&mut self) -> Option<std::process::ExitStatus> {
681 self.child.try_wait().ok().flatten()
682 }
683
684 pub fn state(&self) -> ServerState {
685 self.state
686 }
687
688 pub fn kind(&self) -> ServerKind {
689 self.kind.clone()
690 }
691
692 pub fn root(&self) -> &Path {
693 &self.root
694 }
695
696 fn ensure_can_send(&self) -> Result<(), LspError> {
697 if matches!(self.state, ServerState::ShuttingDown | ServerState::Exited) {
698 return Err(LspError::ServerNotReady(format!(
699 "language server {:?} is not ready (state: {:?})",
700 self.kind, self.state
701 )));
702 }
703 Ok(())
704 }
705
706 fn lock_pending(&self) -> Result<std::sync::MutexGuard<'_, PendingMap>, LspError> {
707 self.pending
708 .lock()
709 .map_err(|_| io::Error::other("pending response map poisoned").into())
710 }
711
712 fn remove_pending(&self, id: &RequestId) {
713 if let Ok(mut pending) = self.pending.lock() {
714 pending.remove(id);
715 }
716 }
717
718 fn send_cancel_request(&mut self, id: &RequestId) -> Result<(), LspError> {
719 let notification = Notification::new("$/cancelRequest", Some(json!({ "id": id })));
720 let mut writer = self
721 .writer
722 .lock()
723 .map_err(|_| LspError::ServerNotReady("writer lock poisoned".to_string()))?;
724 transport::write_notification(&mut *writer, ¬ification)?;
725 Ok(())
726 }
727}
728
729impl Drop for LspClient {
730 fn drop(&mut self) {
731 self.child_registry.untrack(self.child_pid);
734 kill_lsp_child_group(&mut self.child);
735 }
736}
737
738fn spawn_stderr_drain_thread(
739 stderr: std::process::ChildStderr,
740 stderr_tail: Arc<Mutex<VecDeque<String>>>,
741) {
742 thread::spawn(move || {
743 let mut reader = BufReader::new(stderr);
744 let mut line = String::new();
745
746 loop {
747 line.clear();
748 match reader.read_line(&mut line) {
749 Ok(0) => break,
750 Ok(_) => {
751 if let Ok(mut tail) = stderr_tail.lock() {
752 append_stderr_tail(&mut tail, &line);
753 } else {
754 break;
755 }
756 }
757 Err(_) => break,
758 }
759 }
760 });
761}
762
763fn append_stderr_tail(tail: &mut VecDeque<String>, line: &str) {
764 if tail.len() == STDERR_TAIL_LINES {
765 tail.pop_front();
766 }
767 tail.push_back(trim_stderr_line(line));
768}
769
770fn trim_stderr_line(line: &str) -> String {
771 let line = line.trim_end_matches(|ch| ch == '\r' || ch == '\n');
772 if line.len() <= STDERR_LINE_BYTES {
773 return line.to_string();
774 }
775
776 let mut start = line.len() - STDERR_LINE_BYTES;
777 while start < line.len() && !line.is_char_boundary(start) {
778 start += 1;
779 }
780 format!("...{}", &line[start..])
781}
782
783fn stderr_tail_to_string(tail: &VecDeque<String>) -> String {
784 tail.iter()
785 .map(String::as_str)
786 .collect::<Vec<_>>()
787 .join("\n")
788}
789
790fn kill_lsp_child_group(child: &mut std::process::Child) {
797 #[cfg(unix)]
798 {
799 let pgid = child.id() as i32;
800 crate::bash_background::process::terminate_pgid(pgid, Some(child));
801 let _ = child.wait();
802 }
803 #[cfg(not(unix))]
804 {
805 crate::bash_background::process::terminate_process(child);
806 let _ = child.wait();
807 }
808}
809
810fn record_watched_file_registration(
811 registrations: &WatchedFileRegistrations,
812 method: &str,
813 params: Option<&Value>,
814) {
815 match method {
816 "client/registerCapability" => {
817 let Some(items) = params
818 .and_then(|params| params.get("registrations"))
819 .and_then(|registrations| registrations.as_array())
820 else {
821 return;
822 };
823 if let Ok(mut guard) = registrations.lock() {
824 for item in items {
825 if item.get("method").and_then(Value::as_str)
826 == Some("workspace/didChangeWatchedFiles")
827 {
828 if let Some(id) = item.get("id").and_then(Value::as_str) {
829 guard.insert(id.to_string());
830 }
831 }
832 }
833 }
834 }
835 "client/unregisterCapability" => {
836 let Some(items) = params
837 .and_then(|params| params.get("unregisterations"))
838 .and_then(|registrations| registrations.as_array())
839 else {
840 return;
841 };
842 if let Ok(mut guard) = registrations.lock() {
843 for item in items {
844 if item.get("method").and_then(Value::as_str)
845 == Some("workspace/didChangeWatchedFiles")
846 {
847 if let Some(id) = item.get("id").and_then(Value::as_str) {
848 guard.remove(id);
849 }
850 }
851 }
852 }
853 }
854 _ => {}
855 }
856}
857
858fn parse_diagnostic_capabilities(value: &Value) -> ServerDiagnosticCapabilities {
873 let mut caps = ServerDiagnosticCapabilities::default();
874
875 if let Some(provider) = value.get("diagnosticProvider") {
876 if provider.is_object() || provider.as_bool() == Some(true) {
879 caps.pull_diagnostics = true;
880 }
881
882 if let Some(obj) = provider.as_object() {
883 if obj
884 .get("workspaceDiagnostics")
885 .and_then(|v| v.as_bool())
886 .unwrap_or(false)
887 {
888 caps.workspace_diagnostics = true;
889 }
890 if let Some(identifier) = obj.get("identifier").and_then(|v| v.as_str()) {
891 caps.identifier = Some(identifier.to_string());
892 }
893 }
894 }
895
896 if let Some(refresh) = value
899 .get("workspace")
900 .and_then(|w| w.get("diagnostic"))
901 .and_then(|d| d.get("refreshSupport"))
902 .and_then(|r| r.as_bool())
903 {
904 caps.refresh_support = refresh;
905 }
906
907 caps
908}
909
910#[cfg(test)]
911mod tests {
912 use super::*;
913
914 #[test]
915 fn parse_caps_no_diagnostic_provider() {
916 let value = json!({});
917 let caps = parse_diagnostic_capabilities(&value);
918 assert!(!caps.pull_diagnostics);
919 assert!(!caps.workspace_diagnostics);
920 assert!(caps.identifier.is_none());
921 }
922
923 #[test]
924 fn parse_caps_basic_pull_only() {
925 let value = json!({
926 "diagnosticProvider": {
927 "interFileDependencies": false,
928 "workspaceDiagnostics": false
929 }
930 });
931 let caps = parse_diagnostic_capabilities(&value);
932 assert!(caps.pull_diagnostics);
933 assert!(!caps.workspace_diagnostics);
934 }
935
936 #[test]
937 fn parse_caps_full_pull_with_workspace() {
938 let value = json!({
939 "diagnosticProvider": {
940 "interFileDependencies": true,
941 "workspaceDiagnostics": true,
942 "identifier": "rust-analyzer"
943 }
944 });
945 let caps = parse_diagnostic_capabilities(&value);
946 assert!(caps.pull_diagnostics);
947 assert!(caps.workspace_diagnostics);
948 assert_eq!(caps.identifier.as_deref(), Some("rust-analyzer"));
949 }
950
951 #[test]
952 fn parse_caps_provider_as_bare_true() {
953 let value = json!({
955 "diagnosticProvider": true
956 });
957 let caps = parse_diagnostic_capabilities(&value);
958 assert!(caps.pull_diagnostics);
959 assert!(!caps.workspace_diagnostics);
960 }
961
962 #[test]
963 fn interactive_request_timeout_is_eight_seconds() {
964 assert_eq!(INTERACTIVE_REQUEST_TIMEOUT, Duration::from_secs(8));
965 }
966
967 #[test]
968 fn handshake_request_timeout_remains_thirty_seconds() {
969 assert_eq!(HANDSHAKE_REQUEST_TIMEOUT, Duration::from_secs(30));
970 }
971
972 #[test]
973 fn parse_caps_workspace_refresh_support() {
974 let value = json!({
975 "workspace": {
976 "diagnostic": {
977 "refreshSupport": true
978 }
979 }
980 });
981 let caps = parse_diagnostic_capabilities(&value);
982 assert!(caps.refresh_support);
983 assert!(!caps.pull_diagnostics);
985 }
986}