use adapters::latest_github_release;use anyhow::Context as _;use anyhow::bail;use dap::StartDebuggingRequestArguments;use dap::StartDebuggingRequestArgumentsRequest;use dap::adapters::{DebugTaskDefinition, TcpArguments};use gpui::{AsyncApp, SharedString};use language::LanguageName;use std::{collections::HashMap, path::PathBuf, sync::OnceLock};use util::ResultExt; use crate::*; #[derive(Default)]pub(crate) struct PhpDebugAdapter { checked: OnceLock<()>,} impl PhpDebugAdapter { const ADAPTER_NAME: &'static str = "PHP"; const ADAPTER_PACKAGE_NAME: &'static str = "vscode-php-debug"; const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js"; async fn fetch_latest_adapter_version( &self, delegate: &Arc<dyn DapDelegate>, ) -> Result<AdapterVersion> { let release = latest_github_release( &format!("{}/{}", "xdebug", Self::ADAPTER_PACKAGE_NAME), true, false, delegate.http_client(), ) .await?; let asset_name = format!("php-debug-{}.vsix", release.tag_name.replace("v", "")); Ok(AdapterVersion { tag_name: release.tag_name, url: release .assets .iter() .find(|asset| asset.name == asset_name) .with_context(|| format!("no asset found matching {asset_name:?}"))? .browser_download_url .clone(), }) } async fn get_installed_binary( &self, delegate: &Arc<dyn DapDelegate>, task_definition: &DebugTaskDefinition, user_installed_path: Option<PathBuf>, _: &mut AsyncApp, ) -> Result<DebugAdapterBinary> { let adapter_path = if let Some(user_installed_path) = user_installed_path { user_installed_path } else { let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref()); let file_name_prefix = format!("{}_", self.name()); util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { file_name.starts_with(&file_name_prefix) }) .await .context("Couldn't find PHP dap directory")? }; let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default(); let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; Ok(DebugAdapterBinary { command: delegate .node_runtime() .binary_path() .await? .to_string_lossy() .into_owned(), arguments: vec![ adapter_path .join(Self::ADAPTER_PATH) .to_string_lossy() .to_string(), format!("--server={}", port), ], connection: Some(TcpArguments { port, host, timeout, }), cwd: Some(delegate.worktree_root_path().to_path_buf()), envs: HashMap::default(), request_args: StartDebuggingRequestArguments { configuration: task_definition.config.clone(), request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)?, }, }) }} #[async_trait(?Send)]impl DebugAdapter for PhpDebugAdapter { async fn dap_schema(&self) -> serde_json::Value { json!({ "properties": { "request": { "type": "string", "enum": ["launch"], "description": "The request type for the PHP debug adapter, always \"launch\"", "default": "launch" }, "hostname": { "type": "string", "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" }, "port": { "type": "integer", "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.", "default": 9003 }, "program": { "type": "string", "description": "The PHP script to debug (typically a path to a file)", "default": "${file}" }, "cwd": { "type": "string", "description": "Working directory for the debugged program" }, "args": { "type": "array", "items": { "type": "string" }, "description": "Command line arguments to pass to the program" }, "env": { "type": "object", "description": "Environment variables to pass to the program", "additionalProperties": { "type": "string" } }, "stopOnEntry": { "type": "boolean", "description": "Whether to break at the beginning of the script", "default": false }, "pathMappings": { "type": "object", "description": "A mapping of server paths to local paths.", }, "log": { "type": "boolean", "description": "Whether to log all communication between editor and the adapter to the debug console", "default": false }, "ignore": { "type": "array", "description": "An array of glob patterns that errors should be ignored from (for example **/vendor/**/*.php)", "items": { "type": "string" } }, "ignoreExceptions": { "type": "array", "description": "An array of exception class names that should be ignored (for example BaseException, \\NS1\\Exception, \\*\\Exception or \\**\\Exception*)", "items": { "type": "string" } }, "skipFiles": { "type": "array", "description": "An array of glob patterns to skip when debugging. Star patterns and negations are allowed.", "items": { "type": "string" } }, "skipEntryPaths": { "type": "array", "description": "An array of glob patterns to immediately detach from and ignore for debugging if the entry script matches", "items": { "type": "string" } }, "maxConnections": { "type": "integer", "description": "Accept only this number of parallel debugging sessions. Additional connections will be dropped.", "default": 1 }, "proxy": { "type": "object", "description": "DBGp Proxy settings", "properties": { "enable": { "type": "boolean", "description": "To enable proxy registration", "default": false }, "host": { "type": "string", "description": "The address of the proxy. Supports host name, IP address, or Unix domain socket.", "default": "127.0.0.1" }, "port": { "type": "integer", "description": "The port where the adapter will register with the proxy", "default": 9001 }, "key": { "type": "string", "description": "A unique key that allows the proxy to match requests to your editor", "default": "vsc" }, "timeout": { "type": "integer", "description": "The number of milliseconds to wait before giving up on the connection to proxy", "default": 3000 }, "allowMultipleSessions": { "type": "boolean", "description": "If the proxy should forward multiple sessions/connections at the same time or not", "default": true } } }, "xdebugSettings": { "type": "object", "description": "Allows you to override Xdebug's remote debugging settings to fine tune Xdebug to your needs", "properties": { "max_children": { "type": "integer", "description": "Max number of array or object children to initially retrieve" }, "max_data": { "type": "integer", "description": "Max amount of variable data to initially retrieve" }, "max_depth": { "type": "integer", "description": "Maximum depth that the debugger engine may return when sending arrays, hashes or object structures to the IDE" }, "show_hidden": { "type": "integer", "description": "Whether to show detailed internal information on properties (e.g. private members of classes). Zero means hidden members are not shown.", "enum": [0, 1] }, "breakpoint_include_return_value": { "type": "boolean", "description": "Determines whether to enable an additional \"return from function\" debugging step, allowing inspection of the return value when a function call returns" } } }, "xdebugCloudToken": { "type": "string", "description": "Instead of listening locally, open a connection and register with Xdebug Cloud and accept debugging sessions on that connection" }, "stream": { "type": "object", "description": "Allows to influence DBGp streams. Xdebug only supports stdout", "properties": { "stdout": { "type": "integer", "description": "Redirect stdout stream: 0 (disable), 1 (copy), 2 (redirect)", "enum": [0, 1, 2], "default": 0 } } } }, "required": ["request", "program"] }) } fn name(&self) -> DebugAdapterName { DebugAdapterName(Self::ADAPTER_NAME.into()) } fn adapter_language_name(&self) -> Option<LanguageName> { Some(SharedString::new_static("PHP").into()) } fn request_kind(&self, _: &serde_json::Value) -> Result<StartDebuggingRequestArgumentsRequest> { Ok(StartDebuggingRequestArgumentsRequest::Launch) } fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> { let obj = match &zed_scenario.request { dap::DebugRequest::Attach(_) => { bail!("Php adapter doesn't support attaching") } dap::DebugRequest::Launch(launch_config) => json!({ "program": launch_config.program, "cwd": launch_config.cwd, "args": launch_config.args, "env": launch_config.env_json(), "stopOnEntry": zed_scenario.stop_on_entry.unwrap_or_default(), }), }; Ok(DebugScenario { adapter: zed_scenario.adapter, label: zed_scenario.label, build: None, config: obj, tcp_connection: None, }) } async fn get_binary( &self, delegate: &Arc<dyn DapDelegate>, task_definition: &DebugTaskDefinition, user_installed_path: Option<PathBuf>, cx: &mut AsyncApp, ) -> Result<DebugAdapterBinary> { if self.checked.set(()).is_ok() { delegate.output_to_console(format!("Checking latest version of {}...", self.name())); if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() { adapters::download_adapter_from_github( self.name(), version, adapters::DownloadedFileType::Vsix, delegate.as_ref(), ) .await?; } } self.get_installed_binary(delegate, &task_definition, user_installed_path, cx) .await }}
use adapters::latest_github_release;
use anyhow::Context as _;
use anyhow::bail;
use dap::StartDebuggingRequestArguments;
use dap::StartDebuggingRequestArgumentsRequest;
use dap::adapters::{DebugTaskDefinition, TcpArguments};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use util::ResultExt;
use crate::*;
#[derive(Default)]
pub(crate) struct PhpDebugAdapter {
checked: OnceLock<()>,
}
impl PhpDebugAdapter {
const ADAPTER_NAME: &'static str = "PHP";
const ADAPTER_PACKAGE_NAME: &'static str = "vscode-php-debug";
const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js";
async fn fetch_latest_adapter_version(
&self,
delegate: &Arc<dyn DapDelegate>,
) -> Result<AdapterVersion> {
let release = latest_github_release(
&format!("{}/{}", "xdebug", Self::ADAPTER_PACKAGE_NAME),
true,
false,
delegate.http_client(),
)
.await?;
let asset_name = format!("php-debug-{}.vsix", release.tag_name.replace("v", ""));
Ok(AdapterVersion {
tag_name: release.tag_name,
url: release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.with_context(|| format!("no asset found matching {asset_name:?}"))?
.browser_download_url
.clone(),
})
}
async fn get_installed_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
task_definition: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
let adapter_path = if let Some(user_installed_path) = user_installed_path {
user_installed_path
} else {
let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
let file_name_prefix = format!("{}_", self.name());
util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
file_name.starts_with(&file_name_prefix)
})
.await
.context("Couldn't find PHP dap directory")?
};
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary {
command: delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
arguments: vec![
adapter_path
.join(Self::ADAPTER_PATH)
.to_string_lossy()
.to_string(),
format!("--server={}", port),
],
connection: Some(TcpArguments {
port,
host,
timeout,
}),
cwd: Some(delegate.worktree_root_path().to_path_buf()),
envs: HashMap::default(),
request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)?,
},
})
}
}
#[async_trait(?Send)]
impl DebugAdapter for PhpDebugAdapter {
async fn dap_schema(&self) -> serde_json::Value {
json!({
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "The request type for the PHP debug adapter, always \"launch\"",
"default": "launch"
},
"hostname": {
"type": "string",
"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"
},
"port": {
"type": "integer",
"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.",
"default": 9003
},
"program": {
"type": "string",
"description": "The PHP script to debug (typically a path to a file)",
"default": "${file}"
},
"cwd": {
"type": "string",
"description": "Working directory for the debugged program"
},
"args": {
"type": "array",
"items": {
"type": "string"
},
"description": "Command line arguments to pass to the program"
},
"env": {
"type": "object",
"description": "Environment variables to pass to the program",
"additionalProperties": {
"type": "string"
}
},
"stopOnEntry": {
"type": "boolean",
"description": "Whether to break at the beginning of the script",
"default": false
},
"pathMappings": {
"type": "object",
"description": "A mapping of server paths to local paths.",
},
"log": {
"type": "boolean",
"description": "Whether to log all communication between editor and the adapter to the debug console",
"default": false
},
"ignore": {
"type": "array",
"description": "An array of glob patterns that errors should be ignored from (for example **/vendor/**/*.php)",
"items": {
"type": "string"
}
},
"ignoreExceptions": {
"type": "array",
"description": "An array of exception class names that should be ignored (for example BaseException, \\NS1\\Exception, \\*\\Exception or \\**\\Exception*)",
"items": {
"type": "string"
}
},
"skipFiles": {
"type": "array",
"description": "An array of glob patterns to skip when debugging. Star patterns and negations are allowed.",
"items": {
"type": "string"
}
},
"skipEntryPaths": {
"type": "array",
"description": "An array of glob patterns to immediately detach from and ignore for debugging if the entry script matches",
"items": {
"type": "string"
}
},
"maxConnections": {
"type": "integer",
"description": "Accept only this number of parallel debugging sessions. Additional connections will be dropped.",
"default": 1
},
"proxy": {
"type": "object",
"description": "DBGp Proxy settings",
"properties": {
"enable": {
"type": "boolean",
"description": "To enable proxy registration",
"default": false
},
"host": {
"type": "string",
"description": "The address of the proxy. Supports host name, IP address, or Unix domain socket.",
"default": "127.0.0.1"
},
"port": {
"type": "integer",
"description": "The port where the adapter will register with the proxy",
"default": 9001
},
"key": {
"type": "string",
"description": "A unique key that allows the proxy to match requests to your editor",
"default": "vsc"
},
"timeout": {
"type": "integer",
"description": "The number of milliseconds to wait before giving up on the connection to proxy",
"default": 3000
},
"allowMultipleSessions": {
"type": "boolean",
"description": "If the proxy should forward multiple sessions/connections at the same time or not",
"default": true
}
}
},
"xdebugSettings": {
"type": "object",
"description": "Allows you to override Xdebug's remote debugging settings to fine tune Xdebug to your needs",
"properties": {
"max_children": {
"type": "integer",
"description": "Max number of array or object children to initially retrieve"
},
"max_data": {
"type": "integer",
"description": "Max amount of variable data to initially retrieve"
},
"max_depth": {
"type": "integer",
"description": "Maximum depth that the debugger engine may return when sending arrays, hashes or object structures to the IDE"
},
"show_hidden": {
"type": "integer",
"description": "Whether to show detailed internal information on properties (e.g. private members of classes). Zero means hidden members are not shown.",
"enum": [0, 1]
},
"breakpoint_include_return_value": {
"type": "boolean",
"description": "Determines whether to enable an additional \"return from function\" debugging step, allowing inspection of the return value when a function call returns"
}
}
},
"xdebugCloudToken": {
"type": "string",
"description": "Instead of listening locally, open a connection and register with Xdebug Cloud and accept debugging sessions on that connection"
},
"stream": {
"type": "object",
"description": "Allows to influence DBGp streams. Xdebug only supports stdout",
"properties": {
"stdout": {
"type": "integer",
"description": "Redirect stdout stream: 0 (disable), 1 (copy), 2 (redirect)",
"enum": [0, 1, 2],
"default": 0
}
}
}
},
"required": ["request", "program"]
})
}
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("PHP").into())
}
fn request_kind(&self, _: &serde_json::Value) -> Result<StartDebuggingRequestArgumentsRequest> {
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let obj = match &zed_scenario.request {
dap::DebugRequest::Attach(_) => {
bail!("Php adapter doesn't support attaching")
}
dap::DebugRequest::Launch(launch_config) => json!({
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args,
"env": launch_config.env_json(),
"stopOnEntry": zed_scenario.stop_on_entry.unwrap_or_default(),
}),
};
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
build: None,
config: obj,
tcp_connection: None,
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
task_definition: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
if self.checked.set(()).is_ok() {
delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
adapters::download_adapter_from_github(
self.name(),
version,
adapters::DownloadedFileType::Vsix,
delegate.as_ref(),
)
.await?;
}
}
self.get_installed_binary(delegate, &task_definition, user_installed_path, cx)
.await
}
}