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