1use anyhow::{Result, anyhow};
2use async_trait::async_trait;
3use dap::{DapLocator, DebugRequest};
4use serde_json::Value;
5use smol::{
6 io::AsyncReadExt,
7 process::{Command, Stdio},
8};
9use task::SpawnInTerminal;
10
11pub(crate) struct CargoLocator;
12
13async fn find_best_executable(executables: &[String], test_name: &str) -> Option<String> {
14 if executables.len() == 1 {
15 return executables.first().cloned();
16 }
17 for executable in executables {
18 let Some(mut child) = Command::new(&executable)
19 .arg("--list")
20 .stdout(Stdio::piped())
21 .spawn()
22 .ok()
23 else {
24 continue;
25 };
26 let mut test_lines = String::default();
27 if let Some(mut stdout) = child.stdout.take() {
28 stdout.read_to_string(&mut test_lines).await.ok();
29 for line in test_lines.lines() {
30 if line.contains(&test_name) {
31 return Some(executable.clone());
32 }
33 }
34 }
35 }
36 None
37}
38#[async_trait]
39impl DapLocator for CargoLocator {
40 fn accepts(&self, build_config: &SpawnInTerminal) -> bool {
41 if build_config.command != "cargo" {
42 return false;
43 }
44 let Some(command) = build_config.args.first().map(|s| s.as_str()) else {
45 return false;
46 };
47 if matches!(command, "check" | "run") {
48 return false;
49 }
50 !matches!(command, "test" | "bench")
51 || build_config.args.iter().any(|arg| arg == "--no-run")
52 }
53
54 async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest> {
55 let Some(cwd) = build_config.cwd.clone() else {
56 return Err(anyhow!(
57 "Couldn't get cwd from debug config which is needed for locators"
58 ));
59 };
60
61 let mut child = Command::new("cargo")
62 .args(&build_config.args)
63 .arg("--message-format=json")
64 .envs(build_config.env.iter().map(|(k, v)| (k.clone(), v.clone())))
65 .current_dir(cwd)
66 .stdout(Stdio::piped())
67 .spawn()?;
68
69 let mut output = String::new();
70 if let Some(mut stdout) = child.stdout.take() {
71 stdout.read_to_string(&mut output).await?;
72 }
73
74 let status = child.status().await?;
75 if !status.success() {
76 return Err(anyhow::anyhow!("Cargo command failed"));
77 }
78
79 let executables = output
80 .lines()
81 .filter(|line| !line.trim().is_empty())
82 .filter_map(|line| serde_json::from_str(line).ok())
83 .filter_map(|json: Value| {
84 json.get("executable")
85 .and_then(Value::as_str)
86 .map(String::from)
87 })
88 .collect::<Vec<_>>();
89 if executables.is_empty() {
90 return Err(anyhow!("Couldn't get executable in cargo locator"));
91 };
92
93 let is_test = build_config.args.first().map_or(false, |arg| arg == "test");
94
95 let mut test_name = None;
96 if is_test {
97 if let Some(package_index) = build_config
98 .args
99 .iter()
100 .position(|arg| arg == "-p" || arg == "--package")
101 {
102 test_name = build_config
103 .args
104 .get(package_index + 2)
105 .filter(|name| !name.starts_with("--"))
106 .cloned();
107 }
108 }
109 let executable = {
110 if let Some(ref name) = test_name {
111 find_best_executable(&executables, &name).await
112 } else {
113 None
114 }
115 };
116
117 let Some(executable) = executable.or_else(|| executables.first().cloned()) else {
118 return Err(anyhow!("Couldn't get executable in cargo locator"));
119 };
120
121 let args = test_name.into_iter().collect();
122
123 Ok(DebugRequest::Launch(task::LaunchRequest {
124 program: executable,
125 cwd: build_config.cwd.clone(),
126 args,
127 env: build_config
128 .env
129 .iter()
130 .map(|(k, v)| (k.clone(), v.clone()))
131 .collect(),
132 }))
133 }
134}