1use adapters::latest_github_release;
2use dap::adapters::{DebugTaskDefinition, TcpArguments};
3use gpui::AsyncApp;
4use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
5use util::ResultExt;
6
7use crate::*;
8
9#[derive(Default)]
10pub(crate) struct PhpDebugAdapter {
11 checked: OnceLock<()>,
12}
13
14impl PhpDebugAdapter {
15 const ADAPTER_NAME: &'static str = "PHP";
16 const ADAPTER_PACKAGE_NAME: &'static str = "vscode-php-debug";
17 const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js";
18
19 fn request_args(
20 &self,
21 config: &DebugTaskDefinition,
22 ) -> Result<dap::StartDebuggingRequestArguments> {
23 match &config.request {
24 dap::DebugRequest::Attach(_) => {
25 anyhow::bail!("php adapter does not support attaching")
26 }
27 dap::DebugRequest::Launch(launch_config) => Ok(dap::StartDebuggingRequestArguments {
28 configuration: json!({
29 "program": launch_config.program,
30 "cwd": launch_config.cwd,
31 "args": launch_config.args,
32 "env": launch_config.env_json(),
33 "stopOnEntry": config.stop_on_entry.unwrap_or_default(),
34 }),
35 request: config.request.to_dap(),
36 }),
37 }
38 }
39
40 async fn fetch_latest_adapter_version(
41 &self,
42 delegate: &dyn DapDelegate,
43 ) -> Result<AdapterVersion> {
44 let release = latest_github_release(
45 &format!("{}/{}", "xdebug", Self::ADAPTER_PACKAGE_NAME),
46 true,
47 false,
48 delegate.http_client(),
49 )
50 .await?;
51
52 let asset_name = format!("php-debug-{}.vsix", release.tag_name.replace("v", ""));
53
54 Ok(AdapterVersion {
55 tag_name: release.tag_name,
56 url: release
57 .assets
58 .iter()
59 .find(|asset| asset.name == asset_name)
60 .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?
61 .browser_download_url
62 .clone(),
63 })
64 }
65
66 async fn get_installed_binary(
67 &self,
68 delegate: &dyn DapDelegate,
69 config: &DebugTaskDefinition,
70 user_installed_path: Option<PathBuf>,
71 _: &mut AsyncApp,
72 ) -> Result<DebugAdapterBinary> {
73 let adapter_path = if let Some(user_installed_path) = user_installed_path {
74 user_installed_path
75 } else {
76 let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
77
78 let file_name_prefix = format!("{}_", self.name());
79
80 util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
81 file_name.starts_with(&file_name_prefix)
82 })
83 .await
84 .ok_or_else(|| anyhow!("Couldn't find PHP dap directory"))?
85 };
86
87 let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
88 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
89
90 Ok(DebugAdapterBinary {
91 command: delegate
92 .node_runtime()
93 .binary_path()
94 .await?
95 .to_string_lossy()
96 .into_owned(),
97 arguments: vec![
98 adapter_path
99 .join(Self::ADAPTER_PATH)
100 .to_string_lossy()
101 .to_string(),
102 format!("--server={}", port),
103 ],
104 connection: Some(TcpArguments {
105 port,
106 host,
107 timeout,
108 }),
109 cwd: None,
110 envs: HashMap::default(),
111 request_args: self.request_args(config)?,
112 })
113 }
114}
115
116#[async_trait(?Send)]
117impl DebugAdapter for PhpDebugAdapter {
118 fn name(&self) -> DebugAdapterName {
119 DebugAdapterName(Self::ADAPTER_NAME.into())
120 }
121
122 async fn get_binary(
123 &self,
124 delegate: &dyn DapDelegate,
125 config: &DebugTaskDefinition,
126 user_installed_path: Option<PathBuf>,
127 cx: &mut AsyncApp,
128 ) -> Result<DebugAdapterBinary> {
129 if self.checked.set(()).is_ok() {
130 delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
131 if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
132 adapters::download_adapter_from_github(
133 self.name(),
134 version,
135 adapters::DownloadedFileType::Vsix,
136 delegate,
137 )
138 .await?;
139 }
140 }
141
142 self.get_installed_binary(delegate, &config, user_installed_path, cx)
143 .await
144 }
145}