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