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 task::DebugRequest;
9use util::fs::remove_matching;
10
11use crate::*;
12
13#[derive(Default)]
14pub(crate) struct CodeLldbDebugAdapter {
15 path_to_codelldb: OnceLock<String>,
16}
17
18impl CodeLldbDebugAdapter {
19 const ADAPTER_NAME: &'static str = "CodeLLDB";
20
21 fn request_args(&self, config: &DebugTaskDefinition) -> dap::StartDebuggingRequestArguments {
22 let mut configuration = json!({
23 "request": match config.request {
24 DebugRequest::Launch(_) => "launch",
25 DebugRequest::Attach(_) => "attach",
26 },
27 });
28 let map = configuration.as_object_mut().unwrap();
29 // CodeLLDB uses `name` for a terminal label.
30 map.insert(
31 "name".into(),
32 Value::String(String::from(config.label.as_ref())),
33 );
34 let request = config.request.to_dap();
35 match &config.request {
36 DebugRequest::Attach(attach) => {
37 map.insert("pid".into(), attach.process_id.into());
38 }
39 DebugRequest::Launch(launch) => {
40 map.insert("program".into(), launch.program.clone().into());
41
42 if !launch.args.is_empty() {
43 map.insert("args".into(), launch.args.clone().into());
44 }
45 if !launch.env.is_empty() {
46 map.insert("env".into(), launch.env_json());
47 }
48 if let Some(stop_on_entry) = config.stop_on_entry {
49 map.insert("stopOnEntry".into(), stop_on_entry.into());
50 }
51 if let Some(cwd) = launch.cwd.as_ref() {
52 map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
53 }
54 }
55 }
56 dap::StartDebuggingRequestArguments {
57 request,
58 configuration,
59 }
60 }
61
62 async fn fetch_latest_adapter_version(
63 &self,
64 delegate: &Arc<dyn DapDelegate>,
65 ) -> Result<AdapterVersion> {
66 let release =
67 latest_github_release("vadimcn/codelldb", true, false, delegate.http_client()).await?;
68
69 let arch = match std::env::consts::ARCH {
70 "aarch64" => "arm64",
71 "x86_64" => "x64",
72 unsupported => {
73 anyhow::bail!("unsupported architecture {unsupported}");
74 }
75 };
76 let platform = match std::env::consts::OS {
77 "macos" => "darwin",
78 "linux" => "linux",
79 "windows" => "win32",
80 unsupported => {
81 anyhow::bail!("unsupported operating system {unsupported}");
82 }
83 };
84 let asset_name = format!("codelldb-{platform}-{arch}.vsix");
85 let ret = AdapterVersion {
86 tag_name: release.tag_name,
87 url: release
88 .assets
89 .iter()
90 .find(|asset| asset.name == asset_name)
91 .with_context(|| format!("no asset found matching {asset_name:?}"))?
92 .browser_download_url
93 .clone(),
94 };
95
96 Ok(ret)
97 }
98}
99
100#[async_trait(?Send)]
101impl DebugAdapter for CodeLldbDebugAdapter {
102 fn name(&self) -> DebugAdapterName {
103 DebugAdapterName(Self::ADAPTER_NAME.into())
104 }
105
106 async fn get_binary(
107 &self,
108 delegate: &Arc<dyn DapDelegate>,
109 config: &DebugTaskDefinition,
110 user_installed_path: Option<PathBuf>,
111 _: &mut AsyncApp,
112 ) -> Result<DebugAdapterBinary> {
113 let mut command = user_installed_path
114 .map(|p| p.to_string_lossy().to_string())
115 .or(self.path_to_codelldb.get().cloned());
116
117 if command.is_none() {
118 delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
119 let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
120 let version_path =
121 if let Ok(version) = self.fetch_latest_adapter_version(delegate).await {
122 adapters::download_adapter_from_github(
123 self.name(),
124 version.clone(),
125 adapters::DownloadedFileType::Vsix,
126 delegate.as_ref(),
127 )
128 .await?;
129 let version_path =
130 adapter_path.join(format!("{}_{}", Self::ADAPTER_NAME, version.tag_name));
131 remove_matching(&adapter_path, |entry| entry != version_path).await;
132 version_path
133 } else {
134 let mut paths = delegate.fs().read_dir(&adapter_path).await?;
135 paths.next().await.context("No adapter found")??
136 };
137 let adapter_dir = version_path.join("extension").join("adapter");
138 let path = adapter_dir.join("codelldb").to_string_lossy().to_string();
139 // todo("windows")
140 #[cfg(not(windows))]
141 {
142 use smol::fs;
143
144 fs::set_permissions(
145 &path,
146 <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
147 )
148 .await
149 .with_context(|| format!("Settings executable permissions to {path:?}"))?;
150
151 let lldb_binaries_dir = version_path.join("extension").join("lldb").join("bin");
152 let mut lldb_binaries =
153 fs::read_dir(&lldb_binaries_dir).await.with_context(|| {
154 format!("reading lldb binaries dir contents {lldb_binaries_dir:?}")
155 })?;
156 while let Some(binary) = lldb_binaries.next().await {
157 let binary_entry = binary?;
158 let path = binary_entry.path();
159 fs::set_permissions(
160 &path,
161 <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
162 )
163 .await
164 .with_context(|| format!("Settings executable permissions to {path:?}"))?;
165 }
166 }
167 self.path_to_codelldb.set(path.clone()).ok();
168 command = Some(path);
169 };
170
171 Ok(DebugAdapterBinary {
172 command: command.unwrap(),
173 cwd: None,
174 arguments: vec![
175 "--settings".into(),
176 json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
177 ],
178 request_args: self.request_args(config),
179 envs: HashMap::default(),
180 connection: None,
181 })
182 }
183}