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}