1use std::{path::PathBuf, sync::OnceLock};
2
3use anyhow::{Context as _, Result};
4use async_trait::async_trait;
5use collections::HashMap;
6use dap::adapters::{DebugTaskDefinition, latest_github_release};
7use futures::StreamExt;
8use gpui::AsyncApp;
9use serde_json::Value;
10use task::{DebugRequest, DebugScenario, ZedDebugConfig};
11use util::fs::remove_matching;
12
13use crate::*;
14
15#[derive(Default)]
16pub(crate) struct CodeLldbDebugAdapter {
17 path_to_codelldb: OnceLock<String>,
18}
19
20impl CodeLldbDebugAdapter {
21 const ADAPTER_NAME: &'static str = "CodeLLDB";
22
23 async fn request_args(
24 &self,
25 delegate: &Arc<dyn DapDelegate>,
26 mut configuration: Value,
27 label: &str,
28 ) -> Result<dap::StartDebuggingRequestArguments> {
29 let obj = configuration
30 .as_object_mut()
31 .context("CodeLLDB is not a valid json object")?;
32
33 // CodeLLDB uses `name` for a terminal label.
34 obj.entry("name")
35 .or_insert(Value::String(String::from(label)));
36
37 obj.entry("cwd")
38 .or_insert(delegate.worktree_root_path().to_string_lossy().into());
39
40 let request = self.request_kind(&configuration).await?;
41
42 Ok(dap::StartDebuggingRequestArguments {
43 request,
44 configuration,
45 })
46 }
47
48 async fn fetch_latest_adapter_version(
49 &self,
50 delegate: &Arc<dyn DapDelegate>,
51 ) -> Result<AdapterVersion> {
52 let release =
53 latest_github_release("vadimcn/codelldb", true, false, delegate.http_client()).await?;
54
55 let arch = match std::env::consts::ARCH {
56 "aarch64" => "arm64",
57 "x86_64" => "x64",
58 unsupported => {
59 anyhow::bail!("unsupported architecture {unsupported}");
60 }
61 };
62 let platform = match std::env::consts::OS {
63 "macos" => "darwin",
64 "linux" => "linux",
65 "windows" => "win32",
66 unsupported => {
67 anyhow::bail!("unsupported operating system {unsupported}");
68 }
69 };
70 let asset_name = format!("codelldb-{platform}-{arch}.vsix");
71 let ret = AdapterVersion {
72 tag_name: release.tag_name,
73 url: release
74 .assets
75 .iter()
76 .find(|asset| asset.name == asset_name)
77 .with_context(|| format!("no asset found matching {asset_name:?}"))?
78 .browser_download_url
79 .clone(),
80 };
81
82 Ok(ret)
83 }
84}
85
86#[async_trait(?Send)]
87impl DebugAdapter for CodeLldbDebugAdapter {
88 fn name(&self) -> DebugAdapterName {
89 DebugAdapterName(Self::ADAPTER_NAME.into())
90 }
91
92 async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
93 let mut configuration = json!({
94 "request": match zed_scenario.request {
95 DebugRequest::Launch(_) => "launch",
96 DebugRequest::Attach(_) => "attach",
97 },
98 });
99 let map = configuration.as_object_mut().unwrap();
100 // CodeLLDB uses `name` for a terminal label.
101 map.insert(
102 "name".into(),
103 Value::String(String::from(zed_scenario.label.as_ref())),
104 );
105 match &zed_scenario.request {
106 DebugRequest::Attach(attach) => {
107 map.insert("pid".into(), attach.process_id.into());
108 }
109 DebugRequest::Launch(launch) => {
110 map.insert("program".into(), launch.program.clone().into());
111
112 if !launch.args.is_empty() {
113 map.insert("args".into(), launch.args.clone().into());
114 }
115 if !launch.env.is_empty() {
116 map.insert("env".into(), launch.env_json());
117 }
118 if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
119 map.insert("stopOnEntry".into(), stop_on_entry.into());
120 }
121 if let Some(cwd) = launch.cwd.as_ref() {
122 map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
123 }
124 }
125 }
126
127 Ok(DebugScenario {
128 adapter: zed_scenario.adapter,
129 label: zed_scenario.label,
130 config: configuration,
131 build: None,
132 tcp_connection: None,
133 })
134 }
135
136 fn dap_schema(&self) -> serde_json::Value {
137 json!({
138 "properties": {
139 "request": {
140 "type": "string",
141 "enum": ["attach", "launch"],
142 "description": "Debug adapter request type"
143 },
144 "program": {
145 "type": "string",
146 "description": "Path to the program to debug or attach to"
147 },
148 "args": {
149 "type": ["array", "string"],
150 "description": "Program arguments"
151 },
152 "cwd": {
153 "type": "string",
154 "description": "Program working directory"
155 },
156 "env": {
157 "type": "object",
158 "description": "Additional environment variables",
159 "patternProperties": {
160 ".*": {
161 "type": "string"
162 }
163 }
164 },
165 "envFile": {
166 "type": "string",
167 "description": "File to read the environment variables from"
168 },
169 "stdio": {
170 "type": ["null", "string", "array", "object"],
171 "description": "Destination for stdio streams: null = send to debugger console or a terminal, \"<path>\" = attach to a file/tty/fifo"
172 },
173 "terminal": {
174 "type": "string",
175 "enum": ["integrated", "console"],
176 "description": "Terminal type to use",
177 "default": "integrated"
178 },
179 "console": {
180 "type": "string",
181 "enum": ["integratedTerminal", "internalConsole"],
182 "description": "Terminal type to use (compatibility alias of 'terminal')"
183 },
184 "stopOnEntry": {
185 "type": "boolean",
186 "description": "Automatically stop debuggee after launch",
187 "default": false
188 },
189 "initCommands": {
190 "type": "array",
191 "description": "Initialization commands executed upon debugger startup",
192 "items": {
193 "type": "string"
194 }
195 },
196 "targetCreateCommands": {
197 "type": "array",
198 "description": "Commands that create the debug target",
199 "items": {
200 "type": "string"
201 }
202 },
203 "preRunCommands": {
204 "type": "array",
205 "description": "Commands executed just before the program is launched",
206 "items": {
207 "type": "string"
208 }
209 },
210 "processCreateCommands": {
211 "type": "array",
212 "description": "Commands that create the debuggee process",
213 "items": {
214 "type": "string"
215 }
216 },
217 "postRunCommands": {
218 "type": "array",
219 "description": "Commands executed just after the program has been launched",
220 "items": {
221 "type": "string"
222 }
223 },
224 "preTerminateCommands": {
225 "type": "array",
226 "description": "Commands executed just before the debuggee is terminated or disconnected from",
227 "items": {
228 "type": "string"
229 }
230 },
231 "exitCommands": {
232 "type": "array",
233 "description": "Commands executed at the end of debugging session",
234 "items": {
235 "type": "string"
236 }
237 },
238 "expressions": {
239 "type": "string",
240 "enum": ["simple", "python", "native"],
241 "description": "The default evaluator type used for expressions"
242 },
243 "sourceMap": {
244 "type": "object",
245 "description": "Source path remapping between the build machine and the local machine",
246 "patternProperties": {
247 ".*": {
248 "type": ["string", "null"]
249 }
250 }
251 },
252 "relativePathBase": {
253 "type": "string",
254 "description": "Base directory used for resolution of relative source paths. Defaults to the workspace folder"
255 },
256 "sourceLanguages": {
257 "type": "array",
258 "description": "A list of source languages to enable language-specific features for",
259 "items": {
260 "type": "string"
261 }
262 },
263 "reverseDebugging": {
264 "type": "boolean",
265 "description": "Enable reverse debugging",
266 "default": false
267 },
268 "breakpointMode": {
269 "type": "string",
270 "enum": ["path", "file"],
271 "description": "Specifies how source breakpoints should be set"
272 },
273 "pid": {
274 "type": ["integer", "string"],
275 "description": "Process id to attach to"
276 },
277 "waitFor": {
278 "type": "boolean",
279 "description": "Wait for the process to launch (MacOS only)",
280 "default": false
281 }
282 },
283 "required": ["request"],
284 "allOf": [
285 {
286 "if": {
287 "properties": {
288 "request": {
289 "enum": ["launch"]
290 }
291 }
292 },
293 "then": {
294 "oneOf": [
295 {
296 "required": ["program"]
297 },
298 {
299 "required": ["targetCreateCommands"]
300 }
301 ]
302 }
303 },
304 {
305 "if": {
306 "properties": {
307 "request": {
308 "enum": ["attach"]
309 }
310 }
311 },
312 "then": {
313 "oneOf": [
314 {
315 "required": ["pid"]
316 },
317 {
318 "required": ["program"]
319 }
320 ]
321 }
322 }
323 ]
324 })
325 }
326
327 async fn get_binary(
328 &self,
329 delegate: &Arc<dyn DapDelegate>,
330 config: &DebugTaskDefinition,
331 user_installed_path: Option<PathBuf>,
332 user_args: Option<Vec<String>>,
333 user_env: Option<HashMap<String, String>>,
334 _: &mut AsyncApp,
335 ) -> Result<DebugAdapterBinary> {
336 let mut command = user_installed_path
337 .map(|p| p.to_string_lossy().into_owned())
338 .or(self.path_to_codelldb.get().cloned());
339
340 if command.is_none() {
341 delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
342 let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
343 let version_path = match self.fetch_latest_adapter_version(delegate).await {
344 Ok(version) => {
345 adapters::download_adapter_from_github(
346 self.name(),
347 version.clone(),
348 adapters::DownloadedFileType::Vsix,
349 delegate.as_ref(),
350 )
351 .await?;
352 let version_path =
353 adapter_path.join(format!("{}_{}", Self::ADAPTER_NAME, version.tag_name));
354 remove_matching(&adapter_path, |entry| entry != version_path).await;
355 version_path
356 }
357 Err(e) => {
358 delegate.output_to_console("Unable to fetch latest version".to_string());
359 log::error!("Error fetching latest version of {}: {}", self.name(), e);
360 delegate.output_to_console(format!(
361 "Searching for adapters in: {}",
362 adapter_path.display()
363 ));
364 let mut paths = delegate
365 .fs()
366 .read_dir(&adapter_path)
367 .await
368 .context("No cached adapter directory")?;
369 paths
370 .next()
371 .await
372 .context("No cached adapter found")?
373 .context("No cached adapter found")?
374 }
375 };
376 let adapter_dir = version_path.join("extension").join("adapter");
377 let path = adapter_dir.join("codelldb").to_string_lossy().into_owned();
378 self.path_to_codelldb.set(path.clone()).ok();
379 command = Some(path);
380 };
381 let mut json_config = config.config.clone();
382
383 Ok(DebugAdapterBinary {
384 command: Some(command.unwrap()),
385 cwd: Some(delegate.worktree_root_path().to_path_buf()),
386 arguments: user_args.unwrap_or_else(|| {
387 if let Some(config) = json_config.as_object_mut()
388 && let Some(source_languages) = config.get("sourceLanguages").filter(|value| {
389 value
390 .as_array()
391 .is_some_and(|array| array.iter().all(Value::is_string))
392 })
393 {
394 let ret = vec![
395 "--settings".into(),
396 json!({"sourceLanguages": source_languages}).to_string(),
397 ];
398 config.remove("sourceLanguages");
399 ret
400 } else {
401 vec![]
402 }
403 }),
404 request_args: self
405 .request_args(delegate, json_config, &config.label)
406 .await?,
407 envs: user_env.unwrap_or_default(),
408 connection: None,
409 })
410 }
411}