Debugger implementation (#13433)
### 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>
This commit is contained in:
parent
ed4e654fdf
commit
41a60ffecf
156 changed files with 25840 additions and 451 deletions
|
@ -1,18 +1,25 @@
|
|||
pub mod model;
|
||||
|
||||
use std::{path::Path, str::FromStr};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::BTreeMap,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use client::DevServerProjectId;
|
||||
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
|
||||
use gpui::{point, size, Axis, Bounds, WindowBounds, WindowId};
|
||||
use project::debugger::breakpoint_store::{BreakpointKind, SerializedBreakpoint};
|
||||
|
||||
use language::{LanguageName, Toolchain};
|
||||
use project::WorktreeId;
|
||||
use remote::ssh_session::SshProjectId;
|
||||
use sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
statement::Statement,
|
||||
statement::{SqlType, Statement},
|
||||
};
|
||||
|
||||
use ui::px;
|
||||
|
@ -136,6 +143,125 @@ impl Column for SerializedWindowBounds {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Breakpoint {
|
||||
pub position: u32,
|
||||
pub kind: BreakpointKind,
|
||||
}
|
||||
|
||||
/// Wrapper for DB type of a breakpoint
|
||||
struct BreakpointKindWrapper<'a>(Cow<'a, BreakpointKind>);
|
||||
|
||||
impl From<BreakpointKind> for BreakpointKindWrapper<'static> {
|
||||
fn from(kind: BreakpointKind) -> Self {
|
||||
BreakpointKindWrapper(Cow::Owned(kind))
|
||||
}
|
||||
}
|
||||
impl StaticColumnCount for BreakpointKindWrapper<'_> {
|
||||
fn column_count() -> usize {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
impl Bind for BreakpointKindWrapper<'_> {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
|
||||
let next_index = statement.bind(&self.0.to_int(), start_index)?;
|
||||
|
||||
match self.0.as_ref() {
|
||||
BreakpointKind::Standard => {
|
||||
statement.bind_null(next_index)?;
|
||||
Ok(next_index + 1)
|
||||
}
|
||||
BreakpointKind::Log(message) => statement.bind(&message.as_ref(), next_index),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for BreakpointKindWrapper<'_> {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
|
||||
let kind = statement.column_int(start_index)?;
|
||||
|
||||
match kind {
|
||||
0 => Ok((BreakpointKind::Standard.into(), start_index + 2)),
|
||||
1 => {
|
||||
let message = statement.column_text(start_index)?.to_string();
|
||||
Ok((BreakpointKind::Log(message.into()).into(), start_index + 1))
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("Invalid BreakpointKind discriminant")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct is used to implement traits on Vec<breakpoint>
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
struct Breakpoints(Vec<Breakpoint>);
|
||||
|
||||
impl sqlez::bindable::StaticColumnCount for Breakpoint {
|
||||
fn column_count() -> usize {
|
||||
1 + BreakpointKindWrapper::column_count()
|
||||
}
|
||||
}
|
||||
|
||||
impl sqlez::bindable::Bind for Breakpoint {
|
||||
fn bind(
|
||||
&self,
|
||||
statement: &sqlez::statement::Statement,
|
||||
start_index: i32,
|
||||
) -> anyhow::Result<i32> {
|
||||
let next_index = statement.bind(&self.position, start_index)?;
|
||||
statement.bind(
|
||||
&BreakpointKindWrapper(Cow::Borrowed(&self.kind)),
|
||||
next_index,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for Breakpoint {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let position = statement
|
||||
.column_int(start_index)
|
||||
.with_context(|| format!("Failed to read BreakPoint at index {start_index}"))?
|
||||
as u32;
|
||||
let (kind, next_index) = BreakpointKindWrapper::column(statement, start_index + 1)?;
|
||||
|
||||
Ok((
|
||||
Breakpoint {
|
||||
position,
|
||||
kind: kind.0.into_owned(),
|
||||
},
|
||||
next_index,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for Breakpoints {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let mut breakpoints = Vec::new();
|
||||
let mut index = start_index;
|
||||
|
||||
loop {
|
||||
match statement.column_type(index) {
|
||||
Ok(SqlType::Null) => break,
|
||||
_ => {
|
||||
let position = statement
|
||||
.column_int(index)
|
||||
.with_context(|| format!("Failed to read BreakPoint at index {index}"))?
|
||||
as u32;
|
||||
let (kind, next_index) = BreakpointKindWrapper::column(statement, index + 1)?;
|
||||
|
||||
breakpoints.push(Breakpoint {
|
||||
position,
|
||||
kind: kind.0.into_owned(),
|
||||
});
|
||||
index = next_index;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((Breakpoints(breakpoints), index))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct SerializedPixels(gpui::Pixels);
|
||||
impl sqlez::bindable::StaticColumnCount for SerializedPixels {}
|
||||
|
@ -205,6 +331,14 @@ define_connection! {
|
|||
// active: bool, // Indicates if this item is the active one in the pane
|
||||
// preview: bool // Indicates if this item is a preview item
|
||||
// )
|
||||
//
|
||||
// CREATE TABLE breakpoints(
|
||||
// workspace_id: usize Foreign Key, // References workspace table
|
||||
// path: PathBuf, // The absolute path of the file that this breakpoint belongs to
|
||||
// breakpoint_location: Vec<u32>, // A list of the locations of breakpoints
|
||||
// kind: int, // The kind of breakpoint (standard, log)
|
||||
// log_message: String, // log message for log breakpoints, otherwise it's Null
|
||||
// )
|
||||
pub static ref DB: WorkspaceDb<()> =
|
||||
&[
|
||||
sql!(
|
||||
|
@ -383,6 +517,18 @@ define_connection! {
|
|||
sql!(
|
||||
ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}";
|
||||
),
|
||||
sql!(
|
||||
CREATE TABLE breakpoints (
|
||||
workspace_id INTEGER NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
breakpoint_location INTEGER NOT NULL,
|
||||
kind INTEGER NOT NULL,
|
||||
log_message TEXT,
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
);
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -470,6 +616,7 @@ impl WorkspaceDb {
|
|||
display,
|
||||
docks,
|
||||
session_id: None,
|
||||
breakpoints: self.breakpoints(workspace_id),
|
||||
window_id,
|
||||
})
|
||||
}
|
||||
|
@ -523,6 +670,7 @@ impl WorkspaceDb {
|
|||
.log_err()?,
|
||||
window_bounds,
|
||||
centered_layout: centered_layout.unwrap_or(false),
|
||||
breakpoints: self.breakpoints(workspace_id),
|
||||
display,
|
||||
docks,
|
||||
session_id: None,
|
||||
|
@ -530,6 +678,46 @@ impl WorkspaceDb {
|
|||
})
|
||||
}
|
||||
|
||||
fn breakpoints(
|
||||
&self,
|
||||
workspace_id: WorkspaceId,
|
||||
) -> BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>> {
|
||||
let breakpoints: Result<Vec<(PathBuf, Breakpoint)>> = self
|
||||
.select_bound(sql! {
|
||||
SELECT path, breakpoint_location, kind
|
||||
FROM breakpoints
|
||||
WHERE workspace_id = ?
|
||||
})
|
||||
.and_then(|mut prepared_statement| (prepared_statement)(workspace_id));
|
||||
|
||||
match breakpoints {
|
||||
Ok(bp) => {
|
||||
if bp.is_empty() {
|
||||
log::error!("Breakpoints are empty after querying database for them");
|
||||
}
|
||||
|
||||
let mut map: BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>> = Default::default();
|
||||
|
||||
for (path, breakpoint) in bp {
|
||||
let path: Arc<Path> = path.into();
|
||||
map.entry(path.clone())
|
||||
.or_default()
|
||||
.push(SerializedBreakpoint {
|
||||
position: breakpoint.position,
|
||||
path,
|
||||
kind: breakpoint.kind,
|
||||
});
|
||||
}
|
||||
|
||||
map
|
||||
}
|
||||
Err(msg) => {
|
||||
log::error!("Breakpoints query failed with msg: {msg}");
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves a workspace using the worktree roots. Will garbage collect any workspaces
|
||||
/// that used this workspace previously
|
||||
pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
|
||||
|
@ -540,6 +728,31 @@ impl WorkspaceDb {
|
|||
DELETE FROM pane_groups WHERE workspace_id = ?1;
|
||||
DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
|
||||
.context("Clearing old panes")?;
|
||||
for (path, breakpoints) in workspace.breakpoints {
|
||||
conn.exec_bound(sql!(DELETE FROM breakpoints WHERE workspace_id = ?1 AND path = ?2))?((workspace.id, path.as_ref()))
|
||||
.context("Clearing old breakpoints")?;
|
||||
for bp in breakpoints {
|
||||
let kind = BreakpointKindWrapper::from(bp.kind);
|
||||
match conn.exec_bound(sql!(
|
||||
INSERT INTO breakpoints (workspace_id, path, breakpoint_location, kind, log_message)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5);))?
|
||||
|
||||
((
|
||||
workspace.id,
|
||||
path.as_ref(),
|
||||
bp.position,
|
||||
kind,
|
||||
)) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
log::error!("{err}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
match workspace.location {
|
||||
SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => {
|
||||
|
@ -720,6 +933,21 @@ impl WorkspaceDb {
|
|||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
pub fn breakpoints_for_file(workspace_id: WorkspaceId, file_path: &Path) -> Result<Vec<Breakpoint>> {
|
||||
SELECT breakpoint_location
|
||||
FROM breakpoints
|
||||
WHERE workspace_id= ?1 AND path = ?2
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
pub fn clear_breakpoints(file_path: &Path) -> Result<()> {
|
||||
DELETE FROM breakpoints
|
||||
WHERE file_path = ?2
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
fn ssh_projects() -> Result<Vec<SerializedSshProject>> {
|
||||
SELECT id, host, port, paths, user
|
||||
|
@ -1165,6 +1393,70 @@ mod tests {
|
|||
use db::open_test_db;
|
||||
use gpui;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_breakpoints() {
|
||||
env_logger::try_init().ok();
|
||||
|
||||
let db = WorkspaceDb(open_test_db("test_breakpoints").await);
|
||||
let id = db.next_id().await.unwrap();
|
||||
|
||||
let path = Path::new("/tmp/test.rs");
|
||||
|
||||
let breakpoint = Breakpoint {
|
||||
position: 123,
|
||||
kind: BreakpointKind::Standard,
|
||||
};
|
||||
|
||||
let log_breakpoint = Breakpoint {
|
||||
position: 456,
|
||||
kind: BreakpointKind::Log("Test log message".into()),
|
||||
};
|
||||
|
||||
let workspace = SerializedWorkspace {
|
||||
id,
|
||||
location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
|
||||
center_group: Default::default(),
|
||||
window_bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
breakpoints: {
|
||||
let mut map = collections::BTreeMap::default();
|
||||
map.insert(
|
||||
Arc::from(path),
|
||||
vec![
|
||||
SerializedBreakpoint {
|
||||
position: breakpoint.position,
|
||||
path: Arc::from(path),
|
||||
kind: breakpoint.kind.clone(),
|
||||
},
|
||||
SerializedBreakpoint {
|
||||
position: log_breakpoint.position,
|
||||
path: Arc::from(path),
|
||||
kind: log_breakpoint.kind.clone(),
|
||||
},
|
||||
],
|
||||
);
|
||||
map
|
||||
},
|
||||
session_id: None,
|
||||
window_id: None,
|
||||
};
|
||||
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
|
||||
let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
|
||||
let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
|
||||
|
||||
assert_eq!(loaded_breakpoints.len(), 2);
|
||||
assert_eq!(loaded_breakpoints[0].position, breakpoint.position);
|
||||
assert_eq!(loaded_breakpoints[0].kind, breakpoint.kind);
|
||||
assert_eq!(loaded_breakpoints[1].position, log_breakpoint.position);
|
||||
assert_eq!(loaded_breakpoints[1].kind, log_breakpoint.kind);
|
||||
assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
|
||||
assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_next_id_stability() {
|
||||
env_logger::try_init().ok();
|
||||
|
@ -1243,6 +1535,7 @@ mod tests {
|
|||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
breakpoints: Default::default(),
|
||||
session_id: None,
|
||||
window_id: None,
|
||||
};
|
||||
|
@ -1255,6 +1548,7 @@ mod tests {
|
|||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
breakpoints: Default::default(),
|
||||
session_id: None,
|
||||
window_id: None,
|
||||
};
|
||||
|
@ -1359,6 +1653,7 @@ mod tests {
|
|||
),
|
||||
center_group,
|
||||
window_bounds: Default::default(),
|
||||
breakpoints: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
|
@ -1393,6 +1688,7 @@ mod tests {
|
|||
),
|
||||
center_group: Default::default(),
|
||||
window_bounds: Default::default(),
|
||||
breakpoints: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
|
@ -1408,6 +1704,7 @@ mod tests {
|
|||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
breakpoints: Default::default(),
|
||||
session_id: None,
|
||||
window_id: Some(2),
|
||||
};
|
||||
|
@ -1447,6 +1744,7 @@ mod tests {
|
|||
),
|
||||
center_group: Default::default(),
|
||||
window_bounds: Default::default(),
|
||||
breakpoints: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
|
@ -1486,6 +1784,7 @@ mod tests {
|
|||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
breakpoints: Default::default(),
|
||||
session_id: Some("session-id-1".to_owned()),
|
||||
window_id: Some(10),
|
||||
};
|
||||
|
@ -1498,6 +1797,7 @@ mod tests {
|
|||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
breakpoints: Default::default(),
|
||||
session_id: Some("session-id-1".to_owned()),
|
||||
window_id: Some(20),
|
||||
};
|
||||
|
@ -1510,6 +1810,7 @@ mod tests {
|
|||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
breakpoints: Default::default(),
|
||||
session_id: Some("session-id-2".to_owned()),
|
||||
window_id: Some(30),
|
||||
};
|
||||
|
@ -1522,6 +1823,7 @@ mod tests {
|
|||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
breakpoints: Default::default(),
|
||||
session_id: None,
|
||||
window_id: None,
|
||||
};
|
||||
|
@ -1539,6 +1841,7 @@ mod tests {
|
|||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
breakpoints: Default::default(),
|
||||
session_id: Some("session-id-2".to_owned()),
|
||||
window_id: Some(50),
|
||||
};
|
||||
|
@ -1551,6 +1854,7 @@ mod tests {
|
|||
),
|
||||
center_group: Default::default(),
|
||||
window_bounds: Default::default(),
|
||||
breakpoints: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
|
@ -1608,6 +1912,7 @@ mod tests {
|
|||
window_bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
breakpoints: Default::default(),
|
||||
centered_layout: false,
|
||||
session_id: None,
|
||||
window_id: None,
|
||||
|
@ -1655,6 +1960,7 @@ mod tests {
|
|||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
session_id: Some("one-session".to_owned()),
|
||||
breakpoints: Default::default(),
|
||||
window_id: Some(window_id),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
@ -1746,6 +2052,7 @@ mod tests {
|
|||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
session_id: Some("one-session".to_owned()),
|
||||
breakpoints: Default::default(),
|
||||
window_id: Some(window_id),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue