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