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