diff --git a/crates/db/src/workspace.rs b/crates/db/src/workspace.rs index b1d139066f..9b2d9e4563 100644 --- a/crates/db/src/workspace.rs +++ b/crates/db/src/workspace.rs @@ -63,7 +63,7 @@ impl Db { .context("Getting dock pane") .log_err()?, center_group: self - .get_center_group(&workspace_id) + .get_center_pane_group(&workspace_id) .context("Getting center group") .log_err()?, dock_anchor, @@ -104,8 +104,8 @@ impl Db { .exec()?; // Save center pane group and dock pane - self.save_center_group(&workspace_id, &workspace.center_group)?; - self.save_dock_pane(&workspace_id, &workspace.dock_pane)?; + self.save_pane_group(&workspace_id, &workspace.center_group, None)?; + self.save_pane(&workspace_id, &workspace.dock_pane, None)?; Ok(()) }) @@ -152,8 +152,8 @@ mod tests { }; #[test] - fn test_basic_functionality() { - env_logger::init(); + fn test_workspace_assignment() { + env_logger::try_init().ok(); let db = Db::open_in_memory("test_basic_functionality"); diff --git a/crates/db/src/workspace/items.rs b/crates/db/src/workspace/items.rs index 87437ccf73..25873a7f9b 100644 --- a/crates/db/src/workspace/items.rs +++ b/crates/db/src/workspace/items.rs @@ -6,63 +6,11 @@ use crate::{ model::{ItemId, PaneId, SerializedItem, SerializedItemKind, WorkspaceId}, Db, }; -// use collections::HashSet; -// use rusqlite::{named_params, params, types::FromSql}; - -// use crate::workspace::WorkspaceId; - -// use super::Db; - -// /// Current design makes the cut at the item level, -// /// - Maybe A little more bottom up, serialize 'Terminals' and 'Editors' directly, and then make a seperate -// /// - items table, with a kind, and an integer that acts as a key to one of these other tables -// /// This column is a foreign key to ONE OF: editors, terminals, searches -// /// - - -// // (workspace_id, item_id) -// // kind -> ::Editor:: - -// // -> -// // At the workspace level -// // -> (Workspace_ID, item_id) -// // -> One shot, big query, load everything up: - -// // -> SerializedWorkspace::deserialize(tx, itemKey) -// // -> SerializedEditor::deserialize(tx, itemKey) - -// // -> -// // -> Workspace::new(SerializedWorkspace) -// // -> Editor::new(serialized_workspace[???]serializedEditor) - -// // //Pros: Keeps sql out of every body elese, makes changing it easier (e.g. for loading from a network or RocksDB) -// // //Cons: DB has to know the internals of the entire rest of the app - -// // Workspace -// // Worktree roots -// // Pane groups -// // Dock -// // Items -// // Sidebars - -// // Things I'm doing: finding about nullability for foreign keys -// pub(crate) const ITEMS_M_1: &str = " -// CREATE TABLE project_searches( -// workspace_id INTEGER, -// item_id INTEGER, -// query TEXT, -// PRIMARY KEY (workspace_id, item_id) -// FOREIGN KEY(workspace_id) REFERENCES workspace_ids(workspace_id) -// ) STRICT; - -// CREATE TABLE editors( -// workspace_id INTEGER, -// item_id INTEGER, -// path BLOB NOT NULL, -// PRIMARY KEY (workspace_id, item_id) -// FOREIGN KEY(workspace_id) REFERENCES workspace_ids(workspace_id) -// ) STRICT; -// "; +// 1) Move all of this into Workspace crate +// 2) Deserialize items fully +// 3) Typed prepares (including how you expect to pull data out) +// 4) Investigate Tree column impls pub(crate) const ITEM_MIGRATIONS: Migration = Migration::new( "item", &[indoc! {" diff --git a/crates/db/src/workspace/model.rs b/crates/db/src/workspace/model.rs index a2bb0c1cd2..1d9065f6d9 100644 --- a/crates/db/src/workspace/model.rs +++ b/crates/db/src/workspace/model.rs @@ -5,7 +5,6 @@ use std::{ use anyhow::{bail, Result}; -use gpui::Axis; use sqlez::{ bindable::{Bind, Column}, statement::Statement, @@ -91,22 +90,61 @@ pub struct SerializedWorkspace { pub dock_pane: SerializedPane, } -#[derive(Debug, PartialEq, Eq, Default)] -pub struct SerializedPaneGroup { - axis: Axis, - children: Vec, +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum Axis { + #[default] + Horizontal, + Vertical, } -impl SerializedPaneGroup { - pub fn new() -> Self { - SerializedPaneGroup { +impl Bind for Axis { + fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { + match self { + Axis::Horizontal => "Horizontal", + Axis::Vertical => "Vertical", + } + .bind(statement, start_index) + } +} + +impl Column for Axis { + fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { + String::column(statement, start_index).and_then(|(axis_text, next_index)| { + Ok(( + match axis_text.as_str() { + "Horizontal" => Axis::Horizontal, + "Vertical" => Axis::Vertical, + _ => bail!("Stored serialized item kind is incorrect"), + }, + next_index, + )) + }) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum SerializedPaneGroup { + Group { + axis: Axis, + children: Vec, + }, + Pane(SerializedPane), +} + +// Dock panes, and grouped panes combined? +// AND we're collapsing PaneGroup::Pane +// In the case where + +impl Default for SerializedPaneGroup { + fn default() -> Self { + Self::Group { axis: Axis::Horizontal, - children: Vec::new(), + children: vec![Self::Pane(Default::default())], } } } -#[derive(Debug, PartialEq, Eq, Default)] +#[derive(Debug, PartialEq, Eq, Default, Clone)] pub struct SerializedPane { pub(crate) children: Vec, } @@ -142,9 +180,9 @@ impl Bind for SerializedItemKind { impl Column for SerializedItemKind { fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { - String::column(statement, start_index).and_then(|(anchor_text, next_index)| { + String::column(statement, start_index).and_then(|(kind_text, next_index)| { Ok(( - match anchor_text.as_ref() { + match kind_text.as_ref() { "Editor" => SerializedItemKind::Editor, "Diagnostics" => SerializedItemKind::Diagnostics, "ProjectSearch" => SerializedItemKind::ProjectSearch, @@ -157,7 +195,7 @@ impl Column for SerializedItemKind { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum SerializedItem { Editor { item_id: usize, path: Arc }, Diagnostics { item_id: usize }, diff --git a/crates/db/src/workspace/pane.rs b/crates/db/src/workspace/pane.rs index f2b7fc8ef0..7fef2d6b75 100644 --- a/crates/db/src/workspace/pane.rs +++ b/crates/db/src/workspace/pane.rs @@ -1,9 +1,9 @@ -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use indoc::indoc; use sqlez::migrations::Migration; use util::unzip_option; -use crate::model::{GroupId, PaneId, SerializedPane}; +use crate::model::{Axis, GroupId, PaneId, SerializedPane}; use super::{ model::{SerializedPaneGroup, WorkspaceId}, @@ -16,47 +16,107 @@ pub(crate) const PANE_MIGRATIONS: Migration = Migration::new( CREATE TABLE pane_groups( group_id INTEGER PRIMARY KEY, workspace_id BLOB NOT NULL, - parent_group INTEGER, -- NULL indicates that this is a root node + parent_group_id INTEGER, -- NULL indicates that this is a root node + position INTEGER, -- NULL indicates that this is a root node axis TEXT NOT NULL, -- Enum: 'Vertical' / 'Horizontal' FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE, - FOREIGN KEY(parent_group) REFERENCES pane_groups(group_id) ON DELETE CASCADE + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE ) STRICT; CREATE TABLE panes( pane_id INTEGER PRIMARY KEY, workspace_id BLOB NOT NULL, - group_id INTEGER, -- If null, this is a dock pane - position INTEGER, -- If null, this is a dock pane + parent_group_id INTEGER, -- NULL, this is a dock pane + position INTEGER, -- NULL, this is a dock pane FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE, - FOREIGN KEY(group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE ) STRICT; "}], ); impl Db { - pub(crate) fn get_center_group( + pub(crate) fn get_center_pane_group( &self, - _workspace_id: &WorkspaceId, + workspace_id: &WorkspaceId, ) -> Result { - Ok(SerializedPaneGroup::new()) + self.get_pane_group_children(workspace_id, None)? + .into_iter() + .next() + .context("No center pane group") } - pub(crate) fn save_center_group( + fn get_pane_group_children( &self, - _workspace_id: &WorkspaceId, - _center_pane_group: &SerializedPaneGroup, + workspace_id: &WorkspaceId, + group_id: Option, + ) -> Result> { + let children = self + .prepare(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) + WHERE parent_group_id IS ? AND workspace_id = ? + ORDER BY position + "})? + .with_bindings((group_id, workspace_id))? + .rows::<(Option, Option, Option)>()?; + + children + .into_iter() + .map(|(group_id, axis, pane_id)| { + if let Some((group_id, axis)) = group_id.zip(axis) { + Ok(SerializedPaneGroup::Group { + axis, + children: self.get_pane_group_children(workspace_id, Some(group_id))?, + }) + } else if let Some(pane_id) = pane_id { + Ok(SerializedPaneGroup::Pane(SerializedPane { + children: self.get_items(pane_id)?, + })) + } else { + bail!("Pane Group Child was neither a pane group or a pane"); + } + }) + .collect::>() + } + + pub(crate) fn save_pane_group( + &self, + workspace_id: &WorkspaceId, + pane_group: &SerializedPaneGroup, + parent: Option<(GroupId, usize)>, ) -> Result<()> { - // Delete the center pane group for this workspace and any of its children - // Generate new pane group IDs as we go through - // insert them - Ok(()) + if parent.is_none() && !matches!(pane_group, SerializedPaneGroup::Group { .. }) { + bail!("Pane groups must have a SerializedPaneGroup::Group at the root") + } + + let (parent_id, position) = unzip_option(parent); + + match pane_group { + SerializedPaneGroup::Group { axis, children } => { + let parent_id = self.prepare("INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) VALUES (?, ?, ?, ?)")? + .with_bindings((workspace_id, parent_id, position, *axis))? + .insert()? as GroupId; + + for (position, group) in children.iter().enumerate() { + self.save_pane_group(workspace_id, group, Some((parent_id, position)))? + } + Ok(()) + } + SerializedPaneGroup::Pane(pane) => self.save_pane(workspace_id, pane, parent), + } } pub(crate) fn get_dock_pane(&self, workspace_id: &WorkspaceId) -> Result { let pane_id = self .prepare(indoc! {" SELECT pane_id FROM panes - WHERE workspace_id = ? AND group_id IS NULL AND position IS NULL"})? + WHERE workspace_id = ? AND parent_group_id IS NULL AND position IS NULL"})? .with_bindings(workspace_id)? .row::()?; @@ -65,14 +125,6 @@ impl Db { )) } - pub(crate) fn save_dock_pane( - &self, - workspace: &WorkspaceId, - dock_pane: &SerializedPane, - ) -> Result<()> { - self.save_pane(workspace, &dock_pane, None) - } - pub(crate) fn save_pane( &self, workspace_id: &WorkspaceId, @@ -82,7 +134,7 @@ impl Db { let (parent_id, order) = unzip_option(parent); let pane_id = self - .prepare("INSERT INTO panes(workspace_id, group_id, position) VALUES (?, ?, ?)")? + .prepare("INSERT INTO panes(workspace_id, parent_group_id, position) VALUES (?, ?, ?)")? .with_bindings((workspace_id, parent_id, order))? .insert()? as PaneId; @@ -101,18 +153,20 @@ mod tests { fn default_workspace( dock_pane: SerializedPane, - center_group: SerializedPaneGroup, + center_group: &SerializedPaneGroup, ) -> SerializedWorkspace { SerializedWorkspace { dock_anchor: crate::model::DockAnchor::Right, dock_visible: false, - center_group, + center_group: center_group.clone(), dock_pane, } } #[test] fn test_basic_dock_pane() { + env_logger::try_init().ok(); + let db = Db::open_in_memory("basic_dock_pane"); let dock_pane = crate::model::SerializedPane { @@ -124,7 +178,7 @@ mod tests { ], }; - let workspace = default_workspace(dock_pane, SerializedPaneGroup::new()); + let workspace = default_workspace(dock_pane, &Default::default()); db.save_workspace(&["/tmp"], None, &workspace); @@ -133,24 +187,50 @@ mod tests { assert_eq!(workspace.dock_pane, new_workspace.dock_pane); } - // #[test] - // fn test_dock_simple_split() { - // let db = Db::open_in_memory("simple_split"); + #[test] + fn test_simple_split() { + env_logger::try_init().ok(); - // let workspace = db.workspace_for_roots(&["/tmp"]); + let db = Db::open_in_memory("simple_split"); - // // Pane group -> Pane -> 10 , 20 - // let center_pane = SerializedPaneGroup { - // axis: gpui::Axis::Horizontal, - // children: vec![PaneGroupChild::Pane(SerializedPane { - // items: vec![ItemId { item_id: 10 }, ItemId { item_id: 20 }], - // })], - // }; + // ----------------- + // | 1,2 | 5,6 | + // | - - - | | + // | 3,4 | | + // ----------------- + let center_pane = SerializedPaneGroup::Group { + axis: crate::model::Axis::Horizontal, + children: vec![ + SerializedPaneGroup::Group { + axis: crate::model::Axis::Vertical, + children: vec![ + SerializedPaneGroup::Pane(SerializedPane { + children: vec![ + SerializedItem::Terminal { item_id: 1 }, + SerializedItem::Terminal { item_id: 2 }, + ], + }), + SerializedPaneGroup::Pane(SerializedPane { + children: vec![ + SerializedItem::Terminal { item_id: 4 }, + SerializedItem::Terminal { item_id: 3 }, + ], + }), + ], + }, + SerializedPaneGroup::Pane(SerializedPane { + children: vec![ + SerializedItem::Terminal { item_id: 5 }, + SerializedItem::Terminal { item_id: 6 }, + ], + }), + ], + }; - // db.save_pane_splits(&workspace.workspace_id, ¢er_pane); + let workspace = default_workspace(Default::default(), ¢er_pane); - // // let new_workspace = db.workspace_for_roots(&["/tmp"]); + db.save_workspace(&["/tmp"], None, &workspace); - // // assert_eq!(new_workspace.center_group, center_pane); - // } + assert_eq!(workspace.center_group, center_pane); + } } diff --git a/crates/db/test.db b/crates/db/test.db new file mode 100644 index 0000000000..09a0bc8f11 Binary files /dev/null and b/crates/db/test.db differ