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::{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}