1use adapters::latest_github_release;
2use anyhow::Context as _;
3use anyhow::bail;
4use dap::StartDebuggingRequestArguments;
5use dap::adapters::{DebugTaskDefinition, TcpArguments};
6use gpui::{AsyncApp, SharedString};
7use language::LanguageName;
8use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
9use util::ResultExt;
10
11use crate::*;
12
13#[derive(Default)]
14pub(crate) struct PhpDebugAdapter {
15 checked: OnceLock<()>,
16}
17
18impl PhpDebugAdapter {
19 const ADAPTER_NAME: &'static str = "PHP";
20 const ADAPTER_PACKAGE_NAME: &'static str = "vscode-php-debug";
21 const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js";
22
23 async fn fetch_latest_adapter_version(
24 &self,
25 delegate: &Arc<dyn DapDelegate>,
26 ) -> Result<AdapterVersion> {
27 let release = latest_github_release(
28 &format!("{}/{}", "xdebug", Self::ADAPTER_PACKAGE_NAME),
29 true,
30 false,
31 delegate.http_client(),
32 )
33 .await?;
34
35 let asset_name = format!("php-debug-{}.vsix", release.tag_name.replace("v", ""));
36
37 Ok(AdapterVersion {
38 tag_name: release.tag_name,
39 url: release
40 .assets
41 .iter()
42 .find(|asset| asset.name == asset_name)
43 .with_context(|| format!("no asset found matching {asset_name:?}"))?
44 .browser_download_url
45 .clone(),
46 })
47 }
48
49 async fn get_installed_binary(
50 &self,
51 delegate: &Arc<dyn DapDelegate>,
52 task_definition: &DebugTaskDefinition,
53 user_installed_path: Option<PathBuf>,
54 _: &mut AsyncApp,
55 ) -> Result<DebugAdapterBinary> {
56 let adapter_path = if let Some(user_installed_path) = user_installed_path {
57 user_installed_path
58 } else {
59 let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
60
61 let file_name_prefix = format!("{}_", self.name());
62
63 util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
64 file_name.starts_with(&file_name_prefix)
65 })
66 .await
67 .context("Couldn't find PHP dap directory")?
68 };
69
70 let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
71 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
72
73 Ok(DebugAdapterBinary {
74 command: delegate
75 .node_runtime()
76 .binary_path()
77 .await?
78 .to_string_lossy()
79 .into_owned(),
80 arguments: vec![
81 adapter_path
82 .join(Self::ADAPTER_PATH)
83 .to_string_lossy()
84 .to_string(),
85 format!("--server={}", port),
86 ],
87 connection: Some(TcpArguments {
88 port,
89 host,
90 timeout,
91 }),
92 cwd: Some(delegate.worktree_root_path().to_path_buf()),
93 envs: HashMap::default(),
94 request_args: StartDebuggingRequestArguments {
95 configuration: task_definition.config.clone(),
96 request: dap::StartDebuggingRequestArgumentsRequest::Launch,
97 },
98 })
99 }
100}
101
102#[async_trait(?Send)]
103impl DebugAdapter for PhpDebugAdapter {
104 async fn dap_schema(&self) -> serde_json::Value {
105 json!({
106 "properties": {
107 "request": {
108 "type": "string",
109 "enum": ["launch"],
110 "description": "The request type for the PHP debug adapter, always \"launch\"",
111 "default": "launch"
112 },
113 "hostname": {
114 "type": "string",
115 "description": "The address to bind to when listening for Xdebug (default: all IPv6 connections if available, else all IPv4 connections) or Unix Domain socket (prefix with unix://) or Windows Pipe (\\\\?\\pipe\\name) - cannot be combined with port"
116 },
117 "port": {
118 "type": "integer",
119 "description": "The port on which to listen for Xdebug (default: 9003). If port is set to 0 a random port is chosen by the system and a placeholder ${port} is replaced with the chosen port in env and runtimeArgs.",
120 "default": 9003
121 },
122 "program": {
123 "type": "string",
124 "description": "The PHP script to debug (typically a path to a file)",
125 "default": "${file}"
126 },
127 "cwd": {
128 "type": "string",
129 "description": "Working directory for the debugged program"
130 },
131 "args": {
132 "type": "array",
133 "items": {
134 "type": "string"
135 },
136 "description": "Command line arguments to pass to the program"
137 },
138 "env": {
139 "type": "object",
140 "description": "Environment variables to pass to the program",
141 "additionalProperties": {
142 "type": "string"
143 }
144 },
145 "stopOnEntry": {
146 "type": "boolean",
147 "description": "Whether to break at the beginning of the script",
148 "default": false
149 },
150 "pathMappings": {
151 "type": "array",
152 "description": "A list of server paths mapping to the local source paths on your machine for remote host debugging",
153 "items": {
154 "type": "object",
155 "properties": {
156 "serverPath": {
157 "type": "string",
158 "description": "Path on the server"
159 },
160 "localPath": {
161 "type": "string",
162 "description": "Corresponding path on the local machine"
163 }
164 },
165 "required": ["serverPath", "localPath"]
166 }
167 },
168 "log": {
169 "type": "boolean",
170 "description": "Whether to log all communication between editor and the adapter to the debug console",
171 "default": false
172 },
173 "ignore": {
174 "type": "array",
175 "description": "An array of glob patterns that errors should be ignored from (for example **/vendor/**/*.php)",
176 "items": {
177 "type": "string"
178 }
179 },
180 "ignoreExceptions": {
181 "type": "array",
182 "description": "An array of exception class names that should be ignored (for example BaseException, \\NS1\\Exception, \\*\\Exception or \\**\\Exception*)",
183 "items": {
184 "type": "string"
185 }
186 },
187 "skipFiles": {
188 "type": "array",
189 "description": "An array of glob patterns to skip when debugging. Star patterns and negations are allowed.",
190 "items": {
191 "type": "string"
192 }
193 },
194 "skipEntryPaths": {
195 "type": "array",
196 "description": "An array of glob patterns to immediately detach from and ignore for debugging if the entry script matches",
197 "items": {
198 "type": "string"
199 }
200 },
201 "maxConnections": {
202 "type": "integer",
203 "description": "Accept only this number of parallel debugging sessions. Additional connections will be dropped.",
204 "default": 1
205 },
206 "proxy": {
207 "type": "object",
208 "description": "DBGp Proxy settings",
209 "properties": {
210 "enable": {
211 "type": "boolean",
212 "description": "To enable proxy registration",
213 "default": false
214 },
215 "host": {
216 "type": "string",
217 "description": "The address of the proxy. Supports host name, IP address, or Unix domain socket.",
218 "default": "127.0.0.1"
219 },
220 "port": {
221 "type": "integer",
222 "description": "The port where the adapter will register with the proxy",
223 "default": 9001
224 },
225 "key": {
226 "type": "string",
227 "description": "A unique key that allows the proxy to match requests to your editor",
228 "default": "vsc"
229 },
230 "timeout": {
231 "type": "integer",
232 "description": "The number of milliseconds to wait before giving up on the connection to proxy",
233 "default": 3000
234 },
235 "allowMultipleSessions": {
236 "type": "boolean",
237 "description": "If the proxy should forward multiple sessions/connections at the same time or not",
238 "default": true
239 }
240 }
241 },
242 "xdebugSettings": {
243 "type": "object",
244 "description": "Allows you to override Xdebug's remote debugging settings to fine tune Xdebug to your needs",
245 "properties": {
246 "max_children": {
247 "type": "integer",
248 "description": "Max number of array or object children to initially retrieve"
249 },
250 "max_data": {
251 "type": "integer",
252 "description": "Max amount of variable data to initially retrieve"
253 },
254 "max_depth": {
255 "type": "integer",
256 "description": "Maximum depth that the debugger engine may return when sending arrays, hashes or object structures to the IDE"
257 },
258 "show_hidden": {
259 "type": "integer",
260 "description": "Whether to show detailed internal information on properties (e.g. private members of classes). Zero means hidden members are not shown.",
261 "enum": [0, 1]
262 },
263 "breakpoint_include_return_value": {
264 "type": "boolean",
265 "description": "Determines whether to enable an additional \"return from function\" debugging step, allowing inspection of the return value when a function call returns"
266 }
267 }
268 },
269 "xdebugCloudToken": {
270 "type": "string",
271 "description": "Instead of listening locally, open a connection and register with Xdebug Cloud and accept debugging sessions on that connection"
272 },
273 "stream": {
274 "type": "object",
275 "description": "Allows to influence DBGp streams. Xdebug only supports stdout",
276 "properties": {
277 "stdout": {
278 "type": "integer",
279 "description": "Redirect stdout stream: 0 (disable), 1 (copy), 2 (redirect)",
280 "enum": [0, 1, 2],
281 "default": 0
282 }
283 }
284 }
285 },
286 "required": ["request", "program"]
287 })
288 }
289
290 fn name(&self) -> DebugAdapterName {
291 DebugAdapterName(Self::ADAPTER_NAME.into())
292 }
293
294 fn adapter_language_name(&self) -> Option<LanguageName> {
295 Some(SharedString::new_static("PHP").into())
296 }
297
298 fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
299 let obj = match &zed_scenario.request {
300 dap::DebugRequest::Attach(_) => {
301 bail!("Php adapter doesn't support attaching")
302 }
303 dap::DebugRequest::Launch(launch_config) => json!({
304 "program": launch_config.program,
305 "cwd": launch_config.cwd,
306 "args": launch_config.args,
307 "env": launch_config.env_json(),
308 "stopOnEntry": zed_scenario.stop_on_entry.unwrap_or_default(),
309 }),
310 };
311
312 Ok(DebugScenario {
313 adapter: zed_scenario.adapter,
314 label: zed_scenario.label,
315 build: None,
316 config: obj,
317 tcp_connection: None,
318 })
319 }
320
321 async fn get_binary(
322 &self,
323 delegate: &Arc<dyn DapDelegate>,
324 task_definition: &DebugTaskDefinition,
325 user_installed_path: Option<PathBuf>,
326 cx: &mut AsyncApp,
327 ) -> Result<DebugAdapterBinary> {
328 if self.checked.set(()).is_ok() {
329 delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
330 if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
331 adapters::download_adapter_from_github(
332 self.name(),
333 version,
334 adapters::DownloadedFileType::Vsix,
335 delegate.as_ref(),
336 )
337 .await?;
338 }
339 }
340
341 self.get_installed_binary(delegate, &task_definition, user_installed_path, cx)
342 .await
343 }
344}