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                active INTEGER NOT NULL,
 80                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 81                    ON DELETE CASCADE
 82                    ON UPDATE CASCADE,
 83                FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 84                    ON DELETE CASCADE,
 85                PRIMARY KEY(item_id, workspace_id)
 86            ) STRICT;
 87        )]
 88    }
 89}
 90
 91impl WorkspaceDb {
 92    /// Returns a serialized workspace for the given worktree_roots. If the passed array
 93    /// is empty, the most recent workspace is returned instead. If no workspace for the
 94    /// passed roots is stored, returns none.
 95    pub fn workspace_for_roots<P: AsRef<Path>>(
 96        &self,
 97        worktree_roots: &[P],
 98    ) -> Option<SerializedWorkspace> {
 99        let workspace_location: WorkspaceLocation = worktree_roots.into();
100
101        // Note that we re-assign the workspace_id here in case it's empty
102        // and we've grabbed the most recent workspace
103        let (workspace_id, workspace_location, dock_position): (
104            WorkspaceId,
105            WorkspaceLocation,
106            DockPosition,
107        ) = iife!({
108            if worktree_roots.len() == 0 {
109                self.select_row(sql!(
110                    SELECT workspace_id, workspace_location, dock_visible, dock_anchor
111                    FROM workspaces
112                    ORDER BY timestamp DESC LIMIT 1))?()?
113            } else {
114                self.select_row_bound(sql!(
115                    SELECT workspace_id, workspace_location, dock_visible, dock_anchor
116                    FROM workspaces 
117                    WHERE workspace_location = ?))?(&workspace_location)?
118            }
119            .context("No workspaces found")
120        })
121        .warn_on_err()
122        .flatten()?;
123
124        Some(SerializedWorkspace {
125            id: workspace_id,
126            location: workspace_location.clone(),
127            dock_pane: self
128                .get_dock_pane(workspace_id)
129                .context("Getting dock pane")
130                .log_err()?,
131            center_group: self
132                .get_center_pane_group(workspace_id)
133                .context("Getting center group")
134                .log_err()?,
135            dock_position,
136        })
137    }
138
139    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
140    /// that used this workspace previously
141    pub async fn save_workspace(&self, workspace: SerializedWorkspace) {
142        self.write(move |conn| {
143            conn.with_savepoint("update_worktrees", || {
144                // Clear out panes and pane_groups
145                conn.exec_bound(sql!(
146                    UPDATE workspaces SET dock_pane = NULL WHERE workspace_id = ?1;
147                    DELETE FROM pane_groups WHERE workspace_id = ?1;
148                    DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
149                .context("Clearing old panes")?;
150
151                conn.exec_bound(sql!(
152                    DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?
153                ))?((&workspace.location, workspace.id.clone()))
154                .context("clearing out old locations")?;
155
156                // Upsert
157                conn.exec_bound(sql!(
158                        INSERT INTO workspaces(
159                            workspace_id,
160                            workspace_location,
161                            dock_visible,
162                            dock_anchor,
163                            timestamp
164                        )
165                        VALUES (?1, ?2, ?3, ?4, CURRENT_TIMESTAMP)
166                        ON CONFLICT DO
167                            UPDATE SET
168                            workspace_location = ?2,
169                            dock_visible = ?3,
170                            dock_anchor = ?4,
171                            timestamp = CURRENT_TIMESTAMP
172                ))?((workspace.id, &workspace.location, workspace.dock_position))
173                .context("Updating workspace")?;
174
175                // Save center pane group and dock pane
176                Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
177                    .context("save pane group in save workspace")?;
178
179                let dock_id = Self::save_pane(conn, workspace.id, &workspace.dock_pane, None, true)
180                    .context("save pane in save workspace")?;
181
182                // Complete workspace initialization
183                conn.exec_bound(sql!(
184                    UPDATE workspaces
185                    SET dock_pane = ?
186                    WHERE workspace_id = ?
187                ))?((dock_id, workspace.id))
188                .context("Finishing initialization with dock pane")?;
189
190                Ok(())
191            })
192            .log_err();
193        })
194        .await;
195    }
196
197    query! {
198        pub async fn next_id() -> Result<WorkspaceId> {
199            INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
200        }
201    }
202
203    query! {
204        pub fn recent_workspaces(limit: usize) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
205            SELECT workspace_id, workspace_location 
206            FROM workspaces 
207            ORDER BY timestamp DESC 
208            LIMIT ?
209        }
210    }
211
212    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
213        self.get_pane_group(workspace_id, None)?
214            .into_iter()
215            .next()
216            .context("No center pane group")
217    }
218
219    fn get_pane_group(
220        &self,
221        workspace_id: WorkspaceId,
222        group_id: Option<GroupId>,
223    ) -> Result<Vec<SerializedPaneGroup>> {
224        type GroupKey = (Option<GroupId>, WorkspaceId);
225        type GroupOrPane = (Option<GroupId>, Option<Axis>, Option<PaneId>, Option<bool>);
226        self.select_bound::<GroupKey, GroupOrPane>(sql!(
227            SELECT group_id, axis, pane_id, active
228                FROM (SELECT 
229                        group_id,
230                        axis,
231                        NULL as pane_id,
232                        NULL as active,
233                        position,
234                        parent_group_id,
235                        workspace_id
236                      FROM pane_groups 
237                     UNION
238                      SELECT 
239                        NULL,
240                        NULL,  
241                        center_panes.pane_id,
242                        panes.active as active,
243                        position,
244                        parent_group_id,
245                        panes.workspace_id as workspace_id
246                      FROM center_panes
247                      JOIN panes ON center_panes.pane_id = panes.pane_id) 
248            WHERE parent_group_id IS ? AND workspace_id = ?
249            ORDER BY position
250        ))?((group_id, workspace_id))?
251        .into_iter()
252        .map(|(group_id, axis, pane_id, active)| {
253            if let Some((group_id, axis)) = group_id.zip(axis) {
254                Ok(SerializedPaneGroup::Group {
255                    axis,
256                    children: self.get_pane_group(workspace_id, Some(group_id))?,
257                })
258            } else if let Some((pane_id, active)) = pane_id.zip(active) {
259                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
260                    self.get_items(pane_id)?,
261                    active,
262                )))
263            } else {
264                bail!("Pane Group Child was neither a pane group or a pane");
265            }
266        })
267        // Filter out panes and pane groups which don't have any children or items
268        .filter(|pane_group| match pane_group {
269            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
270            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
271            _ => true,
272        })
273        .collect::<Result<_>>()
274    }
275
276    fn save_pane_group(
277        conn: &Connection,
278        workspace_id: WorkspaceId,
279        pane_group: &SerializedPaneGroup,
280        parent: Option<(GroupId, usize)>,
281    ) -> Result<()> {
282        match pane_group {
283            SerializedPaneGroup::Group { axis, children } => {
284                let (parent_id, position) = unzip_option(parent);
285
286                let group_id = conn.select_row_bound::<_, i64>(sql!(
287                        INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) 
288                        VALUES (?, ?, ?, ?) 
289                        RETURNING group_id
290                ))?((
291                    workspace_id,
292                    parent_id,
293                    position,
294                    *axis,
295                ))?
296                .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
297
298                for (position, group) in children.iter().enumerate() {
299                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
300                }
301
302                Ok(())
303            }
304            SerializedPaneGroup::Pane(pane) => {
305                Self::save_pane(conn, workspace_id, &pane, parent, false)?;
306                Ok(())
307            }
308        }
309    }
310
311    fn get_dock_pane(&self, workspace_id: WorkspaceId) -> Result<SerializedPane> {
312        let (pane_id, active) = self.select_row_bound(sql!(
313            SELECT pane_id, active
314            FROM panes
315            WHERE pane_id = (SELECT dock_pane FROM workspaces WHERE workspace_id = ?)
316        ))?(
317            workspace_id,
318        )?
319        .context("No dock pane for workspace")?;
320
321        Ok(SerializedPane::new(
322            self.get_items(pane_id).context("Reading items")?,
323            active,
324        ))
325    }
326
327    fn save_pane(
328        conn: &Connection,
329        workspace_id: WorkspaceId,
330        pane: &SerializedPane,
331        parent: Option<(GroupId, usize)>, // None indicates BOTH dock pane AND center_pane
332        dock: bool,
333    ) -> Result<PaneId> {
334        let pane_id = conn.select_row_bound::<_, i64>(sql!(
335            INSERT INTO panes(workspace_id, active) 
336            VALUES (?, ?) 
337            RETURNING pane_id
338        ))?((workspace_id, pane.active))?
339        .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
340
341        if !dock {
342            let (parent_id, order) = unzip_option(parent);
343            conn.exec_bound(sql!(
344                INSERT INTO center_panes(pane_id, parent_group_id, position)
345                VALUES (?, ?, ?)
346            ))?((pane_id, parent_id, order))?;
347        }
348
349        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
350
351        Ok(pane_id)
352    }
353
354    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
355        Ok(self.select_bound(sql!(
356            SELECT kind, item_id, active FROM items
357            WHERE pane_id = ?
358            ORDER BY position
359        ))?(pane_id)?)
360    }
361
362    fn save_items(
363        conn: &Connection,
364        workspace_id: WorkspaceId,
365        pane_id: PaneId,
366        items: &[SerializedItem],
367    ) -> Result<()> {
368        let mut insert = conn.exec_bound(sql!(
369            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?)
370        )).context("Preparing insertion")?;
371        for (position, item) in items.iter().enumerate() {
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            active: true,
501        });
502        db.save_workspace(workspace_2).await;
503
504        let test_text_2 = db
505            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
506            .unwrap()(2)
507        .unwrap()
508        .unwrap();
509        assert_eq!(test_text_2, "test-text-2");
510
511        let test_text_1 = db
512            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
513            .unwrap()(1)
514        .unwrap()
515        .unwrap();
516        assert_eq!(test_text_1, "test-text-1");
517    }
518
519    #[gpui::test]
520    async fn test_full_workspace_serialization() {
521        env_logger::try_init().ok();
522
523        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
524
525        let dock_pane = crate::persistence::model::SerializedPane {
526            children: vec![
527                SerializedItem::new("Terminal", 1, false),
528                SerializedItem::new("Terminal", 2, false),
529                SerializedItem::new("Terminal", 3, true),
530                SerializedItem::new("Terminal", 4, false),
531            ],
532            active: false,
533        };
534
535        //  -----------------
536        //  | 1,2   | 5,6   |
537        //  | - - - |       |
538        //  | 3,4   |       |
539        //  -----------------
540        let center_group = SerializedPaneGroup::Group {
541            axis: gpui::Axis::Horizontal,
542            children: vec![
543                SerializedPaneGroup::Group {
544                    axis: gpui::Axis::Vertical,
545                    children: vec![
546                        SerializedPaneGroup::Pane(SerializedPane::new(
547                            vec![
548                                SerializedItem::new("Terminal", 5, false),
549                                SerializedItem::new("Terminal", 6, true),
550                            ],
551                            false,
552                        )),
553                        SerializedPaneGroup::Pane(SerializedPane::new(
554                            vec![
555                                SerializedItem::new("Terminal", 7, true),
556                                SerializedItem::new("Terminal", 8, false),
557                            ],
558                            false,
559                        )),
560                    ],
561                },
562                SerializedPaneGroup::Pane(SerializedPane::new(
563                    vec![
564                        SerializedItem::new("Terminal", 9, false),
565                        SerializedItem::new("Terminal", 10, true),
566                    ],
567                    false,
568                )),
569            ],
570        };
571
572        let workspace = SerializedWorkspace {
573            id: 5,
574            location: (["/tmp", "/tmp2"]).into(),
575            dock_position: DockPosition::Shown(DockAnchor::Bottom),
576            center_group,
577            dock_pane,
578        };
579
580        db.save_workspace(workspace.clone()).await;
581        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
582
583        assert_eq!(workspace, round_trip_workspace.unwrap());
584
585        // Test guaranteed duplicate IDs
586        db.save_workspace(workspace.clone()).await;
587        db.save_workspace(workspace.clone()).await;
588
589        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
590        assert_eq!(workspace, round_trip_workspace.unwrap());
591    }
592
593    #[gpui::test]
594    async fn test_workspace_assignment() {
595        env_logger::try_init().ok();
596
597        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
598
599        let workspace_1 = SerializedWorkspace {
600            id: 1,
601            location: (["/tmp", "/tmp2"]).into(),
602            dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
603            center_group: Default::default(),
604            dock_pane: Default::default(),
605        };
606
607        let mut workspace_2 = SerializedWorkspace {
608            id: 2,
609            location: (["/tmp"]).into(),
610            dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
611            center_group: Default::default(),
612            dock_pane: Default::default(),
613        };
614
615        db.save_workspace(workspace_1.clone()).await;
616        db.save_workspace(workspace_2.clone()).await;
617
618        // Test that paths are treated as a set
619        assert_eq!(
620            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
621            workspace_1
622        );
623        assert_eq!(
624            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
625            workspace_1
626        );
627
628        // Make sure that other keys work
629        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
630        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
631
632        // Test 'mutate' case of updating a pre-existing id
633        workspace_2.location = (["/tmp", "/tmp2"]).into();
634
635        db.save_workspace(workspace_2.clone()).await;
636        assert_eq!(
637            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
638            workspace_2
639        );
640
641        // Test other mechanism for mutating
642        let mut workspace_3 = SerializedWorkspace {
643            id: 3,
644            location: (&["/tmp", "/tmp2"]).into(),
645            dock_position: DockPosition::Shown(DockAnchor::Right),
646            center_group: Default::default(),
647            dock_pane: Default::default(),
648        };
649
650        db.save_workspace(workspace_3.clone()).await;
651        assert_eq!(
652            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
653            workspace_3
654        );
655
656        // Make sure that updating paths differently also works
657        workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
658        db.save_workspace(workspace_3.clone()).await;
659        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
660        assert_eq!(
661            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
662                .unwrap(),
663            workspace_3
664        );
665    }
666
667    use crate::dock::DockPosition;
668    use crate::persistence::model::SerializedWorkspace;
669    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
670
671    fn default_workspace<P: AsRef<Path>>(
672        workspace_id: &[P],
673        dock_pane: SerializedPane,
674        center_group: &SerializedPaneGroup,
675    ) -> SerializedWorkspace {
676        SerializedWorkspace {
677            id: 4,
678            location: workspace_id.into(),
679            dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
680            center_group: center_group.clone(),
681            dock_pane,
682        }
683    }
684
685    #[gpui::test]
686    async fn test_basic_dock_pane() {
687        env_logger::try_init().ok();
688
689        let db = WorkspaceDb(open_test_db("basic_dock_pane").await);
690
691        let dock_pane = crate::persistence::model::SerializedPane::new(
692            vec![
693                SerializedItem::new("Terminal", 1, false),
694                SerializedItem::new("Terminal", 4, false),
695                SerializedItem::new("Terminal", 2, false),
696                SerializedItem::new("Terminal", 3, true),
697            ],
698            false,
699        );
700
701        let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default());
702
703        db.save_workspace(workspace.clone()).await;
704
705        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
706
707        assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
708    }
709
710    #[gpui::test]
711    async fn test_simple_split() {
712        env_logger::try_init().ok();
713
714        let db = WorkspaceDb(open_test_db("simple_split").await);
715
716        //  -----------------
717        //  | 1,2   | 5,6   |
718        //  | - - - |       |
719        //  | 3,4   |       |
720        //  -----------------
721        let center_pane = SerializedPaneGroup::Group {
722            axis: gpui::Axis::Horizontal,
723            children: vec![
724                SerializedPaneGroup::Group {
725                    axis: gpui::Axis::Vertical,
726                    children: vec![
727                        SerializedPaneGroup::Pane(SerializedPane::new(
728                            vec![
729                                SerializedItem::new("Terminal", 1, false),
730                                SerializedItem::new("Terminal", 2, true),
731                            ],
732                            false,
733                        )),
734                        SerializedPaneGroup::Pane(SerializedPane::new(
735                            vec![
736                                SerializedItem::new("Terminal", 4, false),
737                                SerializedItem::new("Terminal", 3, true),
738                            ],
739                            true,
740                        )),
741                    ],
742                },
743                SerializedPaneGroup::Pane(SerializedPane::new(
744                    vec![
745                        SerializedItem::new("Terminal", 5, true),
746                        SerializedItem::new("Terminal", 6, false),
747                    ],
748                    false,
749                )),
750            ],
751        };
752
753        let workspace = default_workspace(&["/tmp"], Default::default(), &center_pane);
754
755        db.save_workspace(workspace.clone()).await;
756
757        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
758
759        assert_eq!(workspace.center_group, new_workspace.center_group);
760    }
761
762    #[gpui::test]
763    async fn test_cleanup_panes() {
764        env_logger::try_init().ok();
765
766        let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
767
768        let center_pane = SerializedPaneGroup::Group {
769            axis: gpui::Axis::Horizontal,
770            children: vec![
771                SerializedPaneGroup::Group {
772                    axis: gpui::Axis::Vertical,
773                    children: vec![
774                        SerializedPaneGroup::Pane(SerializedPane::new(
775                            vec![
776                                SerializedItem::new("Terminal", 1, false),
777                                SerializedItem::new("Terminal", 2, true),
778                            ],
779                            false,
780                        )),
781                        SerializedPaneGroup::Pane(SerializedPane::new(
782                            vec![
783                                SerializedItem::new("Terminal", 4, false),
784                                SerializedItem::new("Terminal", 3, true),
785                            ],
786                            true,
787                        )),
788                    ],
789                },
790                SerializedPaneGroup::Pane(SerializedPane::new(
791                    vec![
792                        SerializedItem::new("Terminal", 5, false),
793                        SerializedItem::new("Terminal", 6, true),
794                    ],
795                    false,
796                )),
797            ],
798        };
799
800        let id = &["/tmp"];
801
802        let mut workspace = default_workspace(id, Default::default(), &center_pane);
803
804        db.save_workspace(workspace.clone()).await;
805
806        workspace.center_group = SerializedPaneGroup::Group {
807            axis: gpui::Axis::Vertical,
808            children: vec![
809                SerializedPaneGroup::Pane(SerializedPane::new(
810                    vec![
811                        SerializedItem::new("Terminal", 1, false),
812                        SerializedItem::new("Terminal", 2, true),
813                    ],
814                    false,
815                )),
816                SerializedPaneGroup::Pane(SerializedPane::new(
817                    vec![
818                        SerializedItem::new("Terminal", 4, true),
819                        SerializedItem::new("Terminal", 3, false),
820                    ],
821                    true,
822                )),
823            ],
824        };
825
826        db.save_workspace(workspace.clone()).await;
827
828        let new_workspace = db.workspace_for_roots(id).unwrap();
829
830        assert_eq!(workspace.center_group, new_workspace.center_group);
831    }
832}