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