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                project_panel_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, project_panel_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, project_panel_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, project_panel_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            project_panel_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                            project_panel_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                            project_panel_open = ?3,
164                            dock_visible = ?4,
165                            dock_anchor = ?5,
166                            timestamp = CURRENT_TIMESTAMP
167                ))?((workspace.id, &workspace.location, workspace.project_panel_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            ORDER BY timestamp DESC 
203            LIMIT ?
204        }
205    }
206
207    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
208        self.get_pane_group(workspace_id, None)?
209            .into_iter()
210            .next()
211            .context("No center pane group")
212    }
213
214    fn get_pane_group(
215        &self,
216        workspace_id: WorkspaceId,
217        group_id: Option<GroupId>,
218    ) -> Result<Vec<SerializedPaneGroup>> {
219        type GroupKey = (Option<GroupId>, WorkspaceId);
220        type GroupOrPane = (Option<GroupId>, Option<Axis>, Option<PaneId>, Option<bool>);
221        self.select_bound::<GroupKey, GroupOrPane>(sql!(
222            SELECT group_id, axis, pane_id, active
223                FROM (SELECT 
224                        group_id,
225                        axis,
226                        NULL as pane_id,
227                        NULL as active,
228                        position,
229                        parent_group_id,
230                        workspace_id
231                      FROM pane_groups 
232                     UNION
233                      SELECT 
234                        NULL,
235                        NULL,  
236                        center_panes.pane_id,
237                        panes.active as active,
238                        position,
239                        parent_group_id,
240                        panes.workspace_id as workspace_id
241                      FROM center_panes
242                      JOIN panes ON center_panes.pane_id = panes.pane_id) 
243            WHERE parent_group_id IS ? AND workspace_id = ?
244            ORDER BY position
245        ))?((group_id, workspace_id))?
246        .into_iter()
247        .map(|(group_id, axis, pane_id, active)| {
248            if let Some((group_id, axis)) = group_id.zip(axis) {
249                Ok(SerializedPaneGroup::Group {
250                    axis,
251                    children: self.get_pane_group(workspace_id, Some(group_id))?,
252                })
253            } else if let Some((pane_id, active)) = pane_id.zip(active) {
254                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
255                    self.get_items(pane_id)?,
256                    active,
257                )))
258            } else {
259                bail!("Pane Group Child was neither a pane group or a pane");
260            }
261        })
262        // Filter out panes and pane groups which don't have any children or items
263        .filter(|pane_group| match pane_group {
264            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
265            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
266            _ => true,
267        })
268        .collect::<Result<_>>()
269    }
270
271   
272    fn save_pane_group(
273        conn: &Connection,
274        workspace_id: WorkspaceId,
275        pane_group: &SerializedPaneGroup,
276        parent: Option<(GroupId, usize)>,
277    ) -> Result<()> {
278        match pane_group {
279            SerializedPaneGroup::Group { axis, children } => {
280                let (parent_id, position) = unzip_option(parent);
281
282                let group_id = conn.select_row_bound::<_, i64>(sql!(
283                        INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) 
284                        VALUES (?, ?, ?, ?) 
285                        RETURNING group_id
286                ))?((
287                    workspace_id,
288                    parent_id,
289                    position,
290                    *axis,
291                ))?
292                .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
293
294                for (position, group) in children.iter().enumerate() {
295                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
296                }
297
298                Ok(())
299            }
300            SerializedPaneGroup::Pane(pane) => {
301                Self::save_pane(conn, workspace_id, &pane, parent, false)?;
302                Ok(())
303            }
304        }
305    }
306
307    fn get_dock_pane(&self, workspace_id: WorkspaceId) -> Result<SerializedPane> {
308        let (pane_id, active) = self.select_row_bound(sql!(
309            SELECT pane_id, active
310            FROM panes
311            WHERE pane_id = (SELECT dock_pane FROM workspaces WHERE workspace_id = ?)
312        ))?(
313            workspace_id,
314        )?
315        .context("No dock pane for workspace")?;
316
317        Ok(SerializedPane::new(
318            self.get_items(pane_id).context("Reading items")?,
319            active,
320        ))
321    }
322
323    fn save_pane(
324        conn: &Connection,
325        workspace_id: WorkspaceId,
326        pane: &SerializedPane,
327        parent: Option<(GroupId, usize)>, // None indicates BOTH dock pane AND center_pane
328        dock: bool,
329    ) -> Result<PaneId> {
330        let pane_id = conn.select_row_bound::<_, i64>(sql!(
331            INSERT INTO panes(workspace_id, active) 
332            VALUES (?, ?) 
333            RETURNING pane_id
334        ))?((workspace_id, pane.active))?
335        .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
336
337        if !dock {
338            let (parent_id, order) = unzip_option(parent);
339            conn.exec_bound(sql!(
340                INSERT INTO center_panes(pane_id, parent_group_id, position)
341                VALUES (?, ?, ?)
342            ))?((pane_id, parent_id, order))?;
343        }
344
345        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
346
347        Ok(pane_id)
348    }
349
350    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
351        Ok(self.select_bound(sql!(
352            SELECT kind, item_id, active FROM items
353            WHERE pane_id = ?
354            ORDER BY position
355        ))?(pane_id)?)
356    }
357
358    fn save_items(
359        conn: &Connection,
360        workspace_id: WorkspaceId,
361        pane_id: PaneId,
362        items: &[SerializedItem],
363    ) -> Result<()> {
364        let mut insert = conn.exec_bound(sql!(
365            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?)
366        )).context("Preparing insertion")?;
367        for (position, item) in items.iter().enumerate() {
368            insert((workspace_id, pane_id, position, item))?;
369        }
370
371        Ok(())
372    }
373}
374
375#[cfg(test)]
376mod tests {
377
378    use std::sync::Arc;
379
380    use db::open_test_db;
381    use settings::DockAnchor;
382
383    use super::*;
384
385    #[gpui::test]
386    async fn test_next_id_stability() {
387        env_logger::try_init().ok();
388
389        let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
390
391        db.write(|conn| {
392            conn.migrate(
393                "test_table",
394                &[sql!(
395                    CREATE TABLE test_table(
396                        text TEXT,
397                        workspace_id INTEGER,
398                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
399                            ON DELETE CASCADE
400                    ) STRICT;
401                )],
402            )
403            .unwrap();
404        })
405        .await;
406
407        let id = db.next_id().await.unwrap();
408        // Assert the empty row got inserted
409        assert_eq!(
410            Some(id),
411            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
412                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
413            ))
414            .unwrap()(id)
415            .unwrap()
416        );
417
418        db.write(move |conn| {
419            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
420                .unwrap()(("test-text-1", id))
421            .unwrap()
422        })
423        .await;
424
425        let test_text_1 = db
426            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
427            .unwrap()(1)
428        .unwrap()
429        .unwrap();
430        assert_eq!(test_text_1, "test-text-1");
431    }
432
433    #[gpui::test]
434    async fn test_workspace_id_stability() {
435        env_logger::try_init().ok();
436
437        let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
438
439        db.write(|conn| {
440            conn.migrate(
441                "test_table",
442                &[sql!(
443                    CREATE TABLE test_table(
444                        text TEXT,
445                        workspace_id INTEGER,
446                        FOREIGN KEY(workspace_id) 
447                            REFERENCES workspaces(workspace_id)
448                            ON DELETE CASCADE
449                    ) STRICT;)],
450            )
451        })
452        .await
453        .unwrap();
454
455        let mut workspace_1 = SerializedWorkspace {
456            id: 1,
457            location: (["/tmp", "/tmp2"]).into(),
458            dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
459            center_group: Default::default(),
460            dock_pane: Default::default(),
461            project_panel_open: true
462        };
463
464        let mut workspace_2 = SerializedWorkspace {
465            id: 2,
466            location: (["/tmp"]).into(),
467            dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
468            center_group: Default::default(),
469            dock_pane: Default::default(),
470            project_panel_open: false
471        };
472
473        db.save_workspace(workspace_1.clone()).await;
474
475        db.write(|conn| {
476            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
477                .unwrap()(("test-text-1", 1))
478            .unwrap();
479        })
480        .await;
481
482        db.save_workspace(workspace_2.clone()).await;
483
484        db.write(|conn| {
485            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
486                .unwrap()(("test-text-2", 2))
487            .unwrap();
488        })
489        .await;
490
491        workspace_1.location = (["/tmp", "/tmp3"]).into();
492        db.save_workspace(workspace_1.clone()).await;
493        db.save_workspace(workspace_1).await;
494
495        workspace_2.dock_pane.children.push(SerializedItem {
496            kind: Arc::from("Test"),
497            item_id: 10,
498            active: true,
499        });
500        db.save_workspace(workspace_2).await;
501
502        let test_text_2 = db
503            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
504            .unwrap()(2)
505        .unwrap()
506        .unwrap();
507        assert_eq!(test_text_2, "test-text-2");
508
509        let test_text_1 = db
510            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
511            .unwrap()(1)
512        .unwrap()
513        .unwrap();
514        assert_eq!(test_text_1, "test-text-1");
515    }
516
517    #[gpui::test]
518    async fn test_full_workspace_serialization() {
519        env_logger::try_init().ok();
520
521        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
522
523        let dock_pane = crate::persistence::model::SerializedPane {
524            children: vec![
525                SerializedItem::new("Terminal", 1, false),
526                SerializedItem::new("Terminal", 2, false),
527                SerializedItem::new("Terminal", 3, true),
528                SerializedItem::new("Terminal", 4, false),
529            ],
530            active: false,
531        };
532
533        //  -----------------
534        //  | 1,2   | 5,6   |
535        //  | - - - |       |
536        //  | 3,4   |       |
537        //  -----------------
538        let center_group = SerializedPaneGroup::Group {
539            axis: gpui::Axis::Horizontal,
540            children: vec![
541                SerializedPaneGroup::Group {
542                    axis: gpui::Axis::Vertical,
543                    children: vec![
544                        SerializedPaneGroup::Pane(SerializedPane::new(
545                            vec![
546                                SerializedItem::new("Terminal", 5, false),
547                                SerializedItem::new("Terminal", 6, true),
548                            ],
549                            false,
550                        )),
551                        SerializedPaneGroup::Pane(SerializedPane::new(
552                            vec![
553                                SerializedItem::new("Terminal", 7, true),
554                                SerializedItem::new("Terminal", 8, false),
555                            ],
556                            false,
557                        )),
558                    ],
559                },
560                SerializedPaneGroup::Pane(SerializedPane::new(
561                    vec![
562                        SerializedItem::new("Terminal", 9, false),
563                        SerializedItem::new("Terminal", 10, true),
564                    ],
565                    false,
566                )),
567            ],
568        };
569
570        let workspace = SerializedWorkspace {
571            id: 5,
572            location: (["/tmp", "/tmp2"]).into(),
573            dock_position: DockPosition::Shown(DockAnchor::Bottom),
574            center_group,
575            dock_pane,
576            project_panel_open: true
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            project_panel_open: true,
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            project_panel_open: false,
614        };
615
616        db.save_workspace(workspace_1.clone()).await;
617        db.save_workspace(workspace_2.clone()).await;
618
619        // Test that paths are treated as a set
620        assert_eq!(
621            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
622            workspace_1
623        );
624        assert_eq!(
625            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
626            workspace_1
627        );
628
629        // Make sure that other keys work
630        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
631        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
632
633        // Test 'mutate' case of updating a pre-existing id
634        workspace_2.location = (["/tmp", "/tmp2"]).into();
635
636        db.save_workspace(workspace_2.clone()).await;
637        assert_eq!(
638            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
639            workspace_2
640        );
641
642        // Test other mechanism for mutating
643        let mut workspace_3 = SerializedWorkspace {
644            id: 3,
645            location: (&["/tmp", "/tmp2"]).into(),
646            dock_position: DockPosition::Shown(DockAnchor::Right),
647            center_group: Default::default(),
648            dock_pane: Default::default(),
649            project_panel_open: false
650        };
651
652        db.save_workspace(workspace_3.clone()).await;
653        assert_eq!(
654            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
655            workspace_3
656        );
657
658        // Make sure that updating paths differently also works
659        workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
660        db.save_workspace(workspace_3.clone()).await;
661        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
662        assert_eq!(
663            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
664                .unwrap(),
665            workspace_3
666        );
667    }
668
669    use crate::dock::DockPosition;
670    use crate::persistence::model::SerializedWorkspace;
671    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
672
673    fn default_workspace<P: AsRef<Path>>(
674        workspace_id: &[P],
675        dock_pane: SerializedPane,
676        center_group: &SerializedPaneGroup,
677    ) -> SerializedWorkspace {
678        SerializedWorkspace {
679            id: 4,
680            location: workspace_id.into(),
681            dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
682            center_group: center_group.clone(),
683            dock_pane,
684            project_panel_open: true
685        }
686    }
687
688    #[gpui::test]
689    async fn test_basic_dock_pane() {
690        env_logger::try_init().ok();
691
692        let db = WorkspaceDb(open_test_db("basic_dock_pane").await);
693
694        let dock_pane = crate::persistence::model::SerializedPane::new(
695            vec![
696                SerializedItem::new("Terminal", 1, false),
697                SerializedItem::new("Terminal", 4, false),
698                SerializedItem::new("Terminal", 2, false),
699                SerializedItem::new("Terminal", 3, true),
700            ],
701            false,
702        );
703
704        let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default());
705
706        db.save_workspace(workspace.clone()).await;
707
708        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
709
710        assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
711    }
712
713    #[gpui::test]
714    async fn test_simple_split() {
715        env_logger::try_init().ok();
716
717        let db = WorkspaceDb(open_test_db("simple_split").await);
718
719        //  -----------------
720        //  | 1,2   | 5,6   |
721        //  | - - - |       |
722        //  | 3,4   |       |
723        //  -----------------
724        let center_pane = SerializedPaneGroup::Group {
725            axis: gpui::Axis::Horizontal,
726            children: vec![
727                SerializedPaneGroup::Group {
728                    axis: gpui::Axis::Vertical,
729                    children: vec![
730                        SerializedPaneGroup::Pane(SerializedPane::new(
731                            vec![
732                                SerializedItem::new("Terminal", 1, false),
733                                SerializedItem::new("Terminal", 2, true),
734                            ],
735                            false,
736                        )),
737                        SerializedPaneGroup::Pane(SerializedPane::new(
738                            vec![
739                                SerializedItem::new("Terminal", 4, false),
740                                SerializedItem::new("Terminal", 3, true),
741                            ],
742                            true,
743                        )),
744                    ],
745                },
746                SerializedPaneGroup::Pane(SerializedPane::new(
747                    vec![
748                        SerializedItem::new("Terminal", 5, true),
749                        SerializedItem::new("Terminal", 6, false),
750                    ],
751                    false,
752                )),
753            ],
754        };
755
756        let workspace = default_workspace(&["/tmp"], Default::default(), &center_pane);
757
758        db.save_workspace(workspace.clone()).await;
759
760        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
761
762        assert_eq!(workspace.center_group, new_workspace.center_group);
763    }
764
765    #[gpui::test]
766    async fn test_cleanup_panes() {
767        env_logger::try_init().ok();
768
769        let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
770
771        let center_pane = SerializedPaneGroup::Group {
772            axis: gpui::Axis::Horizontal,
773            children: vec![
774                SerializedPaneGroup::Group {
775                    axis: gpui::Axis::Vertical,
776                    children: vec![
777                        SerializedPaneGroup::Pane(SerializedPane::new(
778                            vec![
779                                SerializedItem::new("Terminal", 1, false),
780                                SerializedItem::new("Terminal", 2, true),
781                            ],
782                            false,
783                        )),
784                        SerializedPaneGroup::Pane(SerializedPane::new(
785                            vec![
786                                SerializedItem::new("Terminal", 4, false),
787                                SerializedItem::new("Terminal", 3, true),
788                            ],
789                            true,
790                        )),
791                    ],
792                },
793                SerializedPaneGroup::Pane(SerializedPane::new(
794                    vec![
795                        SerializedItem::new("Terminal", 5, false),
796                        SerializedItem::new("Terminal", 6, true),
797                    ],
798                    false,
799                )),
800            ],
801        };
802
803        let id = &["/tmp"];
804
805        let mut workspace = default_workspace(id, Default::default(), &center_pane);
806
807        db.save_workspace(workspace.clone()).await;
808
809        workspace.center_group = SerializedPaneGroup::Group {
810            axis: gpui::Axis::Vertical,
811            children: vec![
812                SerializedPaneGroup::Pane(SerializedPane::new(
813                    vec![
814                        SerializedItem::new("Terminal", 1, false),
815                        SerializedItem::new("Terminal", 2, true),
816                    ],
817                    false,
818                )),
819                SerializedPaneGroup::Pane(SerializedPane::new(
820                    vec![
821                        SerializedItem::new("Terminal", 4, true),
822                        SerializedItem::new("Terminal", 3, false),
823                    ],
824                    true,
825                )),
826            ],
827        };
828
829        db.save_workspace(workspace.clone()).await;
830
831        let new_workspace = db.workspace_for_roots(id).unwrap();
832
833        assert_eq!(workspace.center_group, new_workspace.center_group);
834    }
835}