php.rs

  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.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}