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