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