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    use super::*;
501    use db::open_test_db;
502
503    #[gpui::test]
504    async fn test_next_id_stability() {
505        env_logger::try_init().ok();
506
507        let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
508
509        db.write(|conn| {
510            conn.migrate(
511                "test_table",
512                &[sql!(
513                    CREATE TABLE test_table(
514                        text TEXT,
515                        workspace_id INTEGER,
516                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
517                        ON DELETE CASCADE
518                    ) STRICT;
519                )],
520            )
521            .unwrap();
522        })
523        .await;
524
525        let id = db.next_id().await.unwrap();
526        // Assert the empty row got inserted
527        assert_eq!(
528            Some(id),
529            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
530                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
531            ))
532            .unwrap()(id)
533            .unwrap()
534        );
535
536        db.write(move |conn| {
537            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
538                .unwrap()(("test-text-1", id))
539            .unwrap()
540        })
541        .await;
542
543        let test_text_1 = db
544            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
545            .unwrap()(1)
546        .unwrap()
547        .unwrap();
548        assert_eq!(test_text_1, "test-text-1");
549    }
550
551    #[gpui::test]
552    async fn test_workspace_id_stability() {
553        env_logger::try_init().ok();
554
555        let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
556
557        db.write(|conn| {
558            conn.migrate(
559                "test_table",
560                &[sql!(
561                    CREATE TABLE test_table(
562                        text TEXT,
563                        workspace_id INTEGER,
564                        FOREIGN KEY(workspace_id)
565                            REFERENCES workspaces(workspace_id)
566                        ON DELETE CASCADE
567                    ) STRICT;)],
568            )
569        })
570        .await
571        .unwrap();
572
573        let mut workspace_1 = SerializedWorkspace {
574            id: 1,
575            location: (["/tmp", "/tmp2"]).into(),
576            center_group: Default::default(),
577            bounds: Default::default(),
578            display: Default::default(),
579            docks: Default::default(),
580        };
581
582        let workspace_2 = SerializedWorkspace {
583            id: 2,
584            location: (["/tmp"]).into(),
585            center_group: Default::default(),
586            bounds: Default::default(),
587            display: Default::default(),
588            docks: Default::default(),
589        };
590
591        db.save_workspace(workspace_1.clone()).await;
592
593        db.write(|conn| {
594            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
595                .unwrap()(("test-text-1", 1))
596            .unwrap();
597        })
598        .await;
599
600        db.save_workspace(workspace_2.clone()).await;
601
602        db.write(|conn| {
603            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
604                .unwrap()(("test-text-2", 2))
605            .unwrap();
606        })
607        .await;
608
609        workspace_1.location = (["/tmp", "/tmp3"]).into();
610        db.save_workspace(workspace_1.clone()).await;
611        db.save_workspace(workspace_1).await;
612        db.save_workspace(workspace_2).await;
613
614        let test_text_2 = db
615            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
616            .unwrap()(2)
617        .unwrap()
618        .unwrap();
619        assert_eq!(test_text_2, "test-text-2");
620
621        let test_text_1 = db
622            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
623            .unwrap()(1)
624        .unwrap()
625        .unwrap();
626        assert_eq!(test_text_1, "test-text-1");
627    }
628
629    #[gpui::test]
630    async fn test_full_workspace_serialization() {
631        env_logger::try_init().ok();
632
633        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
634
635        //  -----------------
636        //  | 1,2   | 5,6   |
637        //  | - - - |       |
638        //  | 3,4   |       |
639        //  -----------------
640        let center_group = SerializedPaneGroup::Group {
641            axis: gpui::Axis::Horizontal,
642            children: vec![
643                SerializedPaneGroup::Group {
644                    axis: gpui::Axis::Vertical,
645                    children: vec![
646                        SerializedPaneGroup::Pane(SerializedPane::new(
647                            vec![
648                                SerializedItem::new("Terminal", 5, false),
649                                SerializedItem::new("Terminal", 6, true),
650                            ],
651                            false,
652                        )),
653                        SerializedPaneGroup::Pane(SerializedPane::new(
654                            vec![
655                                SerializedItem::new("Terminal", 7, true),
656                                SerializedItem::new("Terminal", 8, false),
657                            ],
658                            false,
659                        )),
660                    ],
661                },
662                SerializedPaneGroup::Pane(SerializedPane::new(
663                    vec![
664                        SerializedItem::new("Terminal", 9, false),
665                        SerializedItem::new("Terminal", 10, true),
666                    ],
667                    false,
668                )),
669            ],
670        };
671
672        let workspace = SerializedWorkspace {
673            id: 5,
674            location: (["/tmp", "/tmp2"]).into(),
675            center_group,
676            bounds: Default::default(),
677            display: Default::default(),
678            docks: Default::default(),
679        };
680
681        db.save_workspace(workspace.clone()).await;
682        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
683
684        assert_eq!(workspace, round_trip_workspace.unwrap());
685
686        // Test guaranteed duplicate IDs
687        db.save_workspace(workspace.clone()).await;
688        db.save_workspace(workspace.clone()).await;
689
690        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
691        assert_eq!(workspace, round_trip_workspace.unwrap());
692    }
693
694    #[gpui::test]
695    async fn test_workspace_assignment() {
696        env_logger::try_init().ok();
697
698        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
699
700        let workspace_1 = SerializedWorkspace {
701            id: 1,
702            location: (["/tmp", "/tmp2"]).into(),
703            center_group: Default::default(),
704            bounds: Default::default(),
705            display: Default::default(),
706            docks: Default::default(),
707        };
708
709        let mut workspace_2 = SerializedWorkspace {
710            id: 2,
711            location: (["/tmp"]).into(),
712            center_group: Default::default(),
713            bounds: Default::default(),
714            display: Default::default(),
715            docks: Default::default(),
716        };
717
718        db.save_workspace(workspace_1.clone()).await;
719        db.save_workspace(workspace_2.clone()).await;
720
721        // Test that paths are treated as a set
722        assert_eq!(
723            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
724            workspace_1
725        );
726        assert_eq!(
727            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
728            workspace_1
729        );
730
731        // Make sure that other keys work
732        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
733        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
734
735        // Test 'mutate' case of updating a pre-existing id
736        workspace_2.location = (["/tmp", "/tmp2"]).into();
737
738        db.save_workspace(workspace_2.clone()).await;
739        assert_eq!(
740            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
741            workspace_2
742        );
743
744        // Test other mechanism for mutating
745        let mut workspace_3 = SerializedWorkspace {
746            id: 3,
747            location: (&["/tmp", "/tmp2"]).into(),
748            center_group: Default::default(),
749            bounds: Default::default(),
750            display: Default::default(),
751            docks: Default::default(),
752        };
753
754        db.save_workspace(workspace_3.clone()).await;
755        assert_eq!(
756            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
757            workspace_3
758        );
759
760        // Make sure that updating paths differently also works
761        workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
762        db.save_workspace(workspace_3.clone()).await;
763        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
764        assert_eq!(
765            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
766                .unwrap(),
767            workspace_3
768        );
769    }
770
771    use crate::persistence::model::SerializedWorkspace;
772    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
773
774    fn default_workspace<P: AsRef<Path>>(
775        workspace_id: &[P],
776        center_group: &SerializedPaneGroup,
777    ) -> SerializedWorkspace {
778        SerializedWorkspace {
779            id: 4,
780            location: workspace_id.into(),
781            center_group: center_group.clone(),
782            bounds: Default::default(),
783            display: Default::default(),
784            docks: Default::default(),
785        }
786    }
787
788    #[gpui::test]
789    async fn test_simple_split() {
790        env_logger::try_init().ok();
791
792        let db = WorkspaceDb(open_test_db("simple_split").await);
793
794        //  -----------------
795        //  | 1,2   | 5,6   |
796        //  | - - - |       |
797        //  | 3,4   |       |
798        //  -----------------
799        let center_pane = SerializedPaneGroup::Group {
800            axis: gpui::Axis::Horizontal,
801            children: vec![
802                SerializedPaneGroup::Group {
803                    axis: gpui::Axis::Vertical,
804                    children: vec![
805                        SerializedPaneGroup::Pane(SerializedPane::new(
806                            vec![
807                                SerializedItem::new("Terminal", 1, false),
808                                SerializedItem::new("Terminal", 2, true),
809                            ],
810                            false,
811                        )),
812                        SerializedPaneGroup::Pane(SerializedPane::new(
813                            vec![
814                                SerializedItem::new("Terminal", 4, false),
815                                SerializedItem::new("Terminal", 3, true),
816                            ],
817                            true,
818                        )),
819                    ],
820                },
821                SerializedPaneGroup::Pane(SerializedPane::new(
822                    vec![
823                        SerializedItem::new("Terminal", 5, true),
824                        SerializedItem::new("Terminal", 6, false),
825                    ],
826                    false,
827                )),
828            ],
829        };
830
831        let workspace = default_workspace(&["/tmp"], &center_pane);
832
833        db.save_workspace(workspace.clone()).await;
834
835        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
836
837        assert_eq!(workspace.center_group, new_workspace.center_group);
838    }
839
840    #[gpui::test]
841    async fn test_cleanup_panes() {
842        env_logger::try_init().ok();
843
844        let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
845
846        let center_pane = SerializedPaneGroup::Group {
847            axis: gpui::Axis::Horizontal,
848            children: vec![
849                SerializedPaneGroup::Group {
850                    axis: gpui::Axis::Vertical,
851                    children: vec![
852                        SerializedPaneGroup::Pane(SerializedPane::new(
853                            vec![
854                                SerializedItem::new("Terminal", 1, false),
855                                SerializedItem::new("Terminal", 2, true),
856                            ],
857                            false,
858                        )),
859                        SerializedPaneGroup::Pane(SerializedPane::new(
860                            vec![
861                                SerializedItem::new("Terminal", 4, false),
862                                SerializedItem::new("Terminal", 3, true),
863                            ],
864                            true,
865                        )),
866                    ],
867                },
868                SerializedPaneGroup::Pane(SerializedPane::new(
869                    vec![
870                        SerializedItem::new("Terminal", 5, false),
871                        SerializedItem::new("Terminal", 6, true),
872                    ],
873                    false,
874                )),
875            ],
876        };
877
878        let id = &["/tmp"];
879
880        let mut workspace = default_workspace(id, &center_pane);
881
882        db.save_workspace(workspace.clone()).await;
883
884        workspace.center_group = SerializedPaneGroup::Group {
885            axis: gpui::Axis::Vertical,
886            children: vec![
887                SerializedPaneGroup::Pane(SerializedPane::new(
888                    vec![
889                        SerializedItem::new("Terminal", 1, false),
890                        SerializedItem::new("Terminal", 2, true),
891                    ],
892                    false,
893                )),
894                SerializedPaneGroup::Pane(SerializedPane::new(
895                    vec![
896                        SerializedItem::new("Terminal", 4, true),
897                        SerializedItem::new("Terminal", 3, false),
898                    ],
899                    true,
900                )),
901            ],
902        };
903
904        db.save_workspace(workspace.clone()).await;
905
906        let new_workspace = db.workspace_for_roots(id).unwrap();
907
908        assert_eq!(workspace.center_group, new_workspace.center_group);
909    }
910}