diff --git a/crates/sqlez/src/migrations.rs b/crates/sqlez/src/migrations.rs index 23af04bbf4e30be0af9c05ccfa1f8bcc0e56cf4b..d77d54095bc7b434410eb8058361dd20aaef56a2 100644 --- a/crates/sqlez/src/migrations.rs +++ b/crates/sqlez/src/migrations.rs @@ -58,7 +58,7 @@ impl Connection { mod test { use indoc::indoc; - use crate::connection::Connection; + use crate::{connection::Connection, thread_safe_connection::ThreadSafeConnection}; #[test] fn test_migrations_are_added_to_table() { diff --git a/crates/sqlez/src/statement.rs b/crates/sqlez/src/statement.rs index f0afc0e020d61f0c32516469a6d97f4c40aaa343..164929010b2698401724e7c6493b0212948d709c 100644 --- a/crates/sqlez/src/statement.rs +++ b/crates/sqlez/src/statement.rs @@ -59,11 +59,11 @@ impl<'a> Statement<'a> { ); remaining_sql = CStr::from_ptr(remaining_sql_ptr); statement.raw_statements.push(raw_statement); - } - connection - .last_error() - .with_context(|| format!("Prepare call failed for query:\n{}", query.as_ref()))?; + connection.last_error().with_context(|| { + format!("Prepare call failed for query:\n{}", query.as_ref()) + })?; + } } Ok(statement) diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs index f415c32960cbd81ecbba2f3abc481d211f16f454..4ed180540746ce96290380c632591eccfeae5d8d 100644 --- a/crates/sqlez/src/thread_safe_connection.rs +++ b/crates/sqlez/src/thread_safe_connection.rs @@ -109,3 +109,50 @@ impl Deref for ThreadSafeConnection { }) } } + +#[cfg(test)] +mod test { + use std::ops::Deref; + + use crate::domain::Domain; + + use super::ThreadSafeConnection; + + #[test] + #[should_panic] + fn wild_zed_lost_failure() { + enum TestWorkspace {} + impl Domain for TestWorkspace { + fn name() -> &'static str { + "workspace" + } + + fn migrations() -> &'static [&'static str] { + &[" + CREATE TABLE workspaces( + workspace_id BLOB PRIMARY KEY, + dock_visible INTEGER, -- Boolean + dock_anchor TEXT, -- Enum: 'Bottom' / 'Right' / 'Expanded' + dock_pane INTEGER, -- NULL indicates that we don't have a dock pane yet + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY(dock_pane) REFERENCES panes(pane_id), + FOREIGN KEY(active_pane) REFERENCES panes(pane_id) + ) STRICT; + + CREATE TABLE panes( + pane_id INTEGER PRIMARY KEY, + workspace_id BLOB NOT NULL, + active INTEGER NOT NULL, -- Boolean + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE + ) STRICT; + "] + } + } + + let _ = ThreadSafeConnection::::new(None, false) + .with_initialize_query("PRAGMA FOREIGN_KEYS=true") + .deref(); + } +} diff --git a/crates/workspace/dest.db b/crates/workspace/dest.db new file mode 100644 index 0000000000000000000000000000000000000000..95cbdffc92f30106519872ee986be92fdb20ebfa Binary files /dev/null and b/crates/workspace/dest.db differ diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index f7517ec8bf79c4d4f8a8b6f5df59da22741e1e14..3f04e5046151e7cf9c29624a94dd56268957f34a 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -26,6 +26,7 @@ use model::{ connection!(DB: WorkspaceDb); + impl Domain for Workspace { fn name() -> &'static str { "workspace" @@ -37,7 +38,9 @@ impl Domain for Workspace { workspace_id BLOB PRIMARY KEY, dock_visible INTEGER, -- Boolean dock_anchor TEXT, -- Enum: 'Bottom' / 'Right' / 'Expanded' - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL + dock_pane INTEGER, -- NULL indicates that we don't have a dock pane yet + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) ) STRICT; CREATE TABLE pane_groups( @@ -55,14 +58,21 @@ impl Domain for Workspace { CREATE TABLE panes( pane_id INTEGER PRIMARY KEY, workspace_id BLOB NOT NULL, - parent_group_id INTEGER, -- NULL means that this is a dock pane - position INTEGER, -- NULL means that this is a dock pane + active INTEGER NOT NULL, -- Boolean FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ON UPDATE CASCADE ) STRICT; + CREATE TABLE center_panes( + pane_id INTEGER PRIMARY KEY, + parent_group_id INTEGER, -- NULL means that this is a root pane + position INTEGER, -- NULL means that this is a root pane + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; + CREATE TABLE items( item_id INTEGER NOT NULL, -- This is the item's view id, so this is not unique workspace_id BLOB NOT NULL, @@ -131,12 +141,13 @@ impl WorkspaceDb { workspace: &SerializedWorkspace, ) { self.with_savepoint("update_worktrees", || { + self.exec_bound(indoc! {" + UPDATE workspaces SET dock_pane = NULL WHERE workspace_id = ?1; + DELETE FROM pane_groups WHERE workspace_id = ?1; + DELETE FROM panes WHERE workspace_id = ?1;"})? + (old_id.as_ref().unwrap_or(&workspace.workspace_id)).context("Clearing old panes")?; + if let Some(old_id) = old_id { - self.exec_bound(indoc! {" - DELETE FROM pane_groups WHERE workspace_id = ?"})?(&old_id)?; - - // If collision, delete - self.exec_bound(indoc! {" UPDATE OR REPLACE workspaces SET workspace_id = ?, @@ -147,18 +158,26 @@ impl WorkspaceDb { &workspace.workspace_id, workspace.dock_position, &old_id, - ))?; + )).context("Updating workspace with new worktree roots")?; } else { - self.exec_bound(indoc! {" - DELETE FROM pane_groups WHERE workspace_id = ?"})?(&workspace.workspace_id)?; self.exec_bound( "INSERT OR REPLACE INTO workspaces(workspace_id, dock_visible, dock_anchor) VALUES (?, ?, ?)", - )?((&workspace.workspace_id, workspace.dock_position))?; + )?((&workspace.workspace_id, workspace.dock_position)).context("Uodating workspace")?; } // Save center pane group and dock pane - self.save_pane_group(&workspace.workspace_id, &workspace.center_group, None)?; - self.save_pane(&workspace.workspace_id, &workspace.dock_pane, None)?; + self.save_pane_group(&workspace.workspace_id, &workspace.center_group, None).context("save pane group in save workspace")?; + + let dock_id = self.save_pane(&workspace.workspace_id, &workspace.dock_pane, None, true).context("save pane in save workspace")?; + + // Complete workspace initialization + self.exec_bound(indoc! {" + UPDATE workspaces + SET dock_pane = ? + WHERE workspace_id = ?"})?(( + dock_id, + &workspace.workspace_id, + )).context("Finishing initialization with dock pane")?; Ok(()) }) @@ -196,38 +215,42 @@ impl WorkspaceDb { .into_iter() .next() .context("No center pane group") - .map(|pane_group| { - // Rewrite the special case of the root being a leaf node - if let SerializedPaneGroup::Group { axis: Axis::Horizontal, ref children } = pane_group { - if children.len() == 1 { - if let Some(SerializedPaneGroup::Pane(pane)) = children.get(0) { - return SerializedPaneGroup::Pane(pane.clone()) - } - } - } - pane_group - }) } - fn get_pane_group_children<'a>( + fn get_pane_group_children( &self, workspace_id: &WorkspaceId, group_id: Option, ) -> Result> { - self.select_bound::<(Option, &WorkspaceId), (Option, Option, Option)>(indoc! {" - SELECT group_id, axis, pane_id - FROM (SELECT group_id, axis, NULL as pane_id, position, parent_group_id, workspace_id - FROM pane_groups - UNION - SELECT NULL, NULL, pane_id, position, parent_group_id, workspace_id - FROM panes - -- Remove the dock panes from the union - WHERE parent_group_id IS NOT NULL and position IS NOT NULL) + type GroupKey<'a> = (Option, &'a WorkspaceId); + type GroupOrPane = (Option, Option, Option, Option); + self.select_bound::(indoc! {" + SELECT group_id, axis, pane_id, active + FROM (SELECT + group_id, + axis, + NULL as pane_id, + NULL as active, + position, + parent_group_id, + workspace_id + FROM pane_groups + UNION + SELECT + NULL, + NULL, + center_panes.pane_id, + panes.active as active, + position, + parent_group_id, + panes.workspace_id as workspace_id + FROM center_panes + JOIN panes ON center_panes.pane_id = panes.pane_id) WHERE parent_group_id IS ? AND workspace_id = ? ORDER BY position "})?((group_id, workspace_id))? .into_iter() - .map(|(group_id, axis, pane_id)| { + .map(|(group_id, axis, pane_id, active)| { if let Some((group_id, axis)) = group_id.zip(axis) { Ok(SerializedPaneGroup::Group { axis, @@ -236,10 +259,8 @@ impl WorkspaceDb { Some(group_id), )?, }) - } else if let Some(pane_id) = pane_id { - Ok(SerializedPaneGroup::Pane(SerializedPane { - children: self.get_items( pane_id)?, - })) + } else if let Some((pane_id, active)) = pane_id.zip(active) { + Ok(SerializedPaneGroup::Pane(SerializedPane::new(self.get_items( pane_id)?, active))) } else { bail!("Pane Group Child was neither a pane group or a pane"); } @@ -253,22 +274,15 @@ impl WorkspaceDb { pane_group: &SerializedPaneGroup, parent: Option<(GroupId, usize)>, ) -> Result<()> { - // Rewrite the root node to fit with the database - let pane_group = if parent.is_none() && matches!(pane_group, SerializedPaneGroup::Pane { .. }) { - SerializedPaneGroup::Group { axis: Axis::Horizontal, children: vec![pane_group.clone()] } - } else { - pane_group.clone() - }; - match pane_group { SerializedPaneGroup::Group { axis, children } => { let (parent_id, position) = unzip_option(parent); let group_id = self.select_row_bound::<_, i64>(indoc!{" - INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) - VALUES (?, ?, ?, ?) - RETURNING group_id"})? - ((workspace_id, parent_id, position, axis))? + INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) + VALUES (?, ?, ?, ?) + RETURNING group_id"})? + ((workspace_id, parent_id, position, *axis))? .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?; for (position, group) in children.iter().enumerate() { @@ -277,21 +291,24 @@ impl WorkspaceDb { Ok(()) } SerializedPaneGroup::Pane(pane) => { - self.save_pane(workspace_id, &pane, parent) + self.save_pane(workspace_id, &pane, parent, false)?; + Ok(()) }, } } pub(crate) fn get_dock_pane(&self, workspace_id: &WorkspaceId) -> Result { - let pane_id = self.select_row_bound(indoc! {" - SELECT pane_id FROM panes - WHERE workspace_id = ? AND parent_group_id IS NULL AND position IS NULL"})?( + let (pane_id, active) = self.select_row_bound(indoc! {" + SELECT pane_id, active + FROM panes + WHERE pane_id = (SELECT dock_pane FROM workspaces WHERE workspace_id = ?)"})?( workspace_id, )? .context("No dock pane for workspace")?; Ok(SerializedPane::new( self.get_items(pane_id).context("Reading items")?, + active )) } @@ -299,20 +316,32 @@ impl WorkspaceDb { &self, workspace_id: &WorkspaceId, pane: &SerializedPane, - parent: Option<(GroupId, usize)>, - ) -> Result<()> { - let (parent_id, order) = unzip_option(parent); - + parent: Option<(GroupId, usize)>, // None indicates BOTH dock pane AND center_pane + dock: bool, + ) -> Result { let pane_id = self.select_row_bound::<_, i64>(indoc!{" - INSERT INTO panes(workspace_id, parent_group_id, position) - VALUES (?, ?, ?) + INSERT INTO panes(workspace_id, active) + VALUES (?, ?) RETURNING pane_id"}, - )?((workspace_id, parent_id, order))? + )?((workspace_id, pane.active))? .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?; + + if !dock { + let (parent_id, order) = unzip_option(parent); + self.exec_bound(indoc! {" + INSERT INTO center_panes(pane_id, parent_group_id, position) + VALUES (?, ?, ?)"})?(( + pane_id, parent_id, order + ))?; + } self.save_items(workspace_id, pane_id, &pane.children) - .context("Saving items") + .context("Saving items")?; + + Ok(pane_id) } + + pub(crate) fn get_items(&self, pane_id: PaneId) -> Result> { Ok(self.select_bound(indoc! {" @@ -352,6 +381,7 @@ mod tests { let db = WorkspaceDb(open_memory_db(Some("test_full_workspace_serialization"))); let dock_pane = crate::persistence::model::SerializedPane { + children: vec![ SerializedItem::new("Terminal", 1), SerializedItem::new("Terminal", 2), @@ -359,6 +389,7 @@ mod tests { SerializedItem::new("Terminal", 4), ], + active: false }; // ----------------- @@ -372,28 +403,30 @@ mod tests { SerializedPaneGroup::Group { axis: gpui::Axis::Vertical, children: vec![ - SerializedPaneGroup::Pane(SerializedPane { - children: vec![ + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ SerializedItem::new("Terminal", 5), SerializedItem::new("Terminal", 6), ], - }), - SerializedPaneGroup::Pane(SerializedPane { - children: vec![ + false) + ), + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ SerializedItem::new("Terminal", 7), SerializedItem::new("Terminal", 8), - ], - }), + false, + )), ], }, - SerializedPaneGroup::Pane(SerializedPane { - children: vec![ + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ SerializedItem::new("Terminal", 9), SerializedItem::new("Terminal", 10), ], - }), + false, + )), ], }; @@ -518,14 +551,14 @@ mod tests { let db = WorkspaceDb(open_memory_db(Some("basic_dock_pane"))); - let dock_pane = crate::persistence::model::SerializedPane { - children: vec![ + let dock_pane = crate::persistence::model::SerializedPane::new( + vec![ SerializedItem::new("Terminal", 1), SerializedItem::new("Terminal", 4), SerializedItem::new("Terminal", 2), SerializedItem::new("Terminal", 3), - ], - }; + ], false + ); let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default()); @@ -538,7 +571,7 @@ mod tests { #[test] fn test_simple_split() { - // env_logger::try_init().ok(); + env_logger::try_init().ok(); let db = WorkspaceDb(open_memory_db(Some("simple_split"))); @@ -553,33 +586,96 @@ mod tests { SerializedPaneGroup::Group { axis: gpui::Axis::Vertical, children: vec![ - SerializedPaneGroup::Pane(SerializedPane { - children: vec![ - SerializedItem::new("Terminal", 1), - SerializedItem::new("Terminal", 2), - ], - }), - SerializedPaneGroup::Pane(SerializedPane { - children: vec![ - SerializedItem::new("Terminal", 4), - SerializedItem::new("Terminal", 3), - ], - }), + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 1), + SerializedItem::new("Terminal", 2), + ], + false)), + SerializedPaneGroup::Pane(SerializedPane::new(vec![ + SerializedItem::new("Terminal", 4), + SerializedItem::new("Terminal", 3), + ], true)), ], }, - SerializedPaneGroup::Pane(SerializedPane { - children: vec![ + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ SerializedItem::new("Terminal", 5), SerializedItem::new("Terminal", 6), ], - }), + false)), ], }; let workspace = default_workspace(&["/tmp"], Default::default(), ¢er_pane); db.save_workspace(None, &workspace); + + let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap(); + + assert_eq!(workspace.center_group, new_workspace.center_group); + } + + #[test] + fn test_cleanup_panes() { + env_logger::try_init().ok(); + + let db = WorkspaceDb(open_memory_db(Some("test_cleanup_panes"))); + + let center_pane = SerializedPaneGroup::Group { + axis: gpui::Axis::Horizontal, + children: vec![ + SerializedPaneGroup::Group { + axis: gpui::Axis::Vertical, + children: vec![ + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 1), + SerializedItem::new("Terminal", 2), + ], + false)), + SerializedPaneGroup::Pane(SerializedPane::new(vec![ + SerializedItem::new("Terminal", 4), + SerializedItem::new("Terminal", 3), + ], true)), + ], + }, + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 5), + SerializedItem::new("Terminal", 6), + ], + false)), + ], + }; + + let id = &["/tmp"]; + + let mut workspace = default_workspace(id, Default::default(), ¢er_pane); + + db.save_workspace(None, &workspace); + + workspace.center_group = SerializedPaneGroup::Group { + axis: gpui::Axis::Vertical, + children: vec![ + SerializedPaneGroup::Pane(SerializedPane::new( + vec![ + SerializedItem::new("Terminal", 1), + SerializedItem::new("Terminal", 2), + ], + false)), + SerializedPaneGroup::Pane(SerializedPane::new(vec![ + SerializedItem::new("Terminal", 4), + SerializedItem::new("Terminal", 3), + ], true)), + ], + }; + + db.save_workspace(None, &workspace); + + let new_workspace = db.workspace_for_roots(id).unwrap(); + + assert_eq!(workspace.center_group, new_workspace.center_group); - assert_eq!(workspace.center_group, center_pane); } } diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index b4b5db5f11462874d0e7b19fd28820d24b144fb7..9eca121c213a0e9b081449356fcec460ce65d966 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -76,6 +76,7 @@ impl Default for SerializedPaneGroup { fn default() -> Self { Self::Pane(SerializedPane { children: Vec::new(), + active: false, }) } } @@ -88,27 +89,35 @@ impl SerializedPaneGroup { workspace_id: &WorkspaceId, workspace: &ViewHandle, cx: &mut AsyncAppContext, - ) -> Member { + ) -> (Member, Option>) { match self { SerializedPaneGroup::Group { axis, children } => { + let mut current_active_pane = None; let mut members = Vec::new(); for child in children { - let new_member = child + let (new_member, active_pane) = child .deserialize(project, workspace_id, workspace, cx) .await; members.push(new_member); + + current_active_pane = current_active_pane.or(active_pane); } - Member::Axis(PaneAxis { - axis: *axis, - members, - }) + ( + Member::Axis(PaneAxis { + axis: *axis, + members, + }), + current_active_pane, + ) } SerializedPaneGroup::Pane(serialized_pane) => { let pane = workspace.update(cx, |workspace, cx| workspace.add_pane(cx)); + let active = serialized_pane.active; serialized_pane .deserialize_to(project, &pane, workspace_id, workspace, cx) .await; - Member::Pane(pane) + + (Member::Pane(pane.clone()), active.then(|| pane)) } } } @@ -116,12 +125,13 @@ impl SerializedPaneGroup { #[derive(Debug, PartialEq, Eq, Default, Clone)] pub struct SerializedPane { + pub(crate) active: bool, pub(crate) children: Vec, } impl SerializedPane { - pub fn new(children: Vec) -> Self { - SerializedPane { children } + pub fn new(children: Vec, active: bool) -> Self { + SerializedPane { children, active } } pub async fn deserialize_to( @@ -154,6 +164,7 @@ impl SerializedPane { }) .await .log_err(); + if let Some(item_handle) = item_handle { workspace.update(cx, |workspace, cx| { Pane::add_item(workspace, &pane_handle, item_handle, false, false, None, cx); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 072bd80e1dfde190bede15e7adf7baec00d3f903..fbe21be81cd8af42526b69e5b5a59ca9e85d16a2 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2280,18 +2280,22 @@ impl Workspace { pane_handle: &ViewHandle, cx: &AppContext, ) -> SerializedPane { - SerializedPane { - children: pane_handle - .read(cx) - .items() - .filter_map(|item_handle| { - Some(SerializedItem { - kind: Arc::from(item_handle.serialized_item_kind()?), - item_id: item_handle.id(), + let (items, active) = { + let pane = pane_handle.read(cx); + ( + pane.items() + .filter_map(|item_handle| { + Some(SerializedItem { + kind: Arc::from(item_handle.serialized_item_kind()?), + item_id: item_handle.id(), + }) }) - }) - .collect::>(), - } + .collect::>(), + pane.is_active(), + ) + }; + + SerializedPane::new(items, active) } let dock_pane = serialize_pane_handle(self.dock.pane(), cx); @@ -2353,7 +2357,7 @@ impl Workspace { // Traverse the splits tree and add to things - let root = serialized_workspace + let (root, active_pane) = serialized_workspace .center_group .deserialize( &project, @@ -2369,11 +2373,14 @@ impl Workspace { // Swap workspace center group workspace.center = PaneGroup::with_root(root); - cx.notify(); - }); - workspace.update(&mut cx, |workspace, cx| { - Dock::set_dock_position(workspace, serialized_workspace.dock_position, cx) + Dock::set_dock_position(workspace, serialized_workspace.dock_position, cx); + + if let Some(active_pane) = active_pane { + cx.focus(active_pane); + } + + cx.notify(); }); } })