Finished the bulk of workspace serialization. Just items and wiring it all through.

Mikayla Maki and kay@zed.dev created

Co-Authored-By: kay@zed.dev

Change summary

crates/db/src/workspace.rs       |  10 
crates/db/src/workspace/items.rs |  60 ----------
crates/db/src/workspace/model.rs |  64 +++++++++--
crates/db/src/workspace/pane.rs  | 174 ++++++++++++++++++++++++---------
crates/db/test.db                |   0 
5 files changed, 187 insertions(+), 121 deletions(-)

Detailed changes

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");
 

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! {"

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<SerializedPaneGroup>,
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
+pub enum Axis {
+    #[default]
+    Horizontal,
+    Vertical,
+}
+
+impl Bind for Axis {
+    fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
+        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,
+            ))
+        })
+    }
 }
 
-impl SerializedPaneGroup {
-    pub fn new() -> Self {
-        SerializedPaneGroup {
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum SerializedPaneGroup {
+    Group {
+        axis: Axis,
+        children: Vec<SerializedPaneGroup>,
+    },
+    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<SerializedItem>,
 }
@@ -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<Path> },
     Diagnostics { item_id: usize },

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<SerializedPaneGroup> {
-        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<GroupId>,
+    ) -> Result<Vec<SerializedPaneGroup>> {
+        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<GroupId>, Option<Axis>, Option<PaneId>)>()?;
+
+        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::<Result<_>>()
+    }
+
+    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<SerializedPane> {
         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::<PaneId>()?;
 
@@ -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");
-
-    //     let workspace = db.workspace_for_roots(&["/tmp"]);
-
-    //     // 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 }],
-    //         })],
-    //     };
+    #[test]
+    fn test_simple_split() {
+        env_logger::try_init().ok();
+
+        let db = Db::open_in_memory("simple_split");
+
+        //  -----------------
+        //  | 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, &center_pane);
+        let workspace = default_workspace(Default::default(), &center_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);
+    }
 }