codelldb.rs

  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}