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::{platform::WindowBounds, Axis};
 10
 11use util::{unzip_option, ResultExt};
 12use uuid::Uuid;
 13
 14use crate::WorkspaceId;
 15
 16use model::{
 17    GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
 18    WorkspaceLocation,
 19};
 20
 21define_connection! {
 22    // Current schema shape using pseudo-rust syntax:
 23    //
 24    // workspaces(
 25    //   workspace_id: usize, // Primary key for workspaces
 26    //   workspace_location: Bincode<Vec<PathBuf>>,
 27    //   dock_visible: bool, // Deprecated
 28    //   dock_anchor: DockAnchor, // Deprecated
 29    //   dock_pane: Option<usize>, // Deprecated
 30    //   left_sidebar_open: boolean,
 31    //   timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
 32    //   window_state: String, // WindowBounds Discriminant
 33    //   window_x: Option<f32>, // WindowBounds::Fixed RectF x
 34    //   window_y: Option<f32>, // WindowBounds::Fixed RectF y
 35    //   window_width: Option<f32>, // WindowBounds::Fixed RectF width
 36    //   window_height: Option<f32>, // WindowBounds::Fixed RectF height
 37    //   display: Option<Uuid>, // Display id
 38    // )
 39    //
 40    // pane_groups(
 41    //   group_id: usize, // Primary key for pane_groups
 42    //   workspace_id: usize, // References workspaces table
 43    //   parent_group_id: Option<usize>, // None indicates that this is the root node
 44    //   position: Optiopn<usize>, // None indicates that this is the root node
 45    //   axis: Option<Axis>, // 'Vertical', 'Horizontal'
 46    // )
 47    //
 48    // panes(
 49    //     pane_id: usize, // Primary key for panes
 50    //     workspace_id: usize, // References workspaces table
 51    //     active: bool,
 52    // )
 53    //
 54    // center_panes(
 55    //     pane_id: usize, // Primary key for center_panes
 56    //     parent_group_id: Option<usize>, // References pane_groups. If none, this is the root
 57    //     position: Option<usize>, // None indicates this is the root
 58    // )
 59    //
 60    // CREATE TABLE items(
 61    //     item_id: usize, // This is the item's view id, so this is not unique
 62    //     workspace_id: usize, // References workspaces table
 63    //     pane_id: usize, // References panes table
 64    //     kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global
 65    //     position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column
 66    //     active: bool, // Indicates if this item is the active one in the pane
 67    // )
 68    pub static ref DB: WorkspaceDb<()> =
 69    &[sql!(
 70        CREATE TABLE workspaces(
 71            workspace_id INTEGER PRIMARY KEY,
 72            workspace_location BLOB UNIQUE,
 73            dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
 74            dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
 75            dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
 76            left_sidebar_open INTEGER, // Boolean
 77            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 78            FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
 79        ) STRICT;
 80
 81        CREATE TABLE pane_groups(
 82            group_id INTEGER PRIMARY KEY,
 83            workspace_id INTEGER NOT NULL,
 84            parent_group_id INTEGER, // NULL indicates that this is a root node
 85            position INTEGER, // NULL indicates that this is a root node
 86            axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
 87            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 88            ON DELETE CASCADE
 89            ON UPDATE CASCADE,
 90            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 91        ) STRICT;
 92
 93        CREATE TABLE panes(
 94            pane_id INTEGER PRIMARY KEY,
 95            workspace_id INTEGER NOT NULL,
 96            active INTEGER NOT NULL, // Boolean
 97            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 98            ON DELETE CASCADE
 99            ON UPDATE CASCADE
100        ) STRICT;
101
102        CREATE TABLE center_panes(
103            pane_id INTEGER PRIMARY KEY,
104            parent_group_id INTEGER, // NULL means that this is a root pane
105            position INTEGER, // NULL means that this is a root pane
106            FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
107            ON DELETE CASCADE,
108            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
109        ) STRICT;
110
111        CREATE TABLE items(
112            item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
113            workspace_id INTEGER NOT NULL,
114            pane_id INTEGER NOT NULL,
115            kind TEXT NOT NULL,
116            position INTEGER NOT NULL,
117            active INTEGER NOT NULL,
118            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
119            ON DELETE CASCADE
120            ON UPDATE CASCADE,
121            FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
122            ON DELETE CASCADE,
123            PRIMARY KEY(item_id, workspace_id)
124        ) STRICT;
125    ),
126    sql!(
127        ALTER TABLE workspaces ADD COLUMN window_state TEXT;
128        ALTER TABLE workspaces ADD COLUMN window_x REAL;
129        ALTER TABLE workspaces ADD COLUMN window_y REAL;
130        ALTER TABLE workspaces ADD COLUMN window_width REAL;
131        ALTER TABLE workspaces ADD COLUMN window_height REAL;
132        ALTER TABLE workspaces ADD COLUMN display BLOB;
133    )];
134}
135
136impl WorkspaceDb {
137    /// Returns a serialized workspace for the given worktree_roots. If the passed array
138    /// is empty, the most recent workspace is returned instead. If no workspace for the
139    /// passed roots is stored, returns none.
140    pub fn workspace_for_roots<P: AsRef<Path>>(
141        &self,
142        worktree_roots: &[P],
143    ) -> Option<SerializedWorkspace> {
144        let workspace_location: WorkspaceLocation = worktree_roots.into();
145
146        // Note that we re-assign the workspace_id here in case it's empty
147        // and we've grabbed the most recent workspace
148        let (workspace_id, workspace_location, left_sidebar_open, bounds, display): (
149            WorkspaceId,
150            WorkspaceLocation,
151            bool,
152            Option<WindowBounds>,
153            Option<Uuid>,
154        ) = self
155            .select_row_bound(sql! {
156                SELECT
157                    workspace_id,
158                    workspace_location,
159                    left_sidebar_open,
160                    window_state,
161                    window_x,
162                    window_y,
163                    window_width,
164                    window_height,
165                    display
166                FROM workspaces
167                WHERE workspace_location = ?
168            })
169            .and_then(|mut prepared_statement| (prepared_statement)(&workspace_location))
170            .context("No workspaces found")
171            .warn_on_err()
172            .flatten()?;
173
174        Some(SerializedWorkspace {
175            id: workspace_id,
176            location: workspace_location.clone(),
177            center_group: self
178                .get_center_pane_group(workspace_id)
179                .context("Getting center group")
180                .log_err()?,
181            left_sidebar_open,
182            bounds,
183            display,
184        })
185    }
186
187    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
188    /// that used this workspace previously
189    pub async fn save_workspace(&self, workspace: SerializedWorkspace) {
190        self.write(move |conn| {
191            conn.with_savepoint("update_worktrees", || {
192                // Clear out panes and pane_groups
193                conn.exec_bound(sql!(
194                    DELETE FROM pane_groups WHERE workspace_id = ?1;
195                    DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
196                .expect("Clearing old panes");
197
198                conn.exec_bound(sql!(
199                    DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?
200                ))?((&workspace.location, workspace.id.clone()))
201                .context("clearing out old locations")?;
202
203                // Upsert
204                conn.exec_bound(sql!(
205                    INSERT INTO workspaces(
206                        workspace_id,
207                        workspace_location,
208                        left_sidebar_open,
209                        timestamp
210                    )
211                    VALUES (?1, ?2, ?3, CURRENT_TIMESTAMP)
212                    ON CONFLICT DO
213                    UPDATE SET
214                        workspace_location = ?2,
215                        left_sidebar_open = ?3,
216                        timestamp = CURRENT_TIMESTAMP
217                ))?((workspace.id, &workspace.location))
218                .context("Updating workspace")?;
219
220                // Save center pane group
221                Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
222                    .context("save pane group in save workspace")?;
223
224                Ok(())
225            })
226            .log_err();
227        })
228        .await;
229    }
230
231    query! {
232        pub async fn next_id() -> Result<WorkspaceId> {
233            INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
234        }
235    }
236
237    query! {
238        fn recent_workspaces() -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
239            SELECT workspace_id, workspace_location
240            FROM workspaces
241            WHERE workspace_location IS NOT NULL
242            ORDER BY timestamp DESC
243        }
244    }
245
246    query! {
247        async fn delete_stale_workspace(id: WorkspaceId) -> Result<()> {
248            DELETE FROM workspaces
249            WHERE workspace_id IS ?
250        }
251    }
252
253    // Returns the recent locations which are still valid on disk and deletes ones which no longer
254    // exist.
255    pub async fn recent_workspaces_on_disk(&self) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
256        let mut result = Vec::new();
257        let mut delete_tasks = Vec::new();
258        for (id, location) in self.recent_workspaces()? {
259            if location.paths().iter().all(|path| path.exists())
260                && location.paths().iter().any(|path| path.is_dir())
261            {
262                result.push((id, location));
263            } else {
264                delete_tasks.push(self.delete_stale_workspace(id));
265            }
266        }
267
268        futures::future::join_all(delete_tasks).await;
269        Ok(result)
270    }
271
272    pub async fn last_workspace(&self) -> Result<Option<WorkspaceLocation>> {
273        Ok(self
274            .recent_workspaces_on_disk()
275            .await?
276            .into_iter()
277            .next()
278            .map(|(_, location)| location))
279    }
280
281    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
282        Ok(self
283            .get_pane_group(workspace_id, None)?
284            .into_iter()
285            .next()
286            .unwrap_or_else(|| {
287                SerializedPaneGroup::Pane(SerializedPane {
288                    active: true,
289                    children: vec![],
290                })
291            }))
292    }
293
294    fn get_pane_group(
295        &self,
296        workspace_id: WorkspaceId,
297        group_id: Option<GroupId>,
298    ) -> Result<Vec<SerializedPaneGroup>> {
299        type GroupKey = (Option<GroupId>, WorkspaceId);
300        type GroupOrPane = (Option<GroupId>, Option<Axis>, Option<PaneId>, Option<bool>);
301        self.select_bound::<GroupKey, GroupOrPane>(sql!(
302            SELECT group_id, axis, pane_id, active
303                FROM (SELECT
304                    group_id,
305                    axis,
306                    NULL as pane_id,
307                    NULL as active,
308                    position,
309                    parent_group_id,
310                    workspace_id
311                    FROM pane_groups
312                    UNION
313                    SELECT
314                    NULL,
315                    NULL,
316                    center_panes.pane_id,
317                    panes.active as active,
318                    position,
319                    parent_group_id,
320                    panes.workspace_id as workspace_id
321                        FROM center_panes
322                        JOIN panes ON center_panes.pane_id = panes.pane_id)
323                WHERE parent_group_id IS ? AND workspace_id = ?
324                ORDER BY position
325        ))?((group_id, workspace_id))?
326        .into_iter()
327        .map(|(group_id, axis, pane_id, active)| {
328            if let Some((group_id, axis)) = group_id.zip(axis) {
329                Ok(SerializedPaneGroup::Group {
330                    axis,
331                    children: self.get_pane_group(workspace_id, Some(group_id))?,
332                })
333            } else if let Some((pane_id, active)) = pane_id.zip(active) {
334                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
335                    self.get_items(pane_id)?,
336                    active,
337                )))
338            } else {
339                bail!("Pane Group Child was neither a pane group or a pane");
340            }
341        })
342        // Filter out panes and pane groups which don't have any children or items
343        .filter(|pane_group| match pane_group {
344            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
345            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
346            _ => true,
347        })
348        .collect::<Result<_>>()
349    }
350
351    fn save_pane_group(
352        conn: &Connection,
353        workspace_id: WorkspaceId,
354        pane_group: &SerializedPaneGroup,
355        parent: Option<(GroupId, usize)>,
356    ) -> Result<()> {
357        match pane_group {
358            SerializedPaneGroup::Group { axis, children } => {
359                let (parent_id, position) = unzip_option(parent);
360
361                let group_id = conn.select_row_bound::<_, i64>(sql!(
362                    INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis)
363                    VALUES (?, ?, ?, ?)
364                    RETURNING group_id
365                ))?((workspace_id, parent_id, position, *axis))?
366                .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
367
368                for (position, group) in children.iter().enumerate() {
369                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
370                }
371
372                Ok(())
373            }
374            SerializedPaneGroup::Pane(pane) => {
375                Self::save_pane(conn, workspace_id, &pane, parent)?;
376                Ok(())
377            }
378        }
379    }
380
381    fn save_pane(
382        conn: &Connection,
383        workspace_id: WorkspaceId,
384        pane: &SerializedPane,
385        parent: Option<(GroupId, usize)>,
386    ) -> Result<PaneId> {
387        let pane_id = conn.select_row_bound::<_, i64>(sql!(
388            INSERT INTO panes(workspace_id, active)
389            VALUES (?, ?)
390            RETURNING pane_id
391        ))?((workspace_id, pane.active))?
392        .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
393
394        let (parent_id, order) = unzip_option(parent);
395        conn.exec_bound(sql!(
396            INSERT INTO center_panes(pane_id, parent_group_id, position)
397            VALUES (?, ?, ?)
398        ))?((pane_id, parent_id, order))?;
399
400        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
401
402        Ok(pane_id)
403    }
404
405    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
406        Ok(self.select_bound(sql!(
407            SELECT kind, item_id, active FROM items
408            WHERE pane_id = ?
409                ORDER BY position
410        ))?(pane_id)?)
411    }
412
413    fn save_items(
414        conn: &Connection,
415        workspace_id: WorkspaceId,
416        pane_id: PaneId,
417        items: &[SerializedItem],
418    ) -> Result<()> {
419        let mut insert = conn.exec_bound(sql!(
420            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?)
421        )).context("Preparing insertion")?;
422        for (position, item) in items.iter().enumerate() {
423            insert((workspace_id, pane_id, position, item))?;
424        }
425
426        Ok(())
427    }
428
429    query! {
430        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
431            UPDATE workspaces
432            SET timestamp = CURRENT_TIMESTAMP
433            WHERE workspace_id = ?
434        }
435    }
436
437    query! {
438        pub async fn set_window_bounds(workspace_id: WorkspaceId, bounds: WindowBounds, display: Uuid) -> Result<()> {
439            UPDATE workspaces
440            SET window_state = ?2,
441                window_x = ?3,
442                window_y = ?4,
443                window_width = ?5,
444                window_height = ?6,
445                display = ?7
446            WHERE workspace_id = ?1
447        }
448    }
449}
450
451#[cfg(test)]
452mod tests {
453
454    use db::open_test_db;
455
456    use super::*;
457
458    #[gpui::test]
459    async fn test_next_id_stability() {
460        env_logger::try_init().ok();
461
462        let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
463
464        db.write(|conn| {
465            conn.migrate(
466                "test_table",
467                &[sql!(
468                    CREATE TABLE test_table(
469                        text TEXT,
470                        workspace_id INTEGER,
471                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
472                        ON DELETE CASCADE
473                    ) STRICT;
474                )],
475            )
476            .unwrap();
477        })
478        .await;
479
480        let id = db.next_id().await.unwrap();
481        // Assert the empty row got inserted
482        assert_eq!(
483            Some(id),
484            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
485                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
486            ))
487            .unwrap()(id)
488            .unwrap()
489        );
490
491        db.write(move |conn| {
492            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
493                .unwrap()(("test-text-1", id))
494            .unwrap()
495        })
496        .await;
497
498        let test_text_1 = db
499            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
500            .unwrap()(1)
501        .unwrap()
502        .unwrap();
503        assert_eq!(test_text_1, "test-text-1");
504    }
505
506    #[gpui::test]
507    async fn test_workspace_id_stability() {
508        env_logger::try_init().ok();
509
510        let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
511
512        db.write(|conn| {
513            conn.migrate(
514                "test_table",
515                &[sql!(
516                    CREATE TABLE test_table(
517                        text TEXT,
518                        workspace_id INTEGER,
519                        FOREIGN KEY(workspace_id)
520                            REFERENCES workspaces(workspace_id)
521                        ON DELETE CASCADE
522                    ) STRICT;)],
523            )
524        })
525        .await
526        .unwrap();
527
528        let mut workspace_1 = SerializedWorkspace {
529            id: 1,
530            location: (["/tmp", "/tmp2"]).into(),
531            center_group: Default::default(),
532            left_sidebar_open: true,
533            bounds: Default::default(),
534            display: Default::default(),
535        };
536
537        let mut _workspace_2 = SerializedWorkspace {
538            id: 2,
539            location: (["/tmp"]).into(),
540            center_group: Default::default(),
541            left_sidebar_open: false,
542            bounds: Default::default(),
543            display: Default::default(),
544        };
545
546        db.save_workspace(workspace_1.clone()).await;
547
548        db.write(|conn| {
549            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
550                .unwrap()(("test-text-1", 1))
551            .unwrap();
552        })
553        .await;
554
555        db.save_workspace(_workspace_2.clone()).await;
556
557        db.write(|conn| {
558            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
559                .unwrap()(("test-text-2", 2))
560            .unwrap();
561        })
562        .await;
563
564        workspace_1.location = (["/tmp", "/tmp3"]).into();
565        db.save_workspace(workspace_1.clone()).await;
566        db.save_workspace(workspace_1).await;
567
568        todo!();
569        // workspace_2.dock_pane.children.push(SerializedItem {
570        //     kind: Arc::from("Test"),
571        //     item_id: 10,
572        //     active: true,
573        // });
574        // db.save_workspace(workspace_2).await;
575
576        // let test_text_2 = db
577        //     .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
578        //     .unwrap()(2)
579        // .unwrap()
580        // .unwrap();
581        // assert_eq!(test_text_2, "test-text-2");
582
583        // let test_text_1 = db
584        //     .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
585        //     .unwrap()(1)
586        // .unwrap()
587        // .unwrap();
588        // assert_eq!(test_text_1, "test-text-1");
589    }
590
591    #[gpui::test]
592    async fn test_full_workspace_serialization() {
593        env_logger::try_init().ok();
594
595        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
596
597        //  -----------------
598        //  | 1,2   | 5,6   |
599        //  | - - - |       |
600        //  | 3,4   |       |
601        //  -----------------
602        let center_group = SerializedPaneGroup::Group {
603            axis: gpui::Axis::Horizontal,
604            children: vec![
605                SerializedPaneGroup::Group {
606                    axis: gpui::Axis::Vertical,
607                    children: vec![
608                        SerializedPaneGroup::Pane(SerializedPane::new(
609                            vec![
610                                SerializedItem::new("Terminal", 5, false),
611                                SerializedItem::new("Terminal", 6, true),
612                            ],
613                            false,
614                        )),
615                        SerializedPaneGroup::Pane(SerializedPane::new(
616                            vec![
617                                SerializedItem::new("Terminal", 7, true),
618                                SerializedItem::new("Terminal", 8, false),
619                            ],
620                            false,
621                        )),
622                    ],
623                },
624                SerializedPaneGroup::Pane(SerializedPane::new(
625                    vec![
626                        SerializedItem::new("Terminal", 9, false),
627                        SerializedItem::new("Terminal", 10, true),
628                    ],
629                    false,
630                )),
631            ],
632        };
633
634        let workspace = SerializedWorkspace {
635            id: 5,
636            location: (["/tmp", "/tmp2"]).into(),
637            center_group,
638            left_sidebar_open: true,
639            bounds: Default::default(),
640            display: Default::default(),
641        };
642
643        db.save_workspace(workspace.clone()).await;
644        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
645
646        assert_eq!(workspace, round_trip_workspace.unwrap());
647
648        // Test guaranteed duplicate IDs
649        db.save_workspace(workspace.clone()).await;
650        db.save_workspace(workspace.clone()).await;
651
652        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
653        assert_eq!(workspace, round_trip_workspace.unwrap());
654    }
655
656    #[gpui::test]
657    async fn test_workspace_assignment() {
658        env_logger::try_init().ok();
659
660        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
661
662        let workspace_1 = SerializedWorkspace {
663            id: 1,
664            location: (["/tmp", "/tmp2"]).into(),
665            center_group: Default::default(),
666            left_sidebar_open: true,
667            bounds: Default::default(),
668            display: Default::default(),
669        };
670
671        let mut workspace_2 = SerializedWorkspace {
672            id: 2,
673            location: (["/tmp"]).into(),
674            center_group: Default::default(),
675            left_sidebar_open: false,
676            bounds: Default::default(),
677            display: Default::default(),
678        };
679
680        db.save_workspace(workspace_1.clone()).await;
681        db.save_workspace(workspace_2.clone()).await;
682
683        // Test that paths are treated as a set
684        assert_eq!(
685            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
686            workspace_1
687        );
688        assert_eq!(
689            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
690            workspace_1
691        );
692
693        // Make sure that other keys work
694        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
695        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
696
697        // Test 'mutate' case of updating a pre-existing id
698        workspace_2.location = (["/tmp", "/tmp2"]).into();
699
700        db.save_workspace(workspace_2.clone()).await;
701        assert_eq!(
702            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
703            workspace_2
704        );
705
706        // Test other mechanism for mutating
707        let mut workspace_3 = SerializedWorkspace {
708            id: 3,
709            location: (&["/tmp", "/tmp2"]).into(),
710            center_group: Default::default(),
711            left_sidebar_open: false,
712            bounds: Default::default(),
713            display: Default::default(),
714        };
715
716        db.save_workspace(workspace_3.clone()).await;
717        assert_eq!(
718            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
719            workspace_3
720        );
721
722        // Make sure that updating paths differently also works
723        workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
724        db.save_workspace(workspace_3.clone()).await;
725        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
726        assert_eq!(
727            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
728                .unwrap(),
729            workspace_3
730        );
731    }
732
733    use crate::persistence::model::SerializedWorkspace;
734    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
735
736    fn default_workspace<P: AsRef<Path>>(
737        workspace_id: &[P],
738        center_group: &SerializedPaneGroup,
739    ) -> SerializedWorkspace {
740        SerializedWorkspace {
741            id: 4,
742            location: workspace_id.into(),
743            center_group: center_group.clone(),
744            left_sidebar_open: true,
745            bounds: Default::default(),
746            display: Default::default(),
747        }
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"], &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, &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}