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