persistence.rs

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