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 workspace_configuration_response(
303 &reader_kind,
304 &reader_root,
305 params.as_ref(),
306 )
307 } else {
308 serde_json::Value::Null
309 };
310 if let Ok(mut w) = reader_writer.lock() {
311 let response = super::jsonrpc::OutgoingResponse::success(
312 id.clone(),
313 response_value,
314 );
315 let _ = transport::write_response(&mut *w, &response);
316 }
317 let _ = event_tx.send(LspEvent::ServerRequest {
319 server_kind: reader_kind.clone(),
320 root: reader_root.clone(),
321 id,
322 method,
323 params,
324 });
325 }
326 Ok(None) | Err(_) => {
327 if let Ok(mut guard) = reader_pending.lock() {
328 guard.clear();
329 }
330 let _ = event_tx.send(LspEvent::ServerExited {
331 server_kind: reader_kind.clone(),
332 root: reader_root.clone(),
333 });
334 break;
335 }
336 }
337 }
338 });
339
340 let rust_analyzer_quiescent = !matches!(&kind, ServerKind::Rust);
341 Ok(Self {
342 kind,
343 root,
344 state: ServerState::Starting,
345 child,
346 child_pid,
347 writer,
348 pending,
349 next_id: AtomicI64::new(1),
350 diagnostic_caps: None,
351 rust_analyzer_quiescent,
352 supports_watched_files: false,
353 watched_file_registrations,
354 child_registry,
355 stderr_tail,
356 })
357 }
358
359 pub fn initialize(
361 &mut self,
362 workspace_root: &Path,
363 initialization_options: Option<serde_json::Value>,
364 ) -> Result<lsp_types::InitializeResult, LspError> {
365 self.ensure_can_send()?;
366 self.state = ServerState::Initializing;
367
368 let root_url = path_to_uri(workspace_root)?;
369 let root_uri = lsp_types::Uri::from_str(root_url.as_str()).map_err(|_| {
370 LspError::NotFound(format!(
371 "failed to convert workspace root '{}' to file URI",
372 workspace_root.display()
373 ))
374 })?;
375
376 let mut params_value = json!({
377 "processId": std::process::id(),
378 "rootUri": root_uri,
379 "capabilities": {
380 "experimental": {
381 "serverStatusNotification": true
382 },
383 "workspace": {
384 "workspaceFolders": true,
385 "configuration": true,
386 "didChangeWatchedFiles": {
387 "dynamicRegistration": true
388 },
389 "diagnostic": {
394 "refreshSupport": false
395 }
396 },
397 "textDocument": {
398 "synchronization": {
399 "dynamicRegistration": false,
400 "didSave": true,
401 "willSave": false,
402 "willSaveWaitUntil": false
403 },
404 "publishDiagnostics": {
405 "relatedInformation": true,
406 "versionSupport": true,
407 "codeDescriptionSupport": true,
408 "dataSupport": true
409 },
410 "diagnostic": {
415 "dynamicRegistration": false,
416 "relatedDocumentSupport": true
417 }
418 }
419 },
420 "clientInfo": {
421 "name": "aft",
422 "version": env!("CARGO_PKG_VERSION")
423 },
424 "workspaceFolders": [
425 {
426 "uri": root_uri,
427 "name": workspace_root
428 .file_name()
429 .and_then(|name| name.to_str())
430 .unwrap_or("workspace")
431 }
432 ]
433 });
434 if let Some(initialization_options) = initialization_options {
435 params_value["initializationOptions"] = initialization_options;
436 }
437
438 let params = serde_json::from_value::<lsp_types::InitializeParams>(params_value)?;
439
440 let result_value = self.send_request_value_with_timeout(
441 <lsp_types::request::Initialize as lsp_types::request::Request>::METHOD,
442 params,
443 HANDSHAKE_REQUEST_TIMEOUT,
444 )?;
445 let result: lsp_types::InitializeResult = serde_json::from_value(result_value.clone())?;
446
447 let caps_value = result_value
452 .get("capabilities")
453 .cloned()
454 .unwrap_or_else(|| serde_json::to_value(&result.capabilities).unwrap_or(Value::Null));
455 self.diagnostic_caps = Some(parse_diagnostic_capabilities(&caps_value));
456
457 self.supports_watched_files = caps_value
463 .pointer("/workspace/didChangeWatchedFiles/dynamicRegistration")
464 .and_then(|v| v.as_bool())
465 .unwrap_or(false)
466 || caps_value
467 .pointer("/workspace/didChangeWatchedFiles")
468 .map(|v| v.is_object() || v.as_bool() == Some(true))
469 .unwrap_or(false);
470
471 self.send_notification::<lsp_types::notification::Initialized>(serde_json::from_value(
472 json!({}),
473 )?)?;
474 self.state = ServerState::Ready;
475 Ok(result)
476 }
477
478 pub fn diagnostic_capabilities(&self) -> Option<&ServerDiagnosticCapabilities> {
482 self.diagnostic_caps.as_ref()
483 }
484
485 pub fn diagnostics_are_provisional(&self) -> bool {
488 matches!(&self.kind, ServerKind::Rust) && !self.rust_analyzer_quiescent
489 }
490
491 pub fn set_rust_analyzer_quiescent(&mut self, quiescent: bool) -> bool {
495 if !matches!(&self.kind, ServerKind::Rust) || !quiescent || self.rust_analyzer_quiescent {
496 return false;
497 }
498 self.rust_analyzer_quiescent = true;
499 true
500 }
501
502 pub fn supports_watched_files(&self) -> bool {
506 self.supports_watched_files
507 }
508
509 pub fn has_watched_file_registration(&self) -> bool {
513 self.watched_file_registrations
514 .lock()
515 .map(|registrations| !registrations.is_empty())
516 .unwrap_or(false)
517 }
518
519 pub fn send_request<R>(&mut self, params: R::Params) -> Result<R::Result, LspError>
521 where
522 R: lsp_types::request::Request,
523 R::Params: serde::Serialize,
524 R::Result: DeserializeOwned,
525 {
526 self.ensure_can_send()?;
527
528 let value = self.send_request_value(R::METHOD, params)?;
529 serde_json::from_value(value).map_err(Into::into)
530 }
531
532 pub fn send_request_with_timeout<R>(
536 &mut self,
537 params: R::Params,
538 timeout: Duration,
539 ) -> Result<R::Result, LspError>
540 where
541 R: lsp_types::request::Request,
542 R::Params: serde::Serialize,
543 R::Result: DeserializeOwned,
544 {
545 self.ensure_can_send()?;
546
547 let value = self.send_request_value_with_timeout(R::METHOD, params, timeout)?;
548 serde_json::from_value(value).map_err(Into::into)
549 }
550
551 fn send_request_value<P>(&mut self, method: &'static str, params: P) -> Result<Value, LspError>
552 where
553 P: serde::Serialize,
554 {
555 self.send_request_value_with_timeout(method, params, INTERACTIVE_REQUEST_TIMEOUT)
556 }
557
558 fn send_request_value_with_timeout<P>(
559 &mut self,
560 method: &'static str,
561 params: P,
562 timeout: Duration,
563 ) -> Result<Value, LspError>
564 where
565 P: serde::Serialize,
566 {
567 self.ensure_can_send()?;
568
569 let id = RequestId::Int(self.next_id.fetch_add(1, Ordering::Relaxed));
570 let (tx, rx) = bounded(1);
571 {
572 let mut pending = self.lock_pending()?;
573 pending.insert(id.clone(), tx);
574 }
575
576 let request = Request::new(id.clone(), method, Some(serde_json::to_value(params)?));
577 {
578 let mut writer = self
579 .writer
580 .lock()
581 .map_err(|_| LspError::ServerNotReady("writer lock poisoned".to_string()))?;
582 if let Err(err) = transport::write_request(&mut *writer, &request) {
583 self.remove_pending(&id);
584 return Err(err.into());
585 }
586 }
587
588 let response = match rx.recv_timeout(timeout) {
589 Ok(response) => response,
590 Err(RecvTimeoutError::Timeout) => {
591 self.remove_pending(&id);
592 self.send_cancel_request(&id)?;
593 return Err(LspError::Timeout(format!(
594 "timed out waiting for '{}' response from {:?}",
595 method, self.kind
596 )));
597 }
598 Err(RecvTimeoutError::Disconnected) => {
599 self.remove_pending(&id);
600 return Err(LspError::ServerNotReady(format!(
601 "language server {:?} disconnected while waiting for '{}'",
602 self.kind, method
603 )));
604 }
605 };
606
607 if let Some(error) = response.error {
608 return Err(LspError::ServerError {
609 code: error.code,
610 message: error.message,
611 });
612 }
613
614 Ok(response.result.unwrap_or(Value::Null))
615 }
616
617 pub fn send_notification<N>(&mut self, params: N::Params) -> Result<(), LspError>
619 where
620 N: lsp_types::notification::Notification,
621 N::Params: serde::Serialize,
622 {
623 self.ensure_can_send()?;
624 let notification = Notification::new(N::METHOD, Some(serde_json::to_value(params)?));
625 let mut writer = self
626 .writer
627 .lock()
628 .map_err(|_| LspError::ServerNotReady("writer lock poisoned".to_string()))?;
629 transport::write_notification(&mut *writer, ¬ification)?;
630 Ok(())
631 }
632
633 pub fn shutdown(&mut self) -> Result<(), LspError> {
635 if self.state == ServerState::Exited {
636 self.child_registry.untrack(self.child_pid);
637 return Ok(());
638 }
639
640 if self.child.try_wait()?.is_some() {
641 self.state = ServerState::Exited;
642 self.child_registry.untrack(self.child_pid);
643 return Ok(());
644 }
645
646 if let Err(err) = self.send_request_with_timeout::<lsp_types::request::Shutdown>(
647 (),
648 HANDSHAKE_REQUEST_TIMEOUT,
649 ) {
650 self.state = ServerState::ShuttingDown;
651 if self.child.try_wait()?.is_some() {
652 self.state = ServerState::Exited;
653 return Ok(());
654 }
655 return Err(err);
656 }
657
658 self.state = ServerState::ShuttingDown;
659
660 if let Err(err) = self.send_notification::<lsp_types::notification::Exit>(()) {
661 if self.child.try_wait()?.is_some() {
662 self.state = ServerState::Exited;
663 return Ok(());
664 }
665 return Err(err);
666 }
667
668 let deadline = Instant::now() + SHUTDOWN_TIMEOUT;
669 loop {
670 if self.child.try_wait()?.is_some() {
671 self.state = ServerState::Exited;
672 return Ok(());
673 }
674 if Instant::now() >= deadline {
675 kill_lsp_child_group(&mut self.child);
679 self.state = ServerState::Exited;
680 return Err(LspError::Timeout(format!(
681 "timed out waiting for {:?} to exit",
682 self.kind
683 )));
684 }
685 thread::sleep(EXIT_POLL_INTERVAL);
686 }
687 }
688
689 pub fn stderr_tail(&self) -> String {
690 self.stderr_tail
691 .lock()
692 .map(|tail| stderr_tail_to_string(&tail))
693 .unwrap_or_default()
694 }
695
696 pub fn child_exited(&mut self) -> bool {
697 self.child.try_wait().ok().flatten().is_some()
698 }
699
700 pub fn child_exit_status(&mut self) -> Option<std::process::ExitStatus> {
701 self.child.try_wait().ok().flatten()
702 }
703
704 pub fn state(&self) -> ServerState {
705 self.state
706 }
707
708 pub fn kind(&self) -> ServerKind {
709 self.kind.clone()
710 }
711
712 pub fn root(&self) -> &Path {
713 &self.root
714 }
715
716 fn ensure_can_send(&self) -> Result<(), LspError> {
717 if matches!(self.state, ServerState::ShuttingDown | ServerState::Exited) {
718 return Err(LspError::ServerNotReady(format!(
719 "language server {:?} is not ready (state: {:?})",
720 self.kind, self.state
721 )));
722 }
723 Ok(())
724 }
725
726 fn lock_pending(&self) -> Result<std::sync::MutexGuard<'_, PendingMap>, LspError> {
727 self.pending
728 .lock()
729 .map_err(|_| io::Error::other("pending response map poisoned").into())
730 }
731
732 fn remove_pending(&self, id: &RequestId) {
733 if let Ok(mut pending) = self.pending.lock() {
734 pending.remove(id);
735 }
736 }
737
738 fn send_cancel_request(&mut self, id: &RequestId) -> Result<(), LspError> {
739 let notification = Notification::new("$/cancelRequest", Some(json!({ "id": id })));
740 let mut writer = self
741 .writer
742 .lock()
743 .map_err(|_| LspError::ServerNotReady("writer lock poisoned".to_string()))?;
744 transport::write_notification(&mut *writer, ¬ification)?;
745 Ok(())
746 }
747}
748
749impl Drop for LspClient {
750 fn drop(&mut self) {
751 self.child_registry.untrack(self.child_pid);
754 kill_lsp_child_group(&mut self.child);
755 }
756}
757
758fn spawn_stderr_drain_thread(
759 stderr: std::process::ChildStderr,
760 stderr_tail: Arc<Mutex<VecDeque<String>>>,
761) {
762 thread::spawn(move || {
763 let mut reader = BufReader::new(stderr);
764 let mut line = String::new();
765
766 loop {
767 line.clear();
768 match reader.read_line(&mut line) {
769 Ok(0) => break,
770 Ok(_) => {
771 if let Ok(mut tail) = stderr_tail.lock() {
772 append_stderr_tail(&mut tail, &line);
773 } else {
774 break;
775 }
776 }
777 Err(_) => break,
778 }
779 }
780 });
781}
782
783fn append_stderr_tail(tail: &mut VecDeque<String>, line: &str) {
784 if tail.len() == STDERR_TAIL_LINES {
785 tail.pop_front();
786 }
787 tail.push_back(trim_stderr_line(line));
788}
789
790fn trim_stderr_line(line: &str) -> String {
791 let line = line.trim_end_matches(|ch| ch == '\r' || ch == '\n');
792 if line.len() <= STDERR_LINE_BYTES {
793 return line.to_string();
794 }
795
796 let mut start = line.len() - STDERR_LINE_BYTES;
797 while start < line.len() && !line.is_char_boundary(start) {
798 start += 1;
799 }
800 format!("...{}", &line[start..])
801}
802
803fn stderr_tail_to_string(tail: &VecDeque<String>) -> String {
804 tail.iter()
805 .map(String::as_str)
806 .collect::<Vec<_>>()
807 .join("\n")
808}
809
810fn kill_lsp_child_group(child: &mut std::process::Child) {
817 #[cfg(unix)]
818 {
819 let pgid = child.id() as i32;
820 crate::bash_background::process::terminate_pgid(pgid, Some(child));
821 let _ = child.wait();
822 }
823 #[cfg(not(unix))]
824 {
825 crate::bash_background::process::terminate_process(child);
826 let _ = child.wait();
827 }
828}
829
830fn record_watched_file_registration(
831 registrations: &WatchedFileRegistrations,
832 method: &str,
833 params: Option<&Value>,
834) {
835 match method {
836 "client/registerCapability" => {
837 let Some(items) = params
838 .and_then(|params| params.get("registrations"))
839 .and_then(|registrations| registrations.as_array())
840 else {
841 return;
842 };
843 if let Ok(mut guard) = registrations.lock() {
844 for item in items {
845 if item.get("method").and_then(Value::as_str)
846 == Some("workspace/didChangeWatchedFiles")
847 {
848 if let Some(id) = item.get("id").and_then(Value::as_str) {
849 guard.insert(id.to_string());
850 }
851 }
852 }
853 }
854 }
855 "client/unregisterCapability" => {
856 let Some(items) = params
857 .and_then(|params| params.get("unregisterations"))
858 .and_then(|registrations| registrations.as_array())
859 else {
860 return;
861 };
862 if let Ok(mut guard) = registrations.lock() {
863 for item in items {
864 if item.get("method").and_then(Value::as_str)
865 == Some("workspace/didChangeWatchedFiles")
866 {
867 if let Some(id) = item.get("id").and_then(Value::as_str) {
868 guard.remove(id);
869 }
870 }
871 }
872 }
873 }
874 _ => {}
875 }
876}
877
878fn workspace_configuration_response(
879 kind: &ServerKind,
880 root: &Path,
881 params: Option<&Value>,
882) -> Value {
883 let items = params
884 .and_then(|params| params.get("items"))
885 .and_then(Value::as_array);
886 let python_path = (kind == &ServerKind::Python)
887 .then(|| project_python_path(root))
888 .flatten()
889 .map(|path| path.to_string_lossy().into_owned());
893
894 Value::Array(match items {
895 Some(items) => items
896 .iter()
897 .map(|item| {
898 if item.get("section").and_then(Value::as_str) == Some("python") {
899 if let Some(path) = &python_path {
900 return json!({ "pythonPath": path });
901 }
902 }
903 Value::Null
904 })
905 .collect(),
906 None => vec![Value::Null],
907 })
908}
909
910fn project_python_path(root: &Path) -> Option<PathBuf> {
911 [root.join(".venv"), root.join("venv")]
912 .into_iter()
913 .find_map(|virtualenv| {
914 if cfg!(windows) {
915 let candidate = virtualenv.join("Scripts").join("python.exe");
916 return candidate.is_file().then_some(candidate);
917 }
918
919 ["python", "python3"]
920 .into_iter()
921 .map(|binary| virtualenv.join("bin").join(binary))
922 .find(|candidate| candidate.is_file())
923 })
924}
925
926fn parse_diagnostic_capabilities(value: &Value) -> ServerDiagnosticCapabilities {
941 let mut caps = ServerDiagnosticCapabilities::default();
942
943 if let Some(provider) = value.get("diagnosticProvider") {
944 if provider.is_object() || provider.as_bool() == Some(true) {
947 caps.pull_diagnostics = true;
948 }
949
950 if let Some(obj) = provider.as_object() {
951 if obj
952 .get("workspaceDiagnostics")
953 .and_then(|v| v.as_bool())
954 .unwrap_or(false)
955 {
956 caps.workspace_diagnostics = true;
957 }
958 if let Some(identifier) = obj.get("identifier").and_then(|v| v.as_str()) {
959 caps.identifier = Some(identifier.to_string());
960 }
961 }
962 }
963
964 if let Some(refresh) = value
967 .get("workspace")
968 .and_then(|w| w.get("diagnostic"))
969 .and_then(|d| d.get("refreshSupport"))
970 .and_then(|r| r.as_bool())
971 {
972 caps.refresh_support = refresh;
973 }
974
975 caps
976}
977
978#[cfg(test)]
979mod tests {
980 use super::*;
981
982 #[test]
983 fn parse_caps_no_diagnostic_provider() {
984 let value = json!({});
985 let caps = parse_diagnostic_capabilities(&value);
986 assert!(!caps.pull_diagnostics);
987 assert!(!caps.workspace_diagnostics);
988 assert!(caps.identifier.is_none());
989 }
990
991 #[test]
992 fn parse_caps_basic_pull_only() {
993 let value = json!({
994 "diagnosticProvider": {
995 "interFileDependencies": false,
996 "workspaceDiagnostics": false
997 }
998 });
999 let caps = parse_diagnostic_capabilities(&value);
1000 assert!(caps.pull_diagnostics);
1001 assert!(!caps.workspace_diagnostics);
1002 }
1003
1004 #[test]
1005 fn parse_caps_full_pull_with_workspace() {
1006 let value = json!({
1007 "diagnosticProvider": {
1008 "interFileDependencies": true,
1009 "workspaceDiagnostics": true,
1010 "identifier": "rust-analyzer"
1011 }
1012 });
1013 let caps = parse_diagnostic_capabilities(&value);
1014 assert!(caps.pull_diagnostics);
1015 assert!(caps.workspace_diagnostics);
1016 assert_eq!(caps.identifier.as_deref(), Some("rust-analyzer"));
1017 }
1018
1019 #[test]
1020 fn parse_caps_provider_as_bare_true() {
1021 let value = json!({
1023 "diagnosticProvider": true
1024 });
1025 let caps = parse_diagnostic_capabilities(&value);
1026 assert!(caps.pull_diagnostics);
1027 assert!(!caps.workspace_diagnostics);
1028 }
1029
1030 #[test]
1031 fn interactive_request_timeout_is_eight_seconds() {
1032 assert_eq!(INTERACTIVE_REQUEST_TIMEOUT, Duration::from_secs(8));
1033 }
1034
1035 #[test]
1036 fn handshake_request_timeout_remains_thirty_seconds() {
1037 assert_eq!(HANDSHAKE_REQUEST_TIMEOUT, Duration::from_secs(30));
1038 }
1039
1040 #[test]
1041 fn parse_caps_workspace_refresh_support() {
1042 let value = json!({
1043 "workspace": {
1044 "diagnostic": {
1045 "refreshSupport": true
1046 }
1047 }
1048 });
1049 let caps = parse_diagnostic_capabilities(&value);
1050 assert!(caps.refresh_support);
1051 assert!(!caps.pull_diagnostics);
1053 }
1054
1055 #[test]
1056 fn pyright_configuration_uses_workspace_virtualenv_interpreter() {
1057 let tmp = tempfile::tempdir().unwrap();
1058 let root = tmp.path();
1059 let python = if cfg!(windows) {
1060 root.join(".venv").join("Scripts").join("python.exe")
1061 } else {
1062 root.join(".venv").join("bin").join("python")
1063 };
1064 std::fs::create_dir_all(python.parent().unwrap()).unwrap();
1065 std::fs::write(&python, []).unwrap();
1066 let params = json!({
1067 "items": [
1068 { "section": "python" },
1069 { "section": "pyright" }
1070 ]
1071 });
1072
1073 let response = workspace_configuration_response(&ServerKind::Python, root, Some(¶ms));
1074
1075 assert_eq!(response[0]["pythonPath"], python.display().to_string());
1076 assert!(response[1].is_null());
1077 }
1078
1079 #[test]
1080 fn ty_configuration_does_not_receive_pyright_interpreter_settings() {
1081 let tmp = tempfile::tempdir().unwrap();
1082 let root = tmp.path();
1083 let python = if cfg!(windows) {
1084 root.join(".venv").join("Scripts").join("python.exe")
1085 } else {
1086 root.join(".venv").join("bin").join("python")
1087 };
1088 std::fs::create_dir_all(python.parent().unwrap()).unwrap();
1089 std::fs::write(python, []).unwrap();
1090 let params = json!({ "items": [{ "section": "python" }] });
1091
1092 let response = workspace_configuration_response(&ServerKind::Ty, root, Some(¶ms));
1093
1094 assert!(response[0].is_null());
1095 }
1096}