php.rs

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