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