persistence.rs

  1#![allow(dead_code)]
  2
  3pub mod model;
  4
  5use std::path::Path;
  6
  7use anyhow::{anyhow, bail, Context, Result};
  8use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
  9use gpui::Axis;
 10
 11use util::{iife, unzip_option, ResultExt};
 12
 13use crate::dock::DockPosition;
 14use crate::WorkspaceId;
 15
 16use model::{
 17    GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
 18    WorkspaceLocation,
 19};
 20
 21define_connection! {
 22    pub static ref DB: WorkspaceDb<()> =
 23        &[sql!(
 24            CREATE TABLE workspaces(
 25                workspace_id INTEGER PRIMARY KEY,
 26                workspace_location BLOB UNIQUE,
 27                dock_visible INTEGER, // Boolean
 28                dock_anchor TEXT, // Enum: 'Bottom' / 'Right' / 'Expanded'
 29                dock_pane INTEGER, // NULL indicates that we don't have a dock pane yet
 30                left_sidebar_open INTEGER, //Boolean
 31                timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 32                FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
 33            ) STRICT;
 34            
 35            CREATE TABLE pane_groups(
 36                group_id INTEGER PRIMARY KEY,
 37                workspace_id INTEGER NOT NULL,
 38                parent_group_id INTEGER, // NULL indicates that this is a root node
 39                position INTEGER, // NULL indicates that this is a root node
 40                axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
 41                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 42                ON DELETE CASCADE
 43                ON UPDATE CASCADE,
 44                FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 45            ) STRICT;
 46            
 47            CREATE TABLE panes(
 48                pane_id INTEGER PRIMARY KEY,
 49                workspace_id INTEGER NOT NULL,
 50                active INTEGER NOT NULL, // Boolean
 51                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 52                ON DELETE CASCADE
 53                ON UPDATE CASCADE
 54            ) STRICT;
 55            
 56            CREATE TABLE center_panes(
 57                pane_id INTEGER PRIMARY KEY,
 58                parent_group_id INTEGER, // NULL means that this is a root pane
 59                position INTEGER, // NULL means that this is a root pane
 60                FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 61                ON DELETE CASCADE,
 62                FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 63            ) STRICT;
 64            
 65            CREATE TABLE items(
 66                item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
 67                workspace_id INTEGER NOT NULL,
 68                pane_id INTEGER NOT NULL,
 69                kind TEXT NOT NULL,
 70                position INTEGER NOT NULL,
 71                active INTEGER NOT NULL,
 72                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 73                ON DELETE CASCADE
 74                ON UPDATE CASCADE,
 75                FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 76                ON DELETE CASCADE,
 77                PRIMARY KEY(item_id, workspace_id)
 78            ) STRICT;
 79        )];
 80}
 81
 82impl WorkspaceDb {
 83    /// Returns a serialized workspace for the given worktree_roots. If the passed array
 84    /// is empty, the most recent workspace is returned instead. If no workspace for the
 85    /// passed roots is stored, returns none.
 86    pub fn workspace_for_roots<P: AsRef<Path>>(
 87        &self,
 88        worktree_roots: &[P],
 89    ) -> Option<SerializedWorkspace> {
 90        let workspace_location: WorkspaceLocation = worktree_roots.into();
 91
 92        // Note that we re-assign the workspace_id here in case it's empty
 93        // and we've grabbed the most recent workspace
 94        let (workspace_id, workspace_location, left_sidebar_open, dock_position): (
 95            WorkspaceId,
 96            WorkspaceLocation,
 97            bool,
 98            DockPosition,
 99        ) = iife!({
100            if worktree_roots.len() == 0 {
101                self.select_row(sql!(
102                    SELECT workspace_id, workspace_location, left_sidebar_open, dock_visible, dock_anchor
103                    FROM workspaces
104                    ORDER BY timestamp DESC LIMIT 1))?()?
105            } else {
106                self.select_row_bound(sql!(
107                    SELECT workspace_id, workspace_location, left_sidebar_open, dock_visible, dock_anchor
108                    FROM workspaces 
109                    WHERE workspace_location = ?))?(&workspace_location)?
110            }
111            .context("No workspaces found")
112        })
113        .warn_on_err()
114        .flatten()?;
115
116        Some(SerializedWorkspace {
117            id: workspace_id,
118            location: workspace_location.clone(),
119            dock_pane: self
120                .get_dock_pane(workspace_id)
121                .context("Getting dock pane")
122                .log_err()?,
123            center_group: self
124                .get_center_pane_group(workspace_id)
125                .context("Getting center group")
126                .log_err()?,
127            dock_position,
128            left_sidebar_open
129        })
130    }
131
132    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
133    /// that used this workspace previously
134    pub async fn save_workspace(&self, workspace: SerializedWorkspace) {
135        self.write(move |conn| {
136            conn.with_savepoint("update_worktrees", || {
137                // Clear out panes and pane_groups
138                conn.exec_bound(sql!(
139                    UPDATE workspaces SET dock_pane = NULL WHERE workspace_id = ?1;
140                    DELETE FROM pane_groups WHERE workspace_id = ?1;
141                    DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
142                .expect("Clearing old panes");
143
144                conn.exec_bound(sql!(
145                    DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?
146                ))?((&workspace.location, workspace.id.clone()))
147                .context("clearing out old locations")?;
148
149                // Upsert
150                conn.exec_bound(sql!(
151                        INSERT INTO workspaces(
152                            workspace_id,
153                            workspace_location,
154                            left_sidebar_open,
155                            dock_visible,
156                            dock_anchor,
157                            timestamp
158                        )
159                        VALUES (?1, ?2, ?3, ?4, ?5, CURRENT_TIMESTAMP)
160                        ON CONFLICT DO
161                            UPDATE SET
162                            workspace_location = ?2,
163                            left_sidebar_open = ?3,
164                            dock_visible = ?4,
165                            dock_anchor = ?5,
166                            timestamp = CURRENT_TIMESTAMP
167                ))?((workspace.id, &workspace.location, workspace.left_sidebar_open, workspace.dock_position))
168                .context("Updating workspace")?;
169
170                // Save center pane group and dock pane
171                Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
172                    .context("save pane group in save workspace")?;
173
174                let dock_id = Self::save_pane(conn, workspace.id, &workspace.dock_pane, None, true)
175                    .context("save pane in save workspace")?;
176
177                // Complete workspace initialization
178                conn.exec_bound(sql!(
179                    UPDATE workspaces
180                    SET dock_pane = ?
181                    WHERE workspace_id = ?
182                ))?((dock_id, workspace.id))
183                .context("Finishing initialization with dock pane")?;
184
185                Ok(())
186            })
187            .log_err();
188        })
189        .await;
190    }
191
192    query! {
193        pub async fn next_id() -> Result<WorkspaceId> {
194            INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
195        }
196    }
197
198    query! {
199        pub fn recent_workspaces(limit: usize) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
200            SELECT workspace_id, workspace_location 
201            FROM workspaces
202            WHERE workspace_location IS NOT NULL
203            ORDER BY timestamp DESC 
204            LIMIT ?
205        }
206    }
207
208    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
209        self.get_pane_group(workspace_id, None)?
210            .into_iter()
211            .next()
212            .context("No center pane group")
213    }
214
215    fn get_pane_group(
216        &self,
217        workspace_id: WorkspaceId,
218        group_id: Option<GroupId>,
219    ) -> Result<Vec<SerializedPaneGroup>> {
220        type GroupKey = (Option<GroupId>, WorkspaceId);
221        type GroupOrPane = (Option<GroupId>, Option<Axis>, Option<PaneId>, Option<bool>);
222        self.select_bound::<GroupKey, GroupOrPane>(sql!(
223            SELECT group_id, axis, pane_id, active
224                FROM (SELECT 
225                        group_id,
226                        axis,
227                        NULL as pane_id,
228                        NULL as active,
229                        position,
230                        parent_group_id,
231                        workspace_id
232                      FROM pane_groups 
233                     UNION
234                      SELECT 
235                        NULL,
236                        NULL,  
237                        center_panes.pane_id,
238                        panes.active as active,
239                        position,
240                        parent_group_id,
241                        panes.workspace_id as workspace_id
242                      FROM center_panes
243                      JOIN panes ON center_panes.pane_id = panes.pane_id) 
244            WHERE parent_group_id IS ? AND workspace_id = ?
245            ORDER BY position
246        ))?((group_id, workspace_id))?
247        .into_iter()
248        .map(|(group_id, axis, pane_id, active)| {
249            if let Some((group_id, axis)) = group_id.zip(axis) {
250                Ok(SerializedPaneGroup::Group {
251                    axis,
252                    children: self.get_pane_group(workspace_id, Some(group_id))?,
253                })
254            } else if let Some((pane_id, active)) = pane_id.zip(active) {
255                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
256                    self.get_items(pane_id)?,
257                    active,
258                )))
259            } else {
260                bail!("Pane Group Child was neither a pane group or a pane");
261            }
262        })
263        // Filter out panes and pane groups which don't have any children or items
264        .filter(|pane_group| match pane_group {
265            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
266            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
267            _ => true,
268        })
269        .collect::<Result<_>>()
270    }
271
272   
273    fn save_pane_group(
274        conn: &Connection,
275        workspace_id: WorkspaceId,
276        pane_group: &SerializedPaneGroup,
277        parent: Option<(GroupId, usize)>,
278    ) -> Result<()> {
279        match pane_group {
280            SerializedPaneGroup::Group { axis, children } => {
281                let (parent_id, position) = unzip_option(parent);
282
283                let group_id = conn.select_row_bound::<_, i64>(sql!(
284                        INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) 
285                        VALUES (?, ?, ?, ?) 
286                        RETURNING group_id
287                ))?((
288                    workspace_id,
289                    parent_id,
290                    position,
291                    *axis,
292                ))?
293                .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
294
295                for (position, group) in children.iter().enumerate() {
296                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
297                }
298
299                Ok(())
300            }
301            SerializedPaneGroup::Pane(pane) => {
302                Self::save_pane(conn, workspace_id, &pane, parent, false)?;
303                Ok(())
304            }
305        }
306    }
307
308    fn get_dock_pane(&self, workspace_id: WorkspaceId) -> Result<SerializedPane> {
309        let (pane_id, active) = self.select_row_bound(sql!(
310            SELECT pane_id, active
311            FROM panes
312            WHERE pane_id = (SELECT dock_pane FROM workspaces WHERE workspace_id = ?)
313        ))?(
314            workspace_id,
315        )?
316        .context("No dock pane for workspace")?;
317
318        Ok(SerializedPane::new(
319            self.get_items(pane_id).context("Reading items")?,
320            active,
321        ))
322    }
323
324    fn save_pane(
325        conn: &Connection,
326        workspace_id: WorkspaceId,
327        pane: &SerializedPane,
328        parent: Option<(GroupId, usize)>, // None indicates BOTH dock pane AND center_pane
329        dock: bool,
330    ) -> Result<PaneId> {
331        let pane_id = conn.select_row_bound::<_, i64>(sql!(
332            INSERT INTO panes(workspace_id, active) 
333            VALUES (?, ?) 
334            RETURNING pane_id
335        ))?((workspace_id, pane.active))?
336        .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
337
338        if !dock {
339            let (parent_id, order) = unzip_option(parent);
340            conn.exec_bound(sql!(
341                INSERT INTO center_panes(pane_id, parent_group_id, position)
342                VALUES (?, ?, ?)
343            ))?((pane_id, parent_id, order))?;
344        }
345
346        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
347
348        Ok(pane_id)
349    }
350
351    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
352        Ok(self.select_bound(sql!(
353            SELECT kind, item_id, active FROM items
354            WHERE pane_id = ?
355            ORDER BY position
356        ))?(pane_id)?)
357    }
358
359    fn save_items(
360        conn: &Connection,
361        workspace_id: WorkspaceId,
362        pane_id: PaneId,
363        items: &[SerializedItem],
364    ) -> Result<()> {
365        let mut insert = conn.exec_bound(sql!(
366            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?)
367        )).context("Preparing insertion")?;
368        for (position, item) in items.iter().enumerate() {
369            insert((workspace_id, pane_id, position, item))?;
370        }
371
372        Ok(())
373    }
374    
375    query!{
376        fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
377            UPDATE workspaces
378            SET timestamp = CURRENT_TIMESTAMP
379            WHERE workspace_id = ?
380        }
381    }
382    
383}
384
385#[cfg(test)]
386mod tests {
387
388    use std::sync::Arc;
389
390    use db::open_test_db;
391    use settings::DockAnchor;
392
393    use super::*;
394
395    #[gpui::test]
396    async fn test_next_id_stability() {
397        env_logger::try_init().ok();
398
399        let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
400
401        db.write(|conn| {
402            conn.migrate(
403                "test_table",
404                &[sql!(
405                    CREATE TABLE test_table(
406                        text TEXT,
407                        workspace_id INTEGER,
408                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
409                            ON DELETE CASCADE
410                    ) STRICT;
411                )],
412            )
413            .unwrap();
414        })
415        .await;
416
417        let id = db.next_id().await.unwrap();
418        // Assert the empty row got inserted
419        assert_eq!(
420            Some(id),
421            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
422                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
423            ))
424            .unwrap()(id)
425            .unwrap()
426        );
427
428        db.write(move |conn| {
429            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
430                .unwrap()(("test-text-1", id))
431            .unwrap()
432        })
433        .await;
434
435        let test_text_1 = db
436            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
437            .unwrap()(1)
438        .unwrap()
439        .unwrap();
440        assert_eq!(test_text_1, "test-text-1");
441    }
442
443    #[gpui::test]
444    async fn test_workspace_id_stability() {
445        env_logger::try_init().ok();
446
447        let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
448
449        db.write(|conn| {
450            conn.migrate(
451                "test_table",
452                &[sql!(
453                    CREATE TABLE test_table(
454                        text TEXT,
455                        workspace_id INTEGER,
456                        FOREIGN KEY(workspace_id) 
457                            REFERENCES workspaces(workspace_id)
458                            ON DELETE CASCADE
459                    ) STRICT;)],
460            )
461        })
462        .await
463        .unwrap();
464
465        let mut workspace_1 = SerializedWorkspace {
466            id: 1,
467            location: (["/tmp", "/tmp2"]).into(),
468            dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
469            center_group: Default::default(),
470            dock_pane: Default::default(),
471            left_sidebar_open: true
472        };
473
474        let mut workspace_2 = SerializedWorkspace {
475            id: 2,
476            location: (["/tmp"]).into(),
477            dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
478            center_group: Default::default(),
479            dock_pane: Default::default(),
480            left_sidebar_open: false
481        };
482
483        db.save_workspace(workspace_1.clone()).await;
484
485        db.write(|conn| {
486            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
487                .unwrap()(("test-text-1", 1))
488            .unwrap();
489        })
490        .await;
491
492        db.save_workspace(workspace_2.clone()).await;
493
494        db.write(|conn| {
495            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
496                .unwrap()(("test-text-2", 2))
497            .unwrap();
498        })
499        .await;
500
501        workspace_1.location = (["/tmp", "/tmp3"]).into();
502        db.save_workspace(workspace_1.clone()).await;
503        db.save_workspace(workspace_1).await;
504
505        workspace_2.dock_pane.children.push(SerializedItem {
506            kind: Arc::from("Test"),
507            item_id: 10,
508            active: true,
509        });
510        db.save_workspace(workspace_2).await;
511
512        let test_text_2 = db
513            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
514            .unwrap()(2)
515        .unwrap()
516        .unwrap();
517        assert_eq!(test_text_2, "test-text-2");
518
519        let test_text_1 = db
520            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
521            .unwrap()(1)
522        .unwrap()
523        .unwrap();
524        assert_eq!(test_text_1, "test-text-1");
525    }
526
527    #[gpui::test]
528    async fn test_full_workspace_serialization() {
529        env_logger::try_init().ok();
530
531        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
532
533        let dock_pane = crate::persistence::model::SerializedPane {
534            children: vec![
535                SerializedItem::new("Terminal", 1, false),
536                SerializedItem::new("Terminal", 2, false),
537                SerializedItem::new("Terminal", 3, true),
538                SerializedItem::new("Terminal", 4, false),
539            ],
540            active: false,
541        };
542
543        //  -----------------
544        //  | 1,2   | 5,6   |
545        //  | - - - |       |
546        //  | 3,4   |       |
547        //  -----------------
548        let center_group = SerializedPaneGroup::Group {
549            axis: gpui::Axis::Horizontal,
550            children: vec![
551                SerializedPaneGroup::Group {
552                    axis: gpui::Axis::Vertical,
553                    children: vec![
554                        SerializedPaneGroup::Pane(SerializedPane::new(
555                            vec![
556                                SerializedItem::new("Terminal", 5, false),
557                                SerializedItem::new("Terminal", 6, true),
558                            ],
559                            false,
560                        )),
561                        SerializedPaneGroup::Pane(SerializedPane::new(
562                            vec![
563                                SerializedItem::new("Terminal", 7, true),
564                                SerializedItem::new("Terminal", 8, false),
565                            ],
566                            false,
567                        )),
568                    ],
569                },
570                SerializedPaneGroup::Pane(SerializedPane::new(
571                    vec![
572                        SerializedItem::new("Terminal", 9, false),
573                        SerializedItem::new("Terminal", 10, true),
574                    ],
575                    false,
576                )),
577            ],
578        };
579
580        let workspace = SerializedWorkspace {
581            id: 5,
582            location: (["/tmp", "/tmp2"]).into(),
583            dock_position: DockPosition::Shown(DockAnchor::Bottom),
584            center_group,
585            dock_pane,
586            left_sidebar_open: true
587        };
588
589        db.save_workspace(workspace.clone()).await;
590        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
591
592        assert_eq!(workspace, round_trip_workspace.unwrap());
593
594        // Test guaranteed duplicate IDs
595        db.save_workspace(workspace.clone()).await;
596        db.save_workspace(workspace.clone()).await;
597
598        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
599        assert_eq!(workspace, round_trip_workspace.unwrap());
600    }
601
602    #[gpui::test]
603    async fn test_workspace_assignment() {
604        env_logger::try_init().ok();
605
606        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
607
608        let workspace_1 = SerializedWorkspace {
609            id: 1,
610            location: (["/tmp", "/tmp2"]).into(),
611            dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
612            center_group: Default::default(),
613            dock_pane: Default::default(),
614            left_sidebar_open: true,
615        };
616
617        let mut workspace_2 = SerializedWorkspace {
618            id: 2,
619            location: (["/tmp"]).into(),
620            dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
621            center_group: Default::default(),
622            dock_pane: Default::default(),
623            left_sidebar_open: false,
624        };
625
626        db.save_workspace(workspace_1.clone()).await;
627        db.save_workspace(workspace_2.clone()).await;
628
629        // Test that paths are treated as a set
630        assert_eq!(
631            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
632            workspace_1
633        );
634        assert_eq!(
635            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
636            workspace_1
637        );
638
639        // Make sure that other keys work
640        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
641        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
642
643        // Test 'mutate' case of updating a pre-existing id
644        workspace_2.location = (["/tmp", "/tmp2"]).into();
645
646        db.save_workspace(workspace_2.clone()).await;
647        assert_eq!(
648            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
649            workspace_2
650        );
651
652        // Test other mechanism for mutating
653        let mut workspace_3 = SerializedWorkspace {
654            id: 3,
655            location: (&["/tmp", "/tmp2"]).into(),
656            dock_position: DockPosition::Shown(DockAnchor::Right),
657            center_group: Default::default(),
658            dock_pane: Default::default(),
659            left_sidebar_open: false
660        };
661
662        db.save_workspace(workspace_3.clone()).await;
663        assert_eq!(
664            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
665            workspace_3
666        );
667
668        // Make sure that updating paths differently also works
669        workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
670        db.save_workspace(workspace_3.clone()).await;
671        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
672        assert_eq!(
673            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
674                .unwrap(),
675            workspace_3
676        );
677    }
678
679    use crate::dock::DockPosition;
680    use crate::persistence::model::SerializedWorkspace;
681    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
682
683    fn default_workspace<P: AsRef<Path>>(
684        workspace_id: &[P],
685        dock_pane: SerializedPane,
686        center_group: &SerializedPaneGroup,
687    ) -> SerializedWorkspace {
688        SerializedWorkspace {
689            id: 4,
690            location: workspace_id.into(),
691            dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
692            center_group: center_group.clone(),
693            dock_pane,
694            left_sidebar_open: true
695        }
696    }
697
698    #[gpui::test]
699    async fn test_basic_dock_pane() {
700        env_logger::try_init().ok();
701
702        let db = WorkspaceDb(open_test_db("basic_dock_pane").await);
703
704        let dock_pane = crate::persistence::model::SerializedPane::new(
705            vec![
706                SerializedItem::new("Terminal", 1, false),
707                SerializedItem::new("Terminal", 4, false),
708                SerializedItem::new("Terminal", 2, false),
709                SerializedItem::new("Terminal", 3, true),
710            ],
711            false,
712        );
713
714        let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default());
715
716        db.save_workspace(workspace.clone()).await;
717
718        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
719
720        assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
721    }
722
723    #[gpui::test]
724    async fn test_simple_split() {
725        env_logger::try_init().ok();
726
727        let db = WorkspaceDb(open_test_db("simple_split").await);
728
729        //  -----------------
730        //  | 1,2   | 5,6   |
731        //  | - - - |       |
732        //  | 3,4   |       |
733        //  -----------------
734        let center_pane = SerializedPaneGroup::Group {
735            axis: gpui::Axis::Horizontal,
736            children: vec![
737                SerializedPaneGroup::Group {
738                    axis: gpui::Axis::Vertical,
739                    children: vec![
740                        SerializedPaneGroup::Pane(SerializedPane::new(
741                            vec![
742                                SerializedItem::new("Terminal", 1, false),
743                                SerializedItem::new("Terminal", 2, true),
744                            ],
745                            false,
746                        )),
747                        SerializedPaneGroup::Pane(SerializedPane::new(
748                            vec![
749                                SerializedItem::new("Terminal", 4, false),
750                                SerializedItem::new("Terminal", 3, true),
751                            ],
752                            true,
753                        )),
754                    ],
755                },
756                SerializedPaneGroup::Pane(SerializedPane::new(
757                    vec![
758                        SerializedItem::new("Terminal", 5, true),
759                        SerializedItem::new("Terminal", 6, false),
760                    ],
761                    false,
762                )),
763            ],
764        };
765
766        let workspace = default_workspace(&["/tmp"], Default::default(), &center_pane);
767
768        db.save_workspace(workspace.clone()).await;
769
770        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
771
772        assert_eq!(workspace.center_group, new_workspace.center_group);
773    }
774
775    #[gpui::test]
776    async fn test_cleanup_panes() {
777        env_logger::try_init().ok();
778
779        let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
780
781        let center_pane = SerializedPaneGroup::Group {
782            axis: gpui::Axis::Horizontal,
783            children: vec![
784                SerializedPaneGroup::Group {
785                    axis: gpui::Axis::Vertical,
786                    children: vec![
787                        SerializedPaneGroup::Pane(SerializedPane::new(
788                            vec![
789                                SerializedItem::new("Terminal", 1, false),
790                                SerializedItem::new("Terminal", 2, true),
791                            ],
792                            false,
793                        )),
794                        SerializedPaneGroup::Pane(SerializedPane::new(
795                            vec![
796                                SerializedItem::new("Terminal", 4, false),
797                                SerializedItem::new("Terminal", 3, true),
798                            ],
799                            true,
800                        )),
801                    ],
802                },
803                SerializedPaneGroup::Pane(SerializedPane::new(
804                    vec![
805                        SerializedItem::new("Terminal", 5, false),
806                        SerializedItem::new("Terminal", 6, true),
807                    ],
808                    false,
809                )),
810            ],
811        };
812
813        let id = &["/tmp"];
814
815        let mut workspace = default_workspace(id, Default::default(), &center_pane);
816
817        db.save_workspace(workspace.clone()).await;
818
819        workspace.center_group = SerializedPaneGroup::Group {
820            axis: gpui::Axis::Vertical,
821            children: vec![
822                SerializedPaneGroup::Pane(SerializedPane::new(
823                    vec![
824                        SerializedItem::new("Terminal", 1, false),
825                        SerializedItem::new("Terminal", 2, true),
826                    ],
827                    false,
828                )),
829                SerializedPaneGroup::Pane(SerializedPane::new(
830                    vec![
831                        SerializedItem::new("Terminal", 4, true),
832                        SerializedItem::new("Terminal", 3, false),
833                    ],
834                    true,
835                )),
836            ],
837        };
838
839        db.save_workspace(workspace.clone()).await;
840
841        let new_workspace = db.workspace_for_roots(id).unwrap();
842
843        assert_eq!(workspace.center_group, new_workspace.center_group);
844    }
845}