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,
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: DebugAdapterName,
269 github_version: AdapterVersion,
270 file_type: DownloadedFileType,
271 delegate: &dyn DapDelegate,
272) -> Result<PathBuf> {
273 let adapter_path = paths::debug_adapters_dir().join(&adapter_name.as_ref());
274 let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name));
275 let fs = delegate.fs();
276
277 if version_path.exists() {
278 return Ok(version_path);
279 }
280
281 if !adapter_path.exists() {
282 fs.create_dir(&adapter_path.as_path())
283 .await
284 .context("Failed creating adapter path")?;
285 }
286
287 log::debug!(
288 "Downloading adapter {} from {}",
289 adapter_name,
290 &github_version.url,
291 );
292 delegate.output_to_console(format!("Downloading from {}...", github_version.url));
293
294 let mut response = delegate
295 .http_client()
296 .get(&github_version.url, Default::default(), true)
297 .await
298 .context("Error downloading release")?;
299 anyhow::ensure!(
300 response.status().is_success(),
301 "download failed with status {}",
302 response.status().to_string()
303 );
304
305 delegate.output_to_console("Download complete".to_owned());
306 match file_type {
307 DownloadedFileType::GzipTar => {
308 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
309 let archive = Archive::new(decompressed_bytes);
310 archive.unpack(&version_path).await?;
311 }
312 DownloadedFileType::Zip | DownloadedFileType::Vsix => {
313 let zip_path = version_path.with_extension("zip");
314 let mut file = File::create(&zip_path).await?;
315 futures::io::copy(response.body_mut(), &mut file).await?;
316 let file = File::open(&zip_path).await?;
317 extract_zip(&version_path, file)
318 .await
319 // we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
320 .ok();
321
322 util::fs::remove_matching(&adapter_path, |entry| {
323 entry
324 .file_name()
325 .is_some_and(|file| file.to_string_lossy().ends_with(".zip"))
326 })
327 .await;
328 }
329 }
330
331 // remove older versions
332 util::fs::remove_matching(&adapter_path, |entry| {
333 entry.to_string_lossy() != version_path.to_string_lossy()
334 })
335 .await;
336
337 Ok(version_path)
338}
339
340#[async_trait(?Send)]
341pub trait DebugAdapter: 'static + Send + Sync {
342 fn name(&self) -> DebugAdapterName;
343
344 async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario>;
345
346 async fn get_binary(
347 &self,
348 delegate: &Arc<dyn DapDelegate>,
349 config: &DebugTaskDefinition,
350 user_installed_path: Option<PathBuf>,
351 user_args: Option<Vec<String>>,
352 cx: &mut AsyncApp,
353 ) -> Result<DebugAdapterBinary>;
354
355 /// Returns the language name of an adapter if it only supports one language
356 fn adapter_language_name(&self) -> Option<LanguageName> {
357 None
358 }
359
360 /// Extracts the kind (attach/launch) of debug configuration from the given JSON config.
361 /// This method should only return error when the kind cannot be determined for a given configuration;
362 /// 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.
363 async fn request_kind(
364 &self,
365 config: &serde_json::Value,
366 ) -> Result<StartDebuggingRequestArgumentsRequest> {
367 match config.get("request") {
368 Some(val) if val == "launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
369 Some(val) if val == "attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
370 _ => Err(anyhow!(
371 "missing or invalid `request` field in config. Expected 'launch' or 'attach'"
372 )),
373 }
374 }
375
376 fn dap_schema(&self) -> serde_json::Value;
377
378 fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> {
379 None
380 }
381
382 fn compact_child_session(&self) -> bool {
383 false
384 }
385
386 fn prefer_thread_name(&self) -> bool {
387 false
388 }
389}
390
391#[cfg(any(test, feature = "test-support"))]
392pub struct FakeAdapter {}
393
394#[cfg(any(test, feature = "test-support"))]
395impl FakeAdapter {
396 pub const ADAPTER_NAME: &'static str = "fake-adapter";
397
398 pub fn new() -> Self {
399 Self {}
400 }
401}
402
403#[cfg(any(test, feature = "test-support"))]
404#[async_trait(?Send)]
405impl DebugAdapter for FakeAdapter {
406 fn name(&self) -> DebugAdapterName {
407 DebugAdapterName(Self::ADAPTER_NAME.into())
408 }
409
410 fn dap_schema(&self) -> serde_json::Value {
411 serde_json::Value::Null
412 }
413
414 async fn request_kind(
415 &self,
416 config: &serde_json::Value,
417 ) -> Result<StartDebuggingRequestArgumentsRequest> {
418 let request = config.as_object().unwrap()["request"].as_str().unwrap();
419
420 let request = match request {
421 "launch" => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
422 "attach" => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
423 _ => unreachable!("Wrong fake adapter input for request field"),
424 };
425
426 Ok(request)
427 }
428
429 fn adapter_language_name(&self) -> Option<LanguageName> {
430 None
431 }
432
433 async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
434 let config = serde_json::to_value(zed_scenario.request).unwrap();
435
436 Ok(DebugScenario {
437 adapter: zed_scenario.adapter,
438 label: zed_scenario.label,
439 build: None,
440 config,
441 tcp_connection: None,
442 })
443 }
444
445 async fn get_binary(
446 &self,
447 _: &Arc<dyn DapDelegate>,
448 task_definition: &DebugTaskDefinition,
449 _: Option<PathBuf>,
450 _: Option<Vec<String>>,
451 _: &mut AsyncApp,
452 ) -> Result<DebugAdapterBinary> {
453 let connection = task_definition
454 .tcp_connection
455 .as_ref()
456 .map(|connection| TcpArguments {
457 host: connection.host(),
458 port: connection.port.unwrap_or(17),
459 timeout: connection.timeout,
460 });
461 Ok(DebugAdapterBinary {
462 command: Some("command".into()),
463 arguments: vec![],
464 connection,
465 envs: HashMap::default(),
466 cwd: None,
467 request_args: StartDebuggingRequestArguments {
468 request: self.request_kind(&task_definition.config).await?,
469 configuration: task_definition.config.clone(),
470 },
471 })
472 }
473}