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