codelldb.rs

  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}