persistence.rs

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