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