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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum ServerState {
40 Starting,
41 Initializing,
42 Ready,
43 ShuttingDown,
44 Exited,
45}
46
47#[derive(Debug)]
49pub enum LspEvent {
50 Notification {
52 server_kind: ServerKind,
53 root: PathBuf,
54 method: String,
55 params: Option<Value>,
56 },
57 ServerRequest {
59 server_kind: ServerKind,
60 root: PathBuf,
61 id: RequestId,
62 method: String,
63 params: Option<Value>,
64 },
65 ServerExited {
67 server_kind: ServerKind,
68 root: PathBuf,
69 },
70}
71
72#[derive(Debug, Clone, Default)]
80pub struct ServerDiagnosticCapabilities {
81 pub pull_diagnostics: bool,
83 pub workspace_diagnostics: bool,
85 pub identifier: Option<String>,
88 pub refresh_support: bool,
92}
93
94pub struct LspClient {
96 kind: ServerKind,
97 root: PathBuf,
98 state: ServerState,
99 child: Child,
100 child_pid: u32,
104 writer: Arc<Mutex<BufWriter<std::process::ChildStdin>>>,
105
106 pending: Arc<Mutex<PendingMap>>,
108 next_id: AtomicI64,
110 diagnostic_caps: Option<ServerDiagnosticCapabilities>,
114 rust_analyzer_quiescent: bool,
118 supports_watched_files: bool,
123 watched_file_registrations: WatchedFileRegistrations,
128 child_registry: LspChildRegistry,
132 stderr_tail: Arc<Mutex<VecDeque<String>>>,
133}
134
135impl LspClient {
136 pub fn spawn(
142 kind: ServerKind,
143 root: PathBuf,
144 binary: &Path,
145 args: &[String],
146 env: &HashMap<String, String>,
147 event_tx: Sender<LspEvent>,
148 child_registry: LspChildRegistry,
149 ) -> io::Result<Self> {
150 Self::spawn_with_reclaim_root(
151 kind,
152 root,
153 binary,
154 args,
155 env,
156 event_tx,
157 child_registry,
158 None,
159 )
160 }
161
162 pub(crate) fn spawn_with_reclaim_root(
164 kind: ServerKind,
165 root: PathBuf,
166 binary: &Path,
167 args: &[String],
168 env: &HashMap<String, String>,
169 event_tx: Sender<LspEvent>,
170 child_registry: LspChildRegistry,
171 reclaim_root: Option<&Path>,
172 ) -> io::Result<Self> {
173 #[cfg(windows)]
174 let is_batch_file = crate::windows_command::is_batch_file(binary);
175 #[cfg(windows)]
176 let mut command = if is_batch_file {
177 crate::windows_command::batch_command(binary, args.iter())?
178 } else {
179 Command::new(binary)
180 };
181 #[cfg(not(windows))]
182 let mut command = crate::effective_path::new_command(binary);
183 #[cfg(windows)]
184 if !is_batch_file {
185 command.args(args);
186 }
187 #[cfg(not(windows))]
188 command.args(args);
189 command
190 .current_dir(&root)
191 .stdin(Stdio::piped())
192 .stdout(Stdio::piped())
193 .stderr(Stdio::piped());
196 for (key, value) in env {
197 #[cfg(windows)]
198 if is_batch_file && crate::windows_command::is_batch_internal_env(key, args.len()) {
199 crate::slog_warn!(
200 "ignoring reserved batch-shim environment variable {key} for LSP server"
201 );
202 continue;
203 }
204 command.env(key, value);
205 }
206
207 #[cfg(unix)]
213 unsafe {
214 use std::os::unix::process::CommandExt;
215 command.pre_exec(|| {
216 #[cfg(target_os = "linux")]
217 {
218 if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) == -1 {
224 return Err(io::Error::last_os_error());
225 }
226 if libc::getppid() == 1 {
227 return Err(io::Error::other("parent died before LSP spawn completed"));
228 }
229 }
230 if libc::setsid() == -1 {
231 return Err(io::Error::last_os_error());
232 }
233 Ok(())
234 });
235 }
236
237 let mut child = child_registry.spawn_tracked_in_root(&mut command, reclaim_root)?;
238 let child_pid = child.id();
239
240 let stdout = child
241 .stdout
242 .take()
243 .ok_or_else(|| io::Error::other("language server missing stdout pipe"))?;
244 let stdin = child
245 .stdin
246 .take()
247 .ok_or_else(|| io::Error::other("language server missing stdin pipe"))?;
248 let stderr = child
249 .stderr
250 .take()
251 .ok_or_else(|| io::Error::other("language server missing stderr pipe"))?;
252 let stderr_tail = Arc::new(Mutex::new(VecDeque::with_capacity(STDERR_TAIL_LINES)));
253 spawn_stderr_drain_thread(stderr, Arc::clone(&stderr_tail));
254
255 let writer = Arc::new(Mutex::new(BufWriter::new(stdin)));
256 let pending = Arc::new(Mutex::new(PendingMap::new()));
257 let watched_file_registrations = Arc::new(Mutex::new(HashSet::new()));
258 let reader_pending = Arc::clone(&pending);
259 let reader_writer = Arc::clone(&writer);
260 let reader_watched_file_registrations = Arc::clone(&watched_file_registrations);
261 let reader_kind = kind.clone();
262 let reader_root = root.clone();
263
264 thread::spawn(move || {
265 let mut reader = BufReader::new(stdout);
266 loop {
267 match transport::read_message(&mut reader) {
268 Ok(Some(ServerMessage::Response(response))) => {
269 if let Ok(mut guard) = reader_pending.lock() {
270 if let Some(tx) = guard.remove(&response.id) {
271 if tx.send(response).is_err() {
272 log::debug!("response channel closed");
273 }
274 }
275 } else {
276 let _ = event_tx.send(LspEvent::ServerExited {
277 server_kind: reader_kind.clone(),
278 root: reader_root.clone(),
279 });
280 break;
281 }
282 }
283 Ok(Some(ServerMessage::Notification { method, params })) => {
284 let _ = event_tx.send(LspEvent::Notification {
285 server_kind: reader_kind.clone(),
286 root: reader_root.clone(),
287 method,
288 params,
289 });
290 }
291 Ok(Some(ServerMessage::Request { id, method, params })) => {
292 record_watched_file_registration(
293 &reader_watched_file_registrations,
294 &method,
295 params.as_ref(),
296 );
297 let response_value = if method == "workspace/configuration" {
307 workspace_configuration_response(
308 &reader_kind,
309 &reader_root,
310 params.as_ref(),
311 )
312 } else {
313 serde_json::Value::Null
314 };
315 if let Ok(mut w) = reader_writer.lock() {
316 let response = super::jsonrpc::OutgoingResponse::success(
317 id.clone(),
318 response_value,
319 );
320 let _ = transport::write_response(&mut *w, &response);
321 }
322 let _ = event_tx.send(LspEvent::ServerRequest {
324 server_kind: reader_kind.clone(),
325 root: reader_root.clone(),
326 id,
327 method,
328 params,
329 });
330 }
331 Ok(None) | Err(_) => {
332 if let Ok(mut guard) = reader_pending.lock() {
333 guard.clear();
334 }
335 let _ = event_tx.send(LspEvent::ServerExited {
336 server_kind: reader_kind.clone(),
337 root: reader_root.clone(),
338 });
339 break;
340 }
341 }
342 }
343 });
344
345 let rust_analyzer_quiescent = !matches!(&kind, ServerKind::Rust);
346 Ok(Self {
347 kind,
348 root,
349 state: ServerState::Starting,
350 child,
351 child_pid,
352 writer,
353 pending,
354 next_id: AtomicI64::new(1),
355 diagnostic_caps: None,
356 rust_analyzer_quiescent,
357 supports_watched_files: false,
358 watched_file_registrations,
359 child_registry,
360 stderr_tail,
361 })
362 }
363
364 pub fn initialize(
366 &mut self,
367 workspace_root: &Path,
368 initialization_options: Option<serde_json::Value>,
369 ) -> Result<lsp_types::InitializeResult, LspError> {
370 self.initialize_with_timeout(
371 workspace_root,
372 initialization_options,
373 HANDSHAKE_REQUEST_TIMEOUT,
374 )
375 }
376
377 pub(crate) fn initialize_with_timeout(
380 &mut self,
381 workspace_root: &Path,
382 initialization_options: Option<serde_json::Value>,
383 timeout: Duration,
384 ) -> Result<lsp_types::InitializeResult, LspError> {
385 self.ensure_can_send()?;
386 self.state = ServerState::Initializing;
387
388 let root_url = path_to_uri(workspace_root)?;
389 let root_uri = lsp_types::Uri::from_str(root_url.as_str()).map_err(|_| {
390 LspError::NotFound(format!(
391 "failed to convert workspace root '{}' to file URI",
392 workspace_root.display()
393 ))
394 })?;
395
396 let mut params_value = json!({
397 "processId": std::process::id(),
398 "rootUri": root_uri,
399 "capabilities": {
400 "experimental": {
401 "serverStatusNotification": true
402 },
403 "workspace": {
404 "workspaceFolders": true,
405 "configuration": true,
406 "didChangeWatchedFiles": {
407 "dynamicRegistration": true
408 },
409 "diagnostic": {
414 "refreshSupport": false
415 }
416 },
417 "textDocument": {
418 "synchronization": {
419 "dynamicRegistration": false,
420 "didSave": true,
421 "willSave": false,
422 "willSaveWaitUntil": false
423 },
424 "publishDiagnostics": {
425 "relatedInformation": true,
426 "versionSupport": true,
427 "codeDescriptionSupport": true,
428 "dataSupport": true
429 },
430 "diagnostic": {
435 "dynamicRegistration": false,
436 "relatedDocumentSupport": true
437 }
438 }
439 },
440 "clientInfo": {
441 "name": "aft",
442 "version": env!("CARGO_PKG_VERSION")
443 },
444 "workspaceFolders": [
445 {
446 "uri": root_uri,
447 "name": workspace_root
448 .file_name()
449 .and_then(|name| name.to_str())
450 .unwrap_or("workspace")
451 }
452 ]
453 });
454 if let Some(initialization_options) = initialization_options {
455 params_value["initializationOptions"] = initialization_options;
456 }
457
458 let params = serde_json::from_value::<lsp_types::InitializeParams>(params_value)?;
459
460 let result_value = self.send_request_value_with_timeout(
461 <lsp_types::request::Initialize as lsp_types::request::Request>::METHOD,
462 params,
463 timeout.min(HANDSHAKE_REQUEST_TIMEOUT),
464 )?;
465 let result: lsp_types::InitializeResult = serde_json::from_value(result_value.clone())?;
466
467 let caps_value = result_value
472 .get("capabilities")
473 .cloned()
474 .unwrap_or_else(|| serde_json::to_value(&result.capabilities).unwrap_or(Value::Null));
475 self.diagnostic_caps = Some(parse_diagnostic_capabilities(&caps_value));
476
477 self.supports_watched_files = caps_value
483 .pointer("/workspace/didChangeWatchedFiles/dynamicRegistration")
484 .and_then(|v| v.as_bool())
485 .unwrap_or(false)
486 || caps_value
487 .pointer("/workspace/didChangeWatchedFiles")
488 .map(|v| v.is_object() || v.as_bool() == Some(true))
489 .unwrap_or(false);
490
491 self.send_notification::<lsp_types::notification::Initialized>(serde_json::from_value(
492 json!({}),
493 )?)?;
494 self.state = ServerState::Ready;
495 Ok(result)
496 }
497
498 pub fn diagnostic_capabilities(&self) -> Option<&ServerDiagnosticCapabilities> {
502 self.diagnostic_caps.as_ref()
503 }
504
505 pub fn diagnostics_are_provisional(&self) -> bool {
508 matches!(&self.kind, ServerKind::Rust) && !self.rust_analyzer_quiescent
509 }
510
511 pub fn set_rust_analyzer_quiescent(&mut self, quiescent: bool) -> bool {
515 if !matches!(&self.kind, ServerKind::Rust) || !quiescent || self.rust_analyzer_quiescent {
516 return false;
517 }
518 self.rust_analyzer_quiescent = true;
519 true
520 }
521
522 pub fn supports_watched_files(&self) -> bool {
526 self.supports_watched_files
527 }
528
529 pub fn has_watched_file_registration(&self) -> bool {
533 self.watched_file_registrations
534 .lock()
535 .map(|registrations| !registrations.is_empty())
536 .unwrap_or(false)
537 }
538
539 pub fn send_request<R>(&mut self, params: R::Params) -> Result<R::Result, LspError>
541 where
542 R: lsp_types::request::Request,
543 R::Params: serde::Serialize,
544 R::Result: DeserializeOwned,
545 {
546 self.ensure_can_send()?;
547
548 let value = self.send_request_value(R::METHOD, params)?;
549 serde_json::from_value(value).map_err(Into::into)
550 }
551
552 pub fn send_request_with_timeout<R>(
556 &mut self,
557 params: R::Params,
558 timeout: Duration,
559 ) -> Result<R::Result, LspError>
560 where
561 R: lsp_types::request::Request,
562 R::Params: serde::Serialize,
563 R::Result: DeserializeOwned,
564 {
565 self.ensure_can_send()?;
566
567 let value = self.send_request_value_with_timeout(R::METHOD, params, timeout)?;
568 serde_json::from_value(value).map_err(Into::into)
569 }
570
571 fn send_request_value<P>(&mut self, method: &'static str, params: P) -> Result<Value, LspError>
572 where
573 P: serde::Serialize,
574 {
575 self.send_request_value_with_timeout(method, params, INTERACTIVE_REQUEST_TIMEOUT)
576 }
577
578 fn send_request_value_with_timeout<P>(
579 &mut self,
580 method: &'static str,
581 params: P,
582 timeout: Duration,
583 ) -> Result<Value, LspError>
584 where
585 P: serde::Serialize,
586 {
587 self.ensure_can_send()?;
588
589 let id = RequestId::Int(self.next_id.fetch_add(1, Ordering::Relaxed));
590 let (tx, rx) = bounded(1);
591 {
592 let mut pending = self.lock_pending()?;
593 pending.insert(id.clone(), tx);
594 }
595
596 let request = Request::new(id.clone(), method, Some(serde_json::to_value(params)?));
597 {
598 let mut writer = self
599 .writer
600 .lock()
601 .map_err(|_| LspError::ServerNotReady("writer lock poisoned".to_string()))?;
602 if let Err(err) = transport::write_request(&mut *writer, &request) {
603 self.remove_pending(&id);
604 return Err(err.into());
605 }
606 }
607
608 let response = match rx.recv_timeout(timeout) {
609 Ok(response) => response,
610 Err(RecvTimeoutError::Timeout) => {
611 self.remove_pending(&id);
612 self.send_cancel_request(&id)?;
613 return Err(LspError::Timeout(format!(
614 "timed out waiting for '{}' response from {:?}",
615 method, self.kind
616 )));
617 }
618 Err(RecvTimeoutError::Disconnected) => {
619 self.remove_pending(&id);
620 return Err(LspError::ServerNotReady(format!(
621 "language server {:?} disconnected while waiting for '{}'",
622 self.kind, method
623 )));
624 }
625 };
626
627 if let Some(error) = response.error {
628 return Err(LspError::ServerError {
629 code: error.code,
630 message: error.message,
631 });
632 }
633
634 Ok(response.result.unwrap_or(Value::Null))
635 }
636
637 pub fn send_notification<N>(&mut self, params: N::Params) -> Result<(), LspError>
639 where
640 N: lsp_types::notification::Notification,
641 N::Params: serde::Serialize,
642 {
643 self.ensure_can_send()?;
644 let notification = Notification::new(N::METHOD, Some(serde_json::to_value(params)?));
645 let mut writer = self
646 .writer
647 .lock()
648 .map_err(|_| LspError::ServerNotReady("writer lock poisoned".to_string()))?;
649 transport::write_notification(&mut *writer, ¬ification)?;
650 Ok(())
651 }
652
653 pub fn shutdown(&mut self) -> Result<(), LspError> {
655 if self.state == ServerState::Exited {
656 self.child_registry.untrack(self.child_pid);
657 return Ok(());
658 }
659
660 if self.child.try_wait()?.is_some() {
661 self.state = ServerState::Exited;
662 self.child_registry.untrack(self.child_pid);
663 return Ok(());
664 }
665
666 if let Err(err) = self.send_request_with_timeout::<lsp_types::request::Shutdown>(
667 (),
668 HANDSHAKE_REQUEST_TIMEOUT,
669 ) {
670 self.state = ServerState::ShuttingDown;
671 if self.child.try_wait()?.is_some() {
672 self.state = ServerState::Exited;
673 return Ok(());
674 }
675 return Err(err);
676 }
677
678 self.state = ServerState::ShuttingDown;
679
680 if let Err(err) = self.send_notification::<lsp_types::notification::Exit>(()) {
681 if self.child.try_wait()?.is_some() {
682 self.state = ServerState::Exited;
683 return Ok(());
684 }
685 return Err(err);
686 }
687
688 let deadline = Instant::now() + SHUTDOWN_TIMEOUT;
689 loop {
690 if self.child.try_wait()?.is_some() {
691 self.state = ServerState::Exited;
692 return Ok(());
693 }
694 if Instant::now() >= deadline {
695 kill_lsp_child_group(&mut self.child);
699 self.state = ServerState::Exited;
700 return Err(LspError::Timeout(format!(
701 "timed out waiting for {:?} to exit",
702 self.kind
703 )));
704 }
705 thread::sleep(EXIT_POLL_INTERVAL);
706 }
707 }
708
709 pub fn stderr_tail(&self) -> String {
710 self.stderr_tail
711 .lock()
712 .map(|tail| stderr_tail_to_string(&tail))
713 .unwrap_or_default()
714 }
715
716 pub fn child_exited(&mut self) -> bool {
717 self.child.try_wait().ok().flatten().is_some()
718 }
719
720 pub fn child_exit_status(&mut self) -> Option<std::process::ExitStatus> {
721 self.child.try_wait().ok().flatten()
722 }
723
724 pub fn state(&self) -> ServerState {
725 self.state
726 }
727
728 pub fn kind(&self) -> ServerKind {
729 self.kind.clone()
730 }
731
732 pub fn root(&self) -> &Path {
733 &self.root
734 }
735
736 fn ensure_can_send(&self) -> Result<(), LspError> {
737 if matches!(self.state, ServerState::ShuttingDown | ServerState::Exited) {
738 return Err(LspError::ServerNotReady(format!(
739 "language server {:?} is not ready (state: {:?})",
740 self.kind, self.state
741 )));
742 }
743 Ok(())
744 }
745
746 fn lock_pending(&self) -> Result<std::sync::MutexGuard<'_, PendingMap>, LspError> {
747 self.pending
748 .lock()
749 .map_err(|_| io::Error::other("pending response map poisoned").into())
750 }
751
752 fn remove_pending(&self, id: &RequestId) {
753 if let Ok(mut pending) = self.pending.lock() {
754 pending.remove(id);
755 }
756 }
757
758 fn send_cancel_request(&mut self, id: &RequestId) -> Result<(), LspError> {
759 let notification = Notification::new("$/cancelRequest", Some(json!({ "id": id })));
760 let mut writer = self
761 .writer
762 .lock()
763 .map_err(|_| LspError::ServerNotReady("writer lock poisoned".to_string()))?;
764 transport::write_notification(&mut *writer, ¬ification)?;
765 Ok(())
766 }
767}
768
769impl Drop for LspClient {
770 fn drop(&mut self) {
771 self.child_registry.untrack(self.child_pid);
774 kill_lsp_child_group(&mut self.child);
775 }
776}
777
778fn spawn_stderr_drain_thread(
779 stderr: std::process::ChildStderr,
780 stderr_tail: Arc<Mutex<VecDeque<String>>>,
781) {
782 thread::spawn(move || {
783 let mut reader = BufReader::new(stderr);
784 let mut line = String::new();
785
786 loop {
787 line.clear();
788 match reader.read_line(&mut line) {
789 Ok(0) => break,
790 Ok(_) => {
791 if let Ok(mut tail) = stderr_tail.lock() {
792 append_stderr_tail(&mut tail, &line);
793 } else {
794 break;
795 }
796 }
797 Err(_) => break,
798 }
799 }
800 });
801}
802
803fn append_stderr_tail(tail: &mut VecDeque<String>, line: &str) {
804 if tail.len() == STDERR_TAIL_LINES {
805 tail.pop_front();
806 }
807 tail.push_back(trim_stderr_line(line));
808}
809
810fn trim_stderr_line(line: &str) -> String {
811 let line = line.trim_end_matches(|ch| ch == '\r' || ch == '\n');
812 if line.len() <= STDERR_LINE_BYTES {
813 return line.to_string();
814 }
815
816 let mut start = line.len() - STDERR_LINE_BYTES;
817 while start < line.len() && !line.is_char_boundary(start) {
818 start += 1;
819 }
820 format!("...{}", &line[start..])
821}
822
823fn stderr_tail_to_string(tail: &VecDeque<String>) -> String {
824 tail.iter()
825 .map(String::as_str)
826 .collect::<Vec<_>>()
827 .join("\n")
828}
829
830fn kill_lsp_child_group(child: &mut std::process::Child) {
837 #[cfg(unix)]
838 {
839 let pgid = child.id() as i32;
840 crate::bash_background::process::terminate_pgid(pgid, Some(child));
841 let _ = child.wait();
842 }
843 #[cfg(not(unix))]
844 {
845 crate::bash_background::process::terminate_process(child);
846 let _ = child.wait();
847 }
848}
849
850fn record_watched_file_registration(
851 registrations: &WatchedFileRegistrations,
852 method: &str,
853 params: Option<&Value>,
854) {
855 match method {
856 "client/registerCapability" => {
857 let Some(items) = params
858 .and_then(|params| params.get("registrations"))
859 .and_then(|registrations| registrations.as_array())
860 else {
861 return;
862 };
863 if let Ok(mut guard) = registrations.lock() {
864 for item in items {
865 if item.get("method").and_then(Value::as_str)
866 == Some("workspace/didChangeWatchedFiles")
867 {
868 if let Some(id) = item.get("id").and_then(Value::as_str) {
869 guard.insert(id.to_string());
870 }
871 }
872 }
873 }
874 }
875 "client/unregisterCapability" => {
876 let Some(items) = params
877 .and_then(|params| params.get("unregisterations"))
878 .and_then(|registrations| registrations.as_array())
879 else {
880 return;
881 };
882 if let Ok(mut guard) = registrations.lock() {
883 for item in items {
884 if item.get("method").and_then(Value::as_str)
885 == Some("workspace/didChangeWatchedFiles")
886 {
887 if let Some(id) = item.get("id").and_then(Value::as_str) {
888 guard.remove(id);
889 }
890 }
891 }
892 }
893 }
894 _ => {}
895 }
896}
897
898fn workspace_configuration_response(
899 kind: &ServerKind,
900 root: &Path,
901 params: Option<&Value>,
902) -> Value {
903 let items = params
904 .and_then(|params| params.get("items"))
905 .and_then(Value::as_array);
906 let python_path = (kind == &ServerKind::Python)
907 .then(|| project_python_path(root))
908 .flatten()
909 .map(|path| path.to_string_lossy().into_owned());
913
914 Value::Array(match items {
915 Some(items) => items
916 .iter()
917 .map(|item| {
918 if item.get("section").and_then(Value::as_str) == Some("python") {
919 if let Some(path) = &python_path {
920 return json!({ "pythonPath": path });
921 }
922 }
923 Value::Null
924 })
925 .collect(),
926 None => vec![Value::Null],
927 })
928}
929
930fn project_python_path(root: &Path) -> Option<PathBuf> {
931 [root.join(".venv"), root.join("venv")]
932 .into_iter()
933 .find_map(|virtualenv| {
934 if cfg!(windows) {
935 let candidate = virtualenv.join("Scripts").join("python.exe");
936 return candidate.is_file().then_some(candidate);
937 }
938
939 ["python", "python3"]
940 .into_iter()
941 .map(|binary| virtualenv.join("bin").join(binary))
942 .find(|candidate| candidate.is_file())
943 })
944}
945
946fn parse_diagnostic_capabilities(value: &Value) -> ServerDiagnosticCapabilities {
961 let mut caps = ServerDiagnosticCapabilities::default();
962
963 if let Some(provider) = value.get("diagnosticProvider") {
964 if provider.is_object() || provider.as_bool() == Some(true) {
967 caps.pull_diagnostics = true;
968 }
969
970 if let Some(obj) = provider.as_object() {
971 if obj
972 .get("workspaceDiagnostics")
973 .and_then(|v| v.as_bool())
974 .unwrap_or(false)
975 {
976 caps.workspace_diagnostics = true;
977 }
978 if let Some(identifier) = obj.get("identifier").and_then(|v| v.as_str()) {
979 caps.identifier = Some(identifier.to_string());
980 }
981 }
982 }
983
984 if let Some(refresh) = value
987 .get("workspace")
988 .and_then(|w| w.get("diagnostic"))
989 .and_then(|d| d.get("refreshSupport"))
990 .and_then(|r| r.as_bool())
991 {
992 caps.refresh_support = refresh;
993 }
994
995 caps
996}
997
998#[cfg(test)]
999mod tests {
1000 use super::*;
1001
1002 #[test]
1003 fn parse_caps_no_diagnostic_provider() {
1004 let value = json!({});
1005 let caps = parse_diagnostic_capabilities(&value);
1006 assert!(!caps.pull_diagnostics);
1007 assert!(!caps.workspace_diagnostics);
1008 assert!(caps.identifier.is_none());
1009 }
1010
1011 #[test]
1012 fn parse_caps_basic_pull_only() {
1013 let value = json!({
1014 "diagnosticProvider": {
1015 "interFileDependencies": false,
1016 "workspaceDiagnostics": false
1017 }
1018 });
1019 let caps = parse_diagnostic_capabilities(&value);
1020 assert!(caps.pull_diagnostics);
1021 assert!(!caps.workspace_diagnostics);
1022 }
1023
1024 #[test]
1025 fn parse_caps_full_pull_with_workspace() {
1026 let value = json!({
1027 "diagnosticProvider": {
1028 "interFileDependencies": true,
1029 "workspaceDiagnostics": true,
1030 "identifier": "rust-analyzer"
1031 }
1032 });
1033 let caps = parse_diagnostic_capabilities(&value);
1034 assert!(caps.pull_diagnostics);
1035 assert!(caps.workspace_diagnostics);
1036 assert_eq!(caps.identifier.as_deref(), Some("rust-analyzer"));
1037 }
1038
1039 #[test]
1040 fn parse_caps_provider_as_bare_true() {
1041 let value = json!({
1043 "diagnosticProvider": true
1044 });
1045 let caps = parse_diagnostic_capabilities(&value);
1046 assert!(caps.pull_diagnostics);
1047 assert!(!caps.workspace_diagnostics);
1048 }
1049
1050 #[test]
1051 fn interactive_request_timeout_is_eight_seconds() {
1052 assert_eq!(INTERACTIVE_REQUEST_TIMEOUT, Duration::from_secs(8));
1053 }
1054
1055 #[test]
1056 fn handshake_request_timeout_remains_thirty_seconds() {
1057 assert_eq!(HANDSHAKE_REQUEST_TIMEOUT, Duration::from_secs(30));
1058 }
1059
1060 #[test]
1061 fn parse_caps_workspace_refresh_support() {
1062 let value = json!({
1063 "workspace": {
1064 "diagnostic": {
1065 "refreshSupport": true
1066 }
1067 }
1068 });
1069 let caps = parse_diagnostic_capabilities(&value);
1070 assert!(caps.refresh_support);
1071 assert!(!caps.pull_diagnostics);
1073 }
1074
1075 #[test]
1076 fn pyright_configuration_uses_workspace_virtualenv_interpreter() {
1077 let tmp = tempfile::tempdir().unwrap();
1078 let root = tmp.path();
1079 let python = if cfg!(windows) {
1080 root.join(".venv").join("Scripts").join("python.exe")
1081 } else {
1082 root.join(".venv").join("bin").join("python")
1083 };
1084 std::fs::create_dir_all(python.parent().unwrap()).unwrap();
1085 std::fs::write(&python, []).unwrap();
1086 let params = json!({
1087 "items": [
1088 { "section": "python" },
1089 { "section": "pyright" }
1090 ]
1091 });
1092
1093 let response = workspace_configuration_response(&ServerKind::Python, root, Some(¶ms));
1094
1095 assert_eq!(response[0]["pythonPath"], python.display().to_string());
1096 assert!(response[1].is_null());
1097 }
1098
1099 #[test]
1100 fn ty_configuration_does_not_receive_pyright_interpreter_settings() {
1101 let tmp = tempfile::tempdir().unwrap();
1102 let root = tmp.path();
1103 let python = if cfg!(windows) {
1104 root.join(".venv").join("Scripts").join("python.exe")
1105 } else {
1106 root.join(".venv").join("bin").join("python")
1107 };
1108 std::fs::create_dir_all(python.parent().unwrap()).unwrap();
1109 std::fs::write(python, []).unwrap();
1110 let params = json!({ "items": [{ "section": "python" }] });
1111
1112 let response = workspace_configuration_response(&ServerKind::Ty, root, Some(¶ms));
1113
1114 assert!(response[0].is_null());
1115 }
1116}