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