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