codelldb.rs

  1use std::{
  2    borrow::Cow,
  3    collections::HashMap,
  4    path::PathBuf,
  5    sync::{LazyLock, OnceLock},
  6};
  7
  8use anyhow::{Context as _, Result};
  9use async_trait::async_trait;
 10use dap::adapters::{DapDelegate, DebugTaskDefinition, latest_github_release};
 11use futures::StreamExt;
 12use gpui::AsyncApp;
 13use serde_json::Value;
 14use task::{DebugRequest, DebugScenario, ZedDebugConfig};
 15use util::fs::remove_matching;
 16
 17use crate::*;
 18
 19#[derive(Default)]
 20pub struct CodeLldbDebugAdapter {
 21    path_to_codelldb: OnceLock<String>,
 22}
 23
 24impl CodeLldbDebugAdapter {
 25    pub const ADAPTER_NAME: &'static str = "CodeLLDB";
 26
 27    async fn request_args(
 28        &self,
 29        delegate: &Arc<dyn DapDelegate>,
 30        mut configuration: Value,
 31        label: &str,
 32    ) -> Result<dap::StartDebuggingRequestArguments> {
 33        let obj = configuration
 34            .as_object_mut()
 35            .context("CodeLLDB is not a valid json object")?;
 36
 37        // CodeLLDB uses `name` for a terminal label.
 38        obj.entry("name")
 39            .or_insert(Value::String(String::from(label)));
 40
 41        obj.entry("cwd")
 42            .or_insert(delegate.worktree_root_path().to_string_lossy().into());
 43
 44        let request = self.request_kind(&configuration).await?;
 45
 46        Ok(dap::StartDebuggingRequestArguments {
 47            request,
 48            configuration,
 49        })
 50    }
 51
 52    async fn fetch_latest_adapter_version(
 53        &self,
 54        delegate: &Arc<dyn DapDelegate>,
 55    ) -> Result<AdapterVersion> {
 56        let release =
 57            latest_github_release("vadimcn/codelldb", true, false, delegate.http_client()).await?;
 58
 59        let arch = match std::env::consts::ARCH {
 60            "aarch64" => "arm64",
 61            "x86_64" => "x64",
 62            unsupported => {
 63                anyhow::bail!("unsupported architecture {unsupported}");
 64            }
 65        };
 66        let platform = match std::env::consts::OS {
 67            "macos" => "darwin",
 68            "linux" => "linux",
 69            "windows" => "win32",
 70            unsupported => {
 71                anyhow::bail!("unsupported operating system {unsupported}");
 72            }
 73        };
 74        let asset_name = format!("codelldb-{platform}-{arch}.vsix");
 75        let ret = AdapterVersion {
 76            tag_name: release.tag_name,
 77            url: release
 78                .assets
 79                .iter()
 80                .find(|asset| asset.name == asset_name)
 81                .with_context(|| format!("no asset found matching {asset_name:?}"))?
 82                .browser_download_url
 83                .clone(),
 84        };
 85
 86        Ok(ret)
 87    }
 88}
 89
 90#[cfg(feature = "update-schemas")]
 91impl CodeLldbDebugAdapter {
 92    pub fn get_schema(
 93        temp_dir: &tempfile::TempDir,
 94        delegate: UpdateSchemasDapDelegate,
 95    ) -> anyhow::Result<serde_json::Value> {
 96        let (package_json, package_nls_json) = get_vsix_package_json(
 97            temp_dir,
 98            "vadimcn/codelldb",
 99            |_| Ok(format!("codelldb-bootstrap.vsix")),
100            delegate,
101        )?;
102        let package_json = parse_package_json(package_json, package_nls_json)?;
103
104        let [debugger] =
105            <[_; 1]>::try_from(package_json.contributes.debuggers).map_err(|debuggers| {
106                anyhow::anyhow!(
107                    "unexpected number of codelldb debuggers: {}",
108                    debuggers.len()
109                )
110            })?;
111
112        Ok(schema_for_configuration_attributes(
113            debugger.configuration_attributes,
114        ))
115    }
116}
117
118#[async_trait(?Send)]
119impl DebugAdapter for CodeLldbDebugAdapter {
120    fn name(&self) -> DebugAdapterName {
121        DebugAdapterName(Self::ADAPTER_NAME.into())
122    }
123
124    async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
125        let mut configuration = json!({
126            "request": match zed_scenario.request {
127                DebugRequest::Launch(_) => "launch",
128                DebugRequest::Attach(_) => "attach",
129            },
130        });
131        let map = configuration.as_object_mut().unwrap();
132        // CodeLLDB uses `name` for a terminal label.
133        map.insert(
134            "name".into(),
135            Value::String(String::from(zed_scenario.label.as_ref())),
136        );
137        match &zed_scenario.request {
138            DebugRequest::Attach(attach) => {
139                map.insert("pid".into(), attach.process_id.into());
140            }
141            DebugRequest::Launch(launch) => {
142                map.insert("program".into(), launch.program.clone().into());
143
144                if !launch.args.is_empty() {
145                    map.insert("args".into(), launch.args.clone().into());
146                }
147                if !launch.env.is_empty() {
148                    map.insert("env".into(), launch.env_json());
149                }
150                if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
151                    map.insert("stopOnEntry".into(), stop_on_entry.into());
152                }
153                if let Some(cwd) = launch.cwd.as_ref() {
154                    map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
155                }
156            }
157        }
158
159        Ok(DebugScenario {
160            adapter: zed_scenario.adapter,
161            label: zed_scenario.label,
162            config: configuration,
163            build: None,
164            tcp_connection: None,
165        })
166    }
167
168    fn dap_schema(&self) -> Cow<'static, serde_json::Value> {
169        static SCHEMA: LazyLock<serde_json::Value> = LazyLock::new(|| {
170            const RAW_SCHEMA: &str = include_str!("../schemas/CodeLLDB.json");
171            serde_json::from_str(RAW_SCHEMA).unwrap()
172        });
173        Cow::Borrowed(&*SCHEMA)
174    }
175
176    async fn get_binary(
177        &self,
178        delegate: &Arc<dyn DapDelegate>,
179        config: &DebugTaskDefinition,
180        user_installed_path: Option<PathBuf>,
181        user_args: Option<Vec<String>>,
182        _: &mut AsyncApp,
183    ) -> Result<DebugAdapterBinary> {
184        let mut command = user_installed_path
185            .map(|p| p.to_string_lossy().to_string())
186            .or(self.path_to_codelldb.get().cloned());
187
188        if command.is_none() {
189            delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
190            let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
191            let version_path =
192                if let Ok(version) = self.fetch_latest_adapter_version(delegate).await {
193                    adapters::download_adapter_from_github(
194                        Self::ADAPTER_NAME,
195                        version.clone(),
196                        adapters::DownloadedFileType::Vsix,
197                        paths::debug_adapters_dir(),
198                        delegate.as_ref(),
199                    )
200                    .await?;
201                    let version_path =
202                        adapter_path.join(format!("{}_{}", Self::ADAPTER_NAME, version.tag_name));
203                    remove_matching(&adapter_path, |entry| entry != version_path).await;
204                    version_path
205                } else {
206                    let mut paths = delegate.fs().read_dir(&adapter_path).await?;
207                    paths.next().await.context("No adapter found")??
208                };
209            let adapter_dir = version_path.join("extension").join("adapter");
210            let path = adapter_dir.join("codelldb").to_string_lossy().to_string();
211            self.path_to_codelldb.set(path.clone()).ok();
212            command = Some(path);
213        };
214        let mut json_config = config.config.clone();
215        Ok(DebugAdapterBinary {
216            command: Some(command.unwrap()),
217            cwd: Some(delegate.worktree_root_path().to_path_buf()),
218            arguments: user_args.unwrap_or_else(|| {
219                if let Some(config) = json_config.as_object_mut()
220                    && let Some(source_languages) = config.get("sourceLanguages").filter(|value| {
221                        value
222                            .as_array()
223                            .map_or(false, |array| array.iter().all(Value::is_string))
224                    })
225                {
226                    let ret = vec![
227                        "--settings".into(),
228                        json!({"sourceLanguages": source_languages}).to_string(),
229                    ];
230                    config.remove("sourceLanguages");
231                    ret
232                } else {
233                    vec![]
234                }
235            }),
236            request_args: self
237                .request_args(delegate, json_config, &config.label)
238                .await?,
239            envs: HashMap::default(),
240            connection: None,
241        })
242    }
243}