
### DISCLAIMER > As of 6th March 2025, debugger is still in development. We plan to merge it behind a staff-only feature flag for staff use only, followed by non-public release and then finally a public one (akin to how Git panel release was handled). This is done to ensure the best experience when it gets released. ### END OF DISCLAIMER **The current state of the debugger implementation:** https://github.com/user-attachments/assets/c4deff07-80dd-4dc6-ad2e-0c252a478fe9 https://github.com/user-attachments/assets/e1ed2345-b750-4bb6-9c97-50961b76904f ---- All the todo's are in the following channel, so it's easier to work on this together: https://zed.dev/channel/zed-debugger-11370 If you are on Linux, you can use the following command to join the channel: ```cli zed https://zed.dev/channel/zed-debugger-11370 ``` ## Current Features - Collab - Breakpoints - Sync when you (re)join a project - Sync when you add/remove a breakpoint - Sync active debug line - Stack frames - Click on stack frame - View variables that belong to the stack frame - Visit the source file - Restart stack frame (if adapter supports this) - Variables - Loaded sources - Modules - Controls - Continue - Step back - Stepping granularity (configurable) - Step into - Stepping granularity (configurable) - Step over - Stepping granularity (configurable) - Step out - Stepping granularity (configurable) - Debug console - Breakpoints - Log breakpoints - line breakpoints - Persistent between zed sessions (configurable) - Multi buffer support - Toggle disable/enable all breakpoints - Stack frames - Click on stack frame - View variables that belong to the stack frame - Visit the source file - Show collapsed stack frames - Restart stack frame (if adapter supports this) - Loaded sources - View all used loaded sources if supported by adapter. - Modules - View all used modules (if adapter supports this) - Variables - Copy value - Copy name - Copy memory reference - Set value (if adapter supports this) - keyboard navigation - Debug Console - See logs - View output that was sent from debug adapter - Output grouping - Evaluate code - Updates the variable list - Auto completion - If not supported by adapter, we will show auto-completion for existing variables - Debug Terminal - Run custom commands and change env values right inside your Zed terminal - Attach to process (if adapter supports this) - Process picker - Controls - Continue - Step back - Stepping granularity (configurable) - Step into - Stepping granularity (configurable) - Step over - Stepping granularity (configurable) - Step out - Stepping granularity (configurable) - Disconnect - Restart - Stop - Warning when a debug session exited without hitting any breakpoint - Debug view to see Adapter/RPC log messages - Testing - Fake debug adapter - Fake requests & events --- Release Notes: - N/A --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Co-authored-by: Anthony Eid <hello@anthonyeid.me> Co-authored-by: Anthony <anthony@zed.dev> Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com> Co-authored-by: Piotr <piotr@zed.dev>
370 lines
10 KiB
Rust
370 lines
10 KiB
Rust
use ::fs::Fs;
|
|
use anyhow::{anyhow, Context as _, Ok, Result};
|
|
use async_compression::futures::bufread::GzipDecoder;
|
|
use async_tar::Archive;
|
|
use async_trait::async_trait;
|
|
use futures::io::BufReader;
|
|
use gpui::{AsyncApp, SharedString};
|
|
pub use http_client::{github::latest_github_release, HttpClient};
|
|
use language::LanguageToolchainStore;
|
|
use node_runtime::NodeRuntime;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
use settings::WorktreeId;
|
|
use smol::{self, fs::File, lock::Mutex};
|
|
use std::{
|
|
collections::{HashMap, HashSet},
|
|
ffi::{OsStr, OsString},
|
|
fmt::Debug,
|
|
net::Ipv4Addr,
|
|
ops::Deref,
|
|
path::{Path, PathBuf},
|
|
sync::Arc,
|
|
};
|
|
use task::DebugAdapterConfig;
|
|
use util::ResultExt;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum DapStatus {
|
|
None,
|
|
CheckingForUpdate,
|
|
Downloading,
|
|
Failed { error: String },
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
pub trait DapDelegate {
|
|
fn worktree_id(&self) -> WorktreeId;
|
|
fn http_client(&self) -> Arc<dyn HttpClient>;
|
|
fn node_runtime(&self) -> NodeRuntime;
|
|
fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore>;
|
|
fn fs(&self) -> Arc<dyn Fs>;
|
|
fn updated_adapters(&self) -> Arc<Mutex<HashSet<DebugAdapterName>>>;
|
|
fn update_status(&self, dap_name: DebugAdapterName, status: DapStatus);
|
|
fn which(&self, command: &OsStr) -> Option<PathBuf>;
|
|
async fn shell_env(&self) -> collections::HashMap<String, String>;
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
|
|
pub struct DebugAdapterName(pub Arc<str>);
|
|
|
|
impl Deref for DebugAdapterName {
|
|
type Target = str;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl AsRef<str> for DebugAdapterName {
|
|
fn as_ref(&self) -> &str {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl AsRef<Path> for DebugAdapterName {
|
|
fn as_ref(&self) -> &Path {
|
|
Path::new(&*self.0)
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for DebugAdapterName {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
std::fmt::Display::fmt(&self.0, f)
|
|
}
|
|
}
|
|
|
|
impl From<DebugAdapterName> for SharedString {
|
|
fn from(name: DebugAdapterName) -> Self {
|
|
SharedString::from(name.0)
|
|
}
|
|
}
|
|
|
|
impl<'a> From<&'a str> for DebugAdapterName {
|
|
fn from(str: &'a str) -> DebugAdapterName {
|
|
DebugAdapterName(str.to_string().into())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct TcpArguments {
|
|
pub host: Ipv4Addr,
|
|
pub port: u16,
|
|
pub timeout: Option<u64>,
|
|
}
|
|
#[derive(Debug, Clone)]
|
|
pub struct DebugAdapterBinary {
|
|
pub command: String,
|
|
pub arguments: Option<Vec<OsString>>,
|
|
pub envs: Option<HashMap<String, String>>,
|
|
pub cwd: Option<PathBuf>,
|
|
pub connection: Option<TcpArguments>,
|
|
}
|
|
|
|
pub struct AdapterVersion {
|
|
pub tag_name: String,
|
|
pub url: String,
|
|
}
|
|
|
|
pub enum DownloadedFileType {
|
|
Vsix,
|
|
GzipTar,
|
|
Zip,
|
|
}
|
|
|
|
pub struct GithubRepo {
|
|
pub repo_name: String,
|
|
pub repo_owner: String,
|
|
}
|
|
|
|
pub async fn download_adapter_from_github(
|
|
adapter_name: DebugAdapterName,
|
|
github_version: AdapterVersion,
|
|
file_type: DownloadedFileType,
|
|
delegate: &dyn DapDelegate,
|
|
) -> Result<PathBuf> {
|
|
let adapter_path = paths::debug_adapters_dir().join(&adapter_name);
|
|
let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name));
|
|
let fs = delegate.fs();
|
|
|
|
if version_path.exists() {
|
|
return Ok(version_path);
|
|
}
|
|
|
|
if !adapter_path.exists() {
|
|
fs.create_dir(&adapter_path.as_path())
|
|
.await
|
|
.context("Failed creating adapter path")?;
|
|
}
|
|
|
|
log::debug!(
|
|
"Downloading adapter {} from {}",
|
|
adapter_name,
|
|
&github_version.url,
|
|
);
|
|
|
|
let mut response = delegate
|
|
.http_client()
|
|
.get(&github_version.url, Default::default(), true)
|
|
.await
|
|
.context("Error downloading release")?;
|
|
if !response.status().is_success() {
|
|
Err(anyhow!(
|
|
"download failed with status {}",
|
|
response.status().to_string()
|
|
))?;
|
|
}
|
|
|
|
match file_type {
|
|
DownloadedFileType::GzipTar => {
|
|
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
|
let archive = Archive::new(decompressed_bytes);
|
|
archive.unpack(&version_path).await?;
|
|
}
|
|
DownloadedFileType::Zip | DownloadedFileType::Vsix => {
|
|
let zip_path = version_path.with_extension("zip");
|
|
|
|
let mut file = File::create(&zip_path).await?;
|
|
futures::io::copy(response.body_mut(), &mut file).await?;
|
|
|
|
// we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
|
|
util::command::new_smol_command("unzip")
|
|
.arg(&zip_path)
|
|
.arg("-d")
|
|
.arg(&version_path)
|
|
.output()
|
|
.await?;
|
|
|
|
util::fs::remove_matching(&adapter_path, |entry| {
|
|
entry
|
|
.file_name()
|
|
.is_some_and(|file| file.to_string_lossy().ends_with(".zip"))
|
|
})
|
|
.await;
|
|
}
|
|
}
|
|
|
|
// remove older versions
|
|
util::fs::remove_matching(&adapter_path, |entry| {
|
|
entry.to_string_lossy() != version_path.to_string_lossy()
|
|
})
|
|
.await;
|
|
|
|
Ok(version_path)
|
|
}
|
|
|
|
pub async fn fetch_latest_adapter_version_from_github(
|
|
github_repo: GithubRepo,
|
|
delegate: &dyn DapDelegate,
|
|
) -> Result<AdapterVersion> {
|
|
let release = latest_github_release(
|
|
&format!("{}/{}", github_repo.repo_owner, github_repo.repo_name),
|
|
false,
|
|
false,
|
|
delegate.http_client(),
|
|
)
|
|
.await?;
|
|
|
|
Ok(AdapterVersion {
|
|
tag_name: release.tag_name,
|
|
url: release.zipball_url,
|
|
})
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
pub trait DebugAdapter: 'static + Send + Sync {
|
|
fn name(&self) -> DebugAdapterName;
|
|
|
|
async fn get_binary(
|
|
&self,
|
|
delegate: &dyn DapDelegate,
|
|
config: &DebugAdapterConfig,
|
|
user_installed_path: Option<PathBuf>,
|
|
cx: &mut AsyncApp,
|
|
) -> Result<DebugAdapterBinary> {
|
|
if delegate
|
|
.updated_adapters()
|
|
.lock()
|
|
.await
|
|
.contains(&self.name())
|
|
{
|
|
log::info!("Using cached debug adapter binary {}", self.name());
|
|
|
|
if let Some(binary) = self
|
|
.get_installed_binary(delegate, &config, user_installed_path.clone(), cx)
|
|
.await
|
|
.log_err()
|
|
{
|
|
return Ok(binary);
|
|
}
|
|
|
|
log::info!(
|
|
"Cached binary {} is corrupt falling back to install",
|
|
self.name()
|
|
);
|
|
}
|
|
|
|
log::info!("Getting latest version of debug adapter {}", self.name());
|
|
delegate.update_status(self.name(), DapStatus::CheckingForUpdate);
|
|
if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
|
|
log::info!(
|
|
"Installiing latest version of debug adapter {}",
|
|
self.name()
|
|
);
|
|
delegate.update_status(self.name(), DapStatus::Downloading);
|
|
self.install_binary(version, delegate).await?;
|
|
|
|
delegate
|
|
.updated_adapters()
|
|
.lock_arc()
|
|
.await
|
|
.insert(self.name());
|
|
}
|
|
|
|
self.get_installed_binary(delegate, &config, user_installed_path, cx)
|
|
.await
|
|
}
|
|
|
|
async fn fetch_latest_adapter_version(
|
|
&self,
|
|
delegate: &dyn DapDelegate,
|
|
) -> Result<AdapterVersion>;
|
|
|
|
/// Installs the binary for the debug adapter.
|
|
/// This method is called when the adapter binary is not found or needs to be updated.
|
|
/// It should download and install the necessary files for the debug adapter to function.
|
|
async fn install_binary(
|
|
&self,
|
|
version: AdapterVersion,
|
|
delegate: &dyn DapDelegate,
|
|
) -> Result<()>;
|
|
|
|
async fn get_installed_binary(
|
|
&self,
|
|
delegate: &dyn DapDelegate,
|
|
config: &DebugAdapterConfig,
|
|
user_installed_path: Option<PathBuf>,
|
|
cx: &mut AsyncApp,
|
|
) -> Result<DebugAdapterBinary>;
|
|
|
|
/// Should return base configuration to make the debug adapter work
|
|
fn request_args(&self, config: &DebugAdapterConfig) -> Value;
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub struct FakeAdapter {}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
impl FakeAdapter {
|
|
const ADAPTER_NAME: &'static str = "fake-adapter";
|
|
|
|
pub fn new() -> Self {
|
|
Self {}
|
|
}
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
#[async_trait(?Send)]
|
|
impl DebugAdapter for FakeAdapter {
|
|
fn name(&self) -> DebugAdapterName {
|
|
DebugAdapterName(Self::ADAPTER_NAME.into())
|
|
}
|
|
|
|
async fn get_binary(
|
|
&self,
|
|
_: &dyn DapDelegate,
|
|
_: &DebugAdapterConfig,
|
|
_: Option<PathBuf>,
|
|
_: &mut AsyncApp,
|
|
) -> Result<DebugAdapterBinary> {
|
|
Ok(DebugAdapterBinary {
|
|
command: "command".into(),
|
|
arguments: None,
|
|
connection: None,
|
|
envs: None,
|
|
cwd: None,
|
|
})
|
|
}
|
|
|
|
async fn fetch_latest_adapter_version(
|
|
&self,
|
|
_delegate: &dyn DapDelegate,
|
|
) -> Result<AdapterVersion> {
|
|
unimplemented!("fetch latest adapter version");
|
|
}
|
|
|
|
async fn install_binary(
|
|
&self,
|
|
_version: AdapterVersion,
|
|
_delegate: &dyn DapDelegate,
|
|
) -> Result<()> {
|
|
unimplemented!("install binary");
|
|
}
|
|
|
|
async fn get_installed_binary(
|
|
&self,
|
|
_: &dyn DapDelegate,
|
|
_: &DebugAdapterConfig,
|
|
_: Option<PathBuf>,
|
|
_: &mut AsyncApp,
|
|
) -> Result<DebugAdapterBinary> {
|
|
unimplemented!("get installed binary");
|
|
}
|
|
|
|
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
|
|
use serde_json::json;
|
|
use task::DebugRequestType;
|
|
|
|
json!({
|
|
"request": match config.request {
|
|
DebugRequestType::Launch => "launch",
|
|
DebugRequestType::Attach(_) => "attach",
|
|
},
|
|
"process_id": if let DebugRequestType::Attach(attach_config) = &config.request {
|
|
attach_config.process_id
|
|
} else {
|
|
None
|
|
},
|
|
})
|
|
}
|
|
}
|