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