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