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
|
@ -219,7 +219,7 @@ pub enum Event {
|
|||
idx: usize,
|
||||
},
|
||||
RemovedItem {
|
||||
item_id: EntityId,
|
||||
item: Box<dyn ItemHandle>,
|
||||
},
|
||||
Split(SplitDirection),
|
||||
JoinAll,
|
||||
|
@ -247,9 +247,9 @@ impl fmt::Debug for Event {
|
|||
.finish(),
|
||||
Event::Remove { .. } => f.write_str("Remove"),
|
||||
Event::RemoveItem { idx } => f.debug_struct("RemoveItem").field("idx", idx).finish(),
|
||||
Event::RemovedItem { item_id } => f
|
||||
Event::RemovedItem { item } => f
|
||||
.debug_struct("RemovedItem")
|
||||
.field("item_id", item_id)
|
||||
.field("item", &item.item_id())
|
||||
.finish(),
|
||||
Event::Split(direction) => f
|
||||
.debug_struct("Split")
|
||||
|
@ -315,6 +315,7 @@ pub struct Pane {
|
|||
display_nav_history_buttons: Option<bool>,
|
||||
double_click_dispatch_action: Box<dyn Action>,
|
||||
save_modals_spawned: HashSet<EntityId>,
|
||||
close_pane_if_empty: bool,
|
||||
pub new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
pub split_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
pinned_tab_count: usize,
|
||||
|
@ -519,6 +520,7 @@ impl Pane {
|
|||
_subscriptions: subscriptions,
|
||||
double_click_dispatch_action,
|
||||
save_modals_spawned: HashSet::default(),
|
||||
close_pane_if_empty: true,
|
||||
split_item_context_menu_handle: Default::default(),
|
||||
new_item_context_menu_handle: Default::default(),
|
||||
pinned_tab_count: 0,
|
||||
|
@ -706,6 +708,11 @@ impl Pane {
|
|||
self.can_split_predicate = can_split_predicate;
|
||||
}
|
||||
|
||||
pub fn set_close_pane_if_empty(&mut self, close_pane_if_empty: bool, cx: &mut Context<Self>) {
|
||||
self.close_pane_if_empty = close_pane_if_empty;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut Context<Self>) {
|
||||
self.toolbar.update(cx, |toolbar, cx| {
|
||||
toolbar.set_can_navigate(can_navigate, cx);
|
||||
|
@ -1632,6 +1639,13 @@ impl Pane {
|
|||
|
||||
// Remove the item from the pane.
|
||||
pane.update_in(&mut cx, |pane, window, cx| {
|
||||
pane.remove_item(
|
||||
item_to_close.item_id(),
|
||||
false,
|
||||
pane.close_pane_if_empty,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
pane.remove_item(item_to_close.item_id(), false, true, window, cx);
|
||||
})
|
||||
.ok();
|
||||
|
@ -1739,13 +1753,9 @@ impl Pane {
|
|||
}
|
||||
}
|
||||
|
||||
cx.emit(Event::RemoveItem { idx: item_index });
|
||||
|
||||
let item = self.items.remove(item_index);
|
||||
|
||||
cx.emit(Event::RemovedItem {
|
||||
item_id: item.item_id(),
|
||||
});
|
||||
cx.emit(Event::RemovedItem { item: item.clone() });
|
||||
if self.items.is_empty() {
|
||||
item.deactivated(window, cx);
|
||||
if close_pane_if_empty {
|
||||
|
@ -2779,7 +2789,7 @@ impl Pane {
|
|||
window.dispatch_action(
|
||||
this.double_click_dispatch_action.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
);
|
||||
}
|
||||
})),
|
||||
),
|
||||
|
|
|
@ -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<_>>();
|
||||
|
|
|
@ -10,10 +10,11 @@ use db::sqlez::{
|
|||
};
|
||||
use gpui::{AsyncWindowContext, Entity, WeakEntity};
|
||||
use itertools::Itertools as _;
|
||||
use project::Project;
|
||||
use project::{debugger::breakpoint_store::SerializedBreakpoint, Project};
|
||||
use remote::ssh_session::SshProjectId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
@ -263,6 +264,7 @@ pub(crate) struct SerializedWorkspace {
|
|||
pub(crate) display: Option<Uuid>,
|
||||
pub(crate) docks: DockStructure,
|
||||
pub(crate) session_id: Option<String>,
|
||||
pub(crate) breakpoints: BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>>,
|
||||
pub(crate) window_id: Option<u64>,
|
||||
}
|
||||
|
||||
|
|
|
@ -127,6 +127,23 @@ static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
|
|||
|
||||
actions!(assistant, [ShowConfiguration]);
|
||||
|
||||
actions!(
|
||||
debugger,
|
||||
[
|
||||
Start,
|
||||
Continue,
|
||||
Disconnect,
|
||||
Pause,
|
||||
Restart,
|
||||
StepInto,
|
||||
StepOver,
|
||||
StepOut,
|
||||
StepBack,
|
||||
Stop,
|
||||
ToggleIgnoreBreakpoints
|
||||
]
|
||||
);
|
||||
|
||||
actions!(
|
||||
workspace,
|
||||
[
|
||||
|
@ -155,6 +172,7 @@ actions!(
|
|||
ReloadActiveItem,
|
||||
SaveAs,
|
||||
SaveWithoutFormat,
|
||||
ShutdownDebugAdapters,
|
||||
ToggleBottomDock,
|
||||
ToggleCenteredLayout,
|
||||
ToggleLeftDock,
|
||||
|
@ -1211,6 +1229,7 @@ impl Workspace {
|
|||
// Get project paths for all of the abs_paths
|
||||
let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
|
||||
Vec::with_capacity(paths_to_open.len());
|
||||
|
||||
for path in paths_to_open.into_iter() {
|
||||
if let Some((_, project_entry)) = cx
|
||||
.update(|cx| {
|
||||
|
@ -3409,9 +3428,10 @@ impl Workspace {
|
|||
serialize_workspace = false;
|
||||
}
|
||||
pane::Event::RemoveItem { .. } => {}
|
||||
pane::Event::RemovedItem { item_id } => {
|
||||
pane::Event::RemovedItem { item } => {
|
||||
cx.emit(Event::ActiveItemChanged);
|
||||
if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
|
||||
self.update_window_edited(window, cx);
|
||||
if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id()) {
|
||||
if entry.get().entity_id() == pane.entity_id() {
|
||||
entry.remove();
|
||||
}
|
||||
|
@ -4644,6 +4664,10 @@ impl Workspace {
|
|||
};
|
||||
|
||||
if let Some(location) = location {
|
||||
let breakpoints = self.project.update(cx, |project, cx| {
|
||||
project.breakpoint_store().read(cx).all_breakpoints(cx)
|
||||
});
|
||||
|
||||
let center_group = build_serialized_pane_group(&self.center.root, window, cx);
|
||||
let docks = build_serialized_docks(self, window, cx);
|
||||
let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
|
||||
|
@ -4656,6 +4680,7 @@ impl Workspace {
|
|||
docks,
|
||||
centered_layout: self.centered_layout,
|
||||
session_id: self.session_id.clone(),
|
||||
breakpoints,
|
||||
window_id: Some(window.window_handle().window_id().as_u64()),
|
||||
};
|
||||
return window.spawn(cx, |_| persistence::DB.save_workspace(serialized_workspace));
|
||||
|
@ -4796,6 +4821,17 @@ impl Workspace {
|
|||
cx.notify();
|
||||
})?;
|
||||
|
||||
let _ = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project
|
||||
.breakpoint_store()
|
||||
.update(cx, |breakpoint_store, cx| {
|
||||
breakpoint_store
|
||||
.with_serialized_breakpoints(serialized_workspace.breakpoints, cx)
|
||||
})
|
||||
})?
|
||||
.await;
|
||||
|
||||
// Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
|
||||
// after loading the items, we might have different items and in order to avoid
|
||||
// the database filling up, we delete items that haven't been loaded now.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue