1use anyhow::{Result, anyhow};
2use async_trait::async_trait;
3use dap::{DapLocator, DebugRequest};
4use gpui::SharedString;
5use serde_json::Value;
6use smol::{
7 io::AsyncReadExt,
8 process::{Command, Stdio},
9};
10use task::{BuildTaskDefinition, DebugScenario, ShellBuilder, SpawnInTerminal, TaskTemplate};
11
12pub(crate) struct CargoLocator;
13
14async fn find_best_executable(executables: &[String], test_name: &str) -> Option<String> {
15 if executables.len() == 1 {
16 return executables.first().cloned();
17 }
18 for executable in executables {
19 let Some(mut child) = Command::new(&executable)
20 .arg("--list")
21 .stdout(Stdio::piped())
22 .spawn()
23 .ok()
24 else {
25 continue;
26 };
27 let mut test_lines = String::default();
28 if let Some(mut stdout) = child.stdout.take() {
29 stdout.read_to_string(&mut test_lines).await.ok();
30 for line in test_lines.lines() {
31 if line.contains(&test_name) {
32 return Some(executable.clone());
33 }
34 }
35 }
36 }
37 None
38}
39#[async_trait]
40impl DapLocator for CargoLocator {
41 fn name(&self) -> SharedString {
42 SharedString::new_static("rust-cargo-locator")
43 }
44 fn create_scenario(&self, build_config: &TaskTemplate, adapter: &str) -> Option<DebugScenario> {
45 if build_config.command != "cargo" {
46 return None;
47 }
48 let mut task_template = build_config.clone();
49 let cargo_action = task_template.args.first_mut()?;
50 if cargo_action == "check" {
51 return None;
52 }
53
54 match cargo_action.as_ref() {
55 "run" => {
56 *cargo_action = "build".to_owned();
57 }
58 "test" | "bench" => {
59 let delimiter = task_template
60 .args
61 .iter()
62 .position(|arg| arg == "--")
63 .unwrap_or(task_template.args.len());
64 if !task_template.args[..delimiter]
65 .iter()
66 .any(|arg| arg == "--no-run")
67 {
68 task_template.args.insert(delimiter, "--no-run".to_owned());
69 }
70 }
71 _ => {}
72 }
73 let label = format!("Debug `{}`", build_config.label);
74 Some(DebugScenario {
75 adapter: adapter.to_owned().into(),
76 label: SharedString::from(label),
77 build: Some(BuildTaskDefinition::Template {
78 task_template,
79 locator_name: Some(self.name()),
80 }),
81 request: None,
82 initialize_args: None,
83 tcp_connection: None,
84 stop_on_entry: None,
85 })
86 }
87
88 async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest> {
89 let Some(cwd) = build_config.cwd.clone() else {
90 return Err(anyhow!(
91 "Couldn't get cwd from debug config which is needed for locators"
92 ));
93 };
94 let builder = ShellBuilder::new(true, &build_config.shell).non_interactive();
95 let (program, args) = builder.build(
96 "cargo".into(),
97 &build_config
98 .args
99 .iter()
100 .cloned()
101 .take_while(|arg| arg != "--")
102 .chain(Some("--message-format=json".to_owned()))
103 .collect(),
104 );
105 let mut child = Command::new(program)
106 .args(args)
107 .envs(build_config.env.iter().map(|(k, v)| (k.clone(), v.clone())))
108 .current_dir(cwd)
109 .stdout(Stdio::piped())
110 .spawn()?;
111
112 let mut output = String::new();
113 if let Some(mut stdout) = child.stdout.take() {
114 stdout.read_to_string(&mut output).await?;
115 }
116
117 let status = child.status().await?;
118 if !status.success() {
119 return Err(anyhow::anyhow!("Cargo command failed"));
120 }
121
122 let executables = output
123 .lines()
124 .filter(|line| !line.trim().is_empty())
125 .filter_map(|line| serde_json::from_str(line).ok())
126 .filter_map(|json: Value| {
127 json.get("executable")
128 .and_then(Value::as_str)
129 .map(String::from)
130 })
131 .collect::<Vec<_>>();
132 if executables.is_empty() {
133 return Err(anyhow!("Couldn't get executable in cargo locator"));
134 };
135 let is_test = build_config.args.first().map_or(false, |arg| arg == "test");
136
137 let mut test_name = None;
138 if is_test {
139 if let Some(package_index) = build_config
140 .args
141 .iter()
142 .position(|arg| arg == "-p" || arg == "--package")
143 {
144 test_name = build_config
145 .args
146 .get(package_index + 2)
147 .filter(|name| !name.starts_with("--"))
148 .cloned();
149 }
150 }
151 let executable = {
152 if let Some(ref name) = test_name {
153 find_best_executable(&executables, &name).await
154 } else {
155 None
156 }
157 };
158
159 let Some(executable) = executable.or_else(|| executables.first().cloned()) else {
160 return Err(anyhow!("Couldn't get executable in cargo locator"));
161 };
162
163 let args = test_name.into_iter().collect();
164
165 Ok(DebugRequest::Launch(task::LaunchRequest {
166 program: executable,
167 cwd: build_config.cwd.clone(),
168 args,
169 env: build_config
170 .env
171 .iter()
172 .map(|(k, v)| (k.clone(), v.clone()))
173 .collect(),
174 }))
175 }
176}