1use anyhow::{Context as _, Result, anyhow};
2use async_compression::futures::bufread::GzipDecoder;
3use async_tar::Archive;
4use async_trait::async_trait;
5use collections::HashMap;
6pub use dap_types::{StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest};
7use fs::Fs;
8use futures::io::BufReader;
9use gpui::{AsyncApp, SharedString};
10pub use http_client::{HttpClient, github::latest_github_release};
11use language::{LanguageName, LanguageToolchainStore};
12use node_runtime::NodeRuntime;
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15use settings::WorktreeId;
16use smol::fs::File;
17use std::{
18 borrow::{Borrow, Cow},
19 ffi::OsStr,
20 fmt::Debug,
21 net::Ipv4Addr,
22 ops::Deref,
23 path::{Path, PathBuf},
24 sync::Arc,
25};
26use task::{DebugScenario, TcpArgumentsTemplate, ZedDebugConfig};
27use util::archive::extract_zip;
28
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum DapStatus {
31 None,
32 CheckingForUpdate,
33 Downloading,
34 Failed { error: String },
35}
36
37#[async_trait]
38pub trait DapDelegate: Send + Sync + 'static {
39 fn worktree_id(&self) -> WorktreeId;
40 fn worktree_root_path(&self) -> &Path;
41 fn http_client(&self) -> Arc<dyn HttpClient>;
42 fn node_runtime(&self) -> NodeRuntime;
43 fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore>;
44 fn fs(&self) -> Arc<dyn Fs>;
45 fn output_to_console(&self, msg: String);
46 async fn which(&self, command: &OsStr) -> Option<PathBuf>;
47 async fn read_text_file(&self, path: PathBuf) -> Result<String>;
48 async fn shell_env(&self) -> collections::HashMap<String, String>;
49}
50
51#[derive(
52 Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, JsonSchema,
53)]
54#[serde(transparent)]
55pub struct DebugAdapterName(pub SharedString);
56
57impl Deref for DebugAdapterName {
58 type Target = str;
59
60 fn deref(&self) -> &Self::Target {
61 &self.0
62 }
63}
64
65impl AsRef<str> for DebugAdapterName {
66 fn as_ref(&self) -> &str {
67 &self.0
68 }
69}
70
71impl Borrow<str> for DebugAdapterName {
72 fn borrow(&self) -> &str {
73 &self.0
74 }
75}
76
77impl std::fmt::Display for DebugAdapterName {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 std::fmt::Display::fmt(&self.0, f)
80 }
81}
82
83impl From<DebugAdapterName> for SharedString {
84 fn from(name: DebugAdapterName) -> Self {
85 name.0
86 }
87}
88impl From<SharedString> for DebugAdapterName {
89 fn from(name: SharedString) -> Self {
90 DebugAdapterName(name)
91 }
92}
93
94impl<'a> From<&'a str> for DebugAdapterName {
95 fn from(str: &'a str) -> DebugAdapterName {
96 DebugAdapterName(str.to_string().into())
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Serialize)]
101pub struct TcpArguments {
102 pub host: Ipv4Addr,
103 pub port: u16,
104 pub timeout: Option<u64>,
105}
106
107impl TcpArguments {
108 pub fn from_proto(proto: proto::TcpHost) -> anyhow::Result<Self> {
109 let host = TcpArgumentsTemplate::from_proto(proto)?;
110 Ok(TcpArguments {
111 host: host.host.context("missing host")?,
112 port: host.port.context("missing port")?,
113 timeout: host.timeout,
114 })
115 }
116
117 pub fn to_proto(&self) -> proto::TcpHost {
118 TcpArgumentsTemplate {
119 host: Some(self.host),
120 port: Some(self.port),
121 timeout: self.timeout,
122 }
123 .to_proto()
124 }
125}
126
127/// Represents a debuggable binary/process (what process is going to be debugged and with what arguments).
128///
129/// We start off with a [DebugScenario], a user-facing type that additionally defines how a debug target is built; once
130/// an optional build step is completed, we turn it's result into a DebugTaskDefinition by running a locator (or using a user-provided task) and resolving task variables.
131/// Finally, a [DebugTaskDefinition] has to be turned into a concrete debugger invocation ([DebugAdapterBinary]).
132#[derive(Clone, Debug, PartialEq)]
133#[cfg_attr(
134 any(feature = "test-support", test),
135 derive(serde::Deserialize, serde::Serialize)
136)]
137pub struct DebugTaskDefinition {
138 /// The name of this debug task
139 pub label: SharedString,
140 /// The debug adapter to use
141 pub adapter: DebugAdapterName,
142 /// The configuration to send to the debug adapter
143 pub config: serde_json::Value,
144 /// Optional TCP connection information
145 ///
146 /// If provided, this will be used to connect to the debug adapter instead of
147 /// spawning a new debug adapter process. This is useful for connecting to a debug adapter
148 /// that is already running or is started by another process.
149 pub tcp_connection: Option<TcpArgumentsTemplate>,
150}
151
152impl DebugTaskDefinition {
153 pub fn to_scenario(&self) -> DebugScenario {
154 DebugScenario {
155 label: self.label.clone(),
156 adapter: self.adapter.clone().into(),
157 build: None,
158 tcp_connection: self.tcp_connection.clone(),
159 config: self.config.clone(),
160 }
161 }
162
163 pub fn to_proto(&self) -> proto::DebugTaskDefinition {
164 proto::DebugTaskDefinition {
165 label: self.label.clone().into(),
166 config: self.config.to_string(),
167 tcp_connection: self.tcp_connection.clone().map(|v| v.to_proto()),
168 adapter: self.adapter.clone().0.into(),
169 }
170 }
171
172 pub fn from_proto(proto: proto::DebugTaskDefinition) -> Result<Self> {
173 Ok(Self {
174 label: proto.label.into(),
175 config: serde_json::from_str(&proto.config)?,
176 tcp_connection: proto
177 .tcp_connection
178 .map(TcpArgumentsTemplate::from_proto)
179 .transpose()?,
180 adapter: DebugAdapterName(proto.adapter.into()),
181 })
182 }
183}
184
185/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
186#[derive(Debug, Clone, PartialEq, Serialize)]
187pub struct DebugAdapterBinary {
188 pub command: Option<String>,
189 pub arguments: Vec<String>,
190 pub envs: HashMap<String, String>,
191 pub cwd: Option<PathBuf>,
192 pub connection: Option<TcpArguments>,
193 pub request_args: StartDebuggingRequestArguments,
194}
195
196impl DebugAdapterBinary {
197 pub fn from_proto(binary: proto::DebugAdapterBinary) -> anyhow::Result<Self> {
198 let request = match binary.launch_type() {
199 proto::debug_adapter_binary::LaunchType::Launch => {
200 StartDebuggingRequestArgumentsRequest::Launch
201 }
202 proto::debug_adapter_binary::LaunchType::Attach => {
203 StartDebuggingRequestArgumentsRequest::Attach
204 }
205 };
206
207 Ok(DebugAdapterBinary {
208 command: binary.command,
209 arguments: binary.arguments,
210 envs: binary.envs.into_iter().collect(),
211 connection: binary
212 .connection
213 .map(TcpArguments::from_proto)
214 .transpose()?,
215 request_args: StartDebuggingRequestArguments {
216 configuration: serde_json::from_str(&binary.configuration)?,
217 request,
218 },
219 cwd: binary.cwd.map(|cwd| cwd.into()),
220 })
221 }
222
223 pub fn to_proto(&self) -> proto::DebugAdapterBinary {
224 proto::DebugAdapterBinary {
225 command: self.command.clone(),
226 arguments: self.arguments.clone(),
227 envs: self
228 .envs
229 .iter()
230 .map(|(k, v)| (k.clone(), v.clone()))
231 .collect(),
232 cwd: self
233 .cwd
234 .as_ref()
235 .map(|cwd| cwd.to_string_lossy().to_string()),
236 connection: self.connection.as_ref().map(|c| c.to_proto()),
237 launch_type: match self.request_args.request {
238 StartDebuggingRequestArgumentsRequest::Launch => {
239 proto::debug_adapter_binary::LaunchType::Launch.into()
240 }
241 StartDebuggingRequestArgumentsRequest::Attach => {
242 proto::debug_adapter_binary::LaunchType::Attach.into()
243 }
244 },
245 configuration: self.request_args.configuration.to_string(),
246 }
247 }
248}
249
250#[derive(Debug, Clone)]
251pub struct AdapterVersion {
252 pub tag_name: String,
253 pub url: String,
254}
255
256pub enum DownloadedFileType {
257 Vsix,
258 GzipTar,
259 Zip,
260}
261
262pub struct GithubRepo {
263 pub repo_name: String,
264 pub repo_owner: String,
265}
266
267pub async fn download_adapter_from_github(
268 adapter_name: &str,
269 github_version: AdapterVersion,
270 file_type: DownloadedFileType,
271 base_path: &Path,
272 delegate: &dyn DapDelegate,
273) -> Result<PathBuf> {
274 let adapter_path = base_path.join(adapter_name);
275 let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name));
276 let fs = delegate.fs();
277
278 if version_path.exists() {
279 return Ok(version_path);
280 }
281
282 if !adapter_path.exists() {
283 fs.create_dir(&adapter_path.as_path())
284 .await
285 .context("Failed creating adapter path")?;
286 }
287
288 log::debug!("Downloading {} from {}", adapter_name, &github_version.url);
289 delegate.output_to_console(format!("Downloading from {}...", github_version.url));
290
291 let mut response = delegate
292 .http_client()
293 .get(&github_version.url, Default::default(), true)
294 .await
295 .context("Error downloading release")?;
296 anyhow::ensure!(
297 response.status().is_success(),
298 "download failed with status {}",
299 response.status().to_string()
300 );
301
302 delegate.output_to_console("Download complete".to_owned());
303 match file_type {
304 DownloadedFileType::GzipTar => {
305 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
306 let archive = Archive::new(decompressed_bytes);
307 archive.unpack(&version_path).await?;
308 }
309 DownloadedFileType::Zip | DownloadedFileType::Vsix => {
310 let zip_path = version_path.with_extension("zip");
311 let mut file = File::create(&zip_path).await?;
312 futures::io::copy(response.body_mut(), &mut file).await?;
313 let file = File::open(&zip_path).await?;
314 extract_zip(&version_path, file)
315 .await
316 // we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
317 .ok();
318
319 util::fs::remove_matching(&adapter_path, |entry| {
320 entry
321 .file_name()
322 .is_some_and(|file| file.to_string_lossy().ends_with(".zip"))
323 })
324 .await;
325 }
326 }
327
328 // remove older versions
329 util::fs::remove_matching(&adapter_path, |entry| {
330 entry.to_string_lossy() != version_path.to_string_lossy()
331 })
332 .await;
333
334 Ok(version_path)
335}
336
337#[async_trait(?Send)]
338pub trait DebugAdapter: 'static + Send + Sync {
339 fn name(&self) -> DebugAdapterName;
340
341 async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario>;
342
343 async fn get_binary(
344 &self,
345 delegate: &Arc<dyn DapDelegate>,
346 config: &DebugTaskDefinition,
347 user_installed_path: Option<PathBuf>,
348 user_args: Option<Vec<String>>,
349 cx: &mut AsyncApp,
350 ) -> Result<DebugAdapterBinary>;
351
352 /// Returns the language name of an adapter if it only supports one language
353 fn adapter_language_name(&self) -> Option<LanguageName> {
354 None
355 }
356
357 /// Extracts the kind (attach/launch) of debug configuration from the given JSON config.
358 /// This method should only return error when the kind cannot be determined for a given configuration;
359 /// in particular, it *should not* validate whether the request as a whole is valid, because that's best left to the debug adapter itself to decide.
360 async fn request_kind(
361 &self,
362 config: &serde_json::Value,
363 ) -> Result<StartDebuggingRequestArgumentsRequest> {
364 match config.get("request") {
365 Some(val) if val == "launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
366 Some(val) if val == "attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
367 _ => Err(anyhow!(
368 "missing or invalid `request` field in config. Expected 'launch' or 'attach'"
369 )),
370 }
371 }
372
373 fn dap_schema(&self) -> Cow<'static, serde_json::Value>;
374
375 fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> {
376 None
377 }
378}
379
380#[cfg(any(test, feature = "test-support"))]
381pub struct FakeAdapter {}
382
383#[cfg(any(test, feature = "test-support"))]
384impl FakeAdapter {
385 pub const ADAPTER_NAME: &'static str = "fake-adapter";
386
387 pub fn new() -> Self {
388 Self {}
389 }
390}
391
392#[cfg(any(test, feature = "test-support"))]
393#[async_trait(?Send)]
394impl DebugAdapter for FakeAdapter {
395 fn name(&self) -> DebugAdapterName {
396 DebugAdapterName(Self::ADAPTER_NAME.into())
397 }
398
399 fn dap_schema(&self) -> Cow<'static, serde_json::Value> {
400 Cow::Owned(serde_json::Value::Null)
401 }
402
403 async fn request_kind(
404 &self,
405 config: &serde_json::Value,
406 ) -> Result<StartDebuggingRequestArgumentsRequest> {
407 let request = config.as_object().unwrap()["request"].as_str().unwrap();
408
409 let request = match request {
410 "launch" => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
411 "attach" => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
412 _ => unreachable!("Wrong fake adapter input for request field"),
413 };
414
415 Ok(request)
416 }
417
418 fn adapter_language_name(&self) -> Option<LanguageName> {
419 None
420 }
421
422 async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
423 let config = serde_json::to_value(zed_scenario.request).unwrap();
424
425 Ok(DebugScenario {
426 adapter: zed_scenario.adapter,
427 label: zed_scenario.label,
428 build: None,
429 config,
430 tcp_connection: None,
431 })
432 }
433
434 async fn get_binary(
435 &self,
436 _: &Arc<dyn DapDelegate>,
437 task_definition: &DebugTaskDefinition,
438 _: Option<PathBuf>,
439 _: Option<Vec<String>>,
440 _: &mut AsyncApp,
441 ) -> Result<DebugAdapterBinary> {
442 Ok(DebugAdapterBinary {
443 command: Some("command".into()),
444 arguments: vec![],
445 connection: None,
446 envs: HashMap::default(),
447 cwd: None,
448 request_args: StartDebuggingRequestArguments {
449 request: self.request_kind(&task_definition.config).await?,
450 configuration: task_definition.config.clone(),
451 },
452 })
453 }
454}