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:
Remco Smits 2025-03-18 17:55:25 +01:00 committed by GitHub
parent ed4e654fdf
commit 41a60ffecf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
156 changed files with 25840 additions and 451 deletions

View file

@ -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<_>>();