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 mut _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
613        todo!();
614        // workspace_2.dock_pane.children.push(SerializedItem {
615        //     kind: Arc::from("Test"),
616        //     item_id: 10,
617        //     active: true,
618        // });
619        // db.save_workspace(workspace_2).await;
620
621        // let test_text_2 = db
622        //     .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
623        //     .unwrap()(2)
624        // .unwrap()
625        // .unwrap();
626        // assert_eq!(test_text_2, "test-text-2");
627
628        // let test_text_1 = db
629        //     .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
630        //     .unwrap()(1)
631        // .unwrap()
632        // .unwrap();
633        // assert_eq!(test_text_1, "test-text-1");
634    }
635
636    #[gpui::test]
637    async fn test_full_workspace_serialization() {
638        env_logger::try_init().ok();
639
640        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
641
642        //  -----------------
643        //  | 1,2   | 5,6   |
644        //  | - - - |       |
645        //  | 3,4   |       |
646        //  -----------------
647        let center_group = SerializedPaneGroup::Group {
648            axis: gpui::Axis::Horizontal,
649            children: vec![
650                SerializedPaneGroup::Group {
651                    axis: gpui::Axis::Vertical,
652                    children: vec![
653                        SerializedPaneGroup::Pane(SerializedPane::new(
654                            vec![
655                                SerializedItem::new("Terminal", 5, false),
656                                SerializedItem::new("Terminal", 6, true),
657                            ],
658                            false,
659                        )),
660                        SerializedPaneGroup::Pane(SerializedPane::new(
661                            vec![
662                                SerializedItem::new("Terminal", 7, true),
663                                SerializedItem::new("Terminal", 8, false),
664                            ],
665                            false,
666                        )),
667                    ],
668                },
669                SerializedPaneGroup::Pane(SerializedPane::new(
670                    vec![
671                        SerializedItem::new("Terminal", 9, false),
672                        SerializedItem::new("Terminal", 10, true),
673                    ],
674                    false,
675                )),
676            ],
677        };
678
679        let workspace = SerializedWorkspace {
680            id: 5,
681            location: (["/tmp", "/tmp2"]).into(),
682            center_group,
683            bounds: Default::default(),
684            display: Default::default(),
685            docks: Default::default(),
686        };
687
688        db.save_workspace(workspace.clone()).await;
689        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
690
691        assert_eq!(workspace, round_trip_workspace.unwrap());
692
693        // Test guaranteed duplicate IDs
694        db.save_workspace(workspace.clone()).await;
695        db.save_workspace(workspace.clone()).await;
696
697        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
698        assert_eq!(workspace, round_trip_workspace.unwrap());
699    }
700
701    #[gpui::test]
702    async fn test_workspace_assignment() {
703        env_logger::try_init().ok();
704
705        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
706
707        let workspace_1 = SerializedWorkspace {
708            id: 1,
709            location: (["/tmp", "/tmp2"]).into(),
710            center_group: Default::default(),
711            bounds: Default::default(),
712            display: Default::default(),
713            docks: Default::default(),
714        };
715
716        let mut workspace_2 = SerializedWorkspace {
717            id: 2,
718            location: (["/tmp"]).into(),
719            center_group: Default::default(),
720            bounds: Default::default(),
721            display: Default::default(),
722            docks: Default::default(),
723        };
724
725        db.save_workspace(workspace_1.clone()).await;
726        db.save_workspace(workspace_2.clone()).await;
727
728        // Test that paths are treated as a set
729        assert_eq!(
730            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
731            workspace_1
732        );
733        assert_eq!(
734            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
735            workspace_1
736        );
737
738        // Make sure that other keys work
739        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
740        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
741
742        // Test 'mutate' case of updating a pre-existing id
743        workspace_2.location = (["/tmp", "/tmp2"]).into();
744
745        db.save_workspace(workspace_2.clone()).await;
746        assert_eq!(
747            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
748            workspace_2
749        );
750
751        // Test other mechanism for mutating
752        let mut workspace_3 = SerializedWorkspace {
753            id: 3,
754            location: (&["/tmp", "/tmp2"]).into(),
755            center_group: Default::default(),
756            bounds: Default::default(),
757            display: Default::default(),
758            docks: Default::default(),
759        };
760
761        db.save_workspace(workspace_3.clone()).await;
762        assert_eq!(
763            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
764            workspace_3
765        );
766
767        // Make sure that updating paths differently also works
768        workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
769        db.save_workspace(workspace_3.clone()).await;
770        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
771        assert_eq!(
772            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
773                .unwrap(),
774            workspace_3
775        );
776    }
777
778    use crate::persistence::model::SerializedWorkspace;
779    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
780
781    fn default_workspace<P: AsRef<Path>>(
782        workspace_id: &[P],
783        center_group: &SerializedPaneGroup,
784    ) -> SerializedWorkspace {
785        SerializedWorkspace {
786            id: 4,
787            location: workspace_id.into(),
788            center_group: center_group.clone(),
789            bounds: Default::default(),
790            display: Default::default(),
791            docks: Default::default(),
792        }
793    }
794
795    #[gpui::test]
796    async fn test_simple_split() {
797        env_logger::try_init().ok();
798
799        let db = WorkspaceDb(open_test_db("simple_split").await);
800
801        //  -----------------
802        //  | 1,2   | 5,6   |
803        //  | - - - |       |
804        //  | 3,4   |       |
805        //  -----------------
806        let center_pane = SerializedPaneGroup::Group {
807            axis: gpui::Axis::Horizontal,
808            children: vec![
809                SerializedPaneGroup::Group {
810                    axis: gpui::Axis::Vertical,
811                    children: vec![
812                        SerializedPaneGroup::Pane(SerializedPane::new(
813                            vec![
814                                SerializedItem::new("Terminal", 1, false),
815                                SerializedItem::new("Terminal", 2, true),
816                            ],
817                            false,
818                        )),
819                        SerializedPaneGroup::Pane(SerializedPane::new(
820                            vec![
821                                SerializedItem::new("Terminal", 4, false),
822                                SerializedItem::new("Terminal", 3, true),
823                            ],
824                            true,
825                        )),
826                    ],
827                },
828                SerializedPaneGroup::Pane(SerializedPane::new(
829                    vec![
830                        SerializedItem::new("Terminal", 5, true),
831                        SerializedItem::new("Terminal", 6, false),
832                    ],
833                    false,
834                )),
835            ],
836        };
837
838        let workspace = default_workspace(&["/tmp"], &center_pane);
839
840        db.save_workspace(workspace.clone()).await;
841
842        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
843
844        assert_eq!(workspace.center_group, new_workspace.center_group);
845    }
846
847    #[gpui::test]
848    async fn test_cleanup_panes() {
849        env_logger::try_init().ok();
850
851        let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
852
853        let center_pane = SerializedPaneGroup::Group {
854            axis: gpui::Axis::Horizontal,
855            children: vec![
856                SerializedPaneGroup::Group {
857                    axis: gpui::Axis::Vertical,
858                    children: vec![
859                        SerializedPaneGroup::Pane(SerializedPane::new(
860                            vec![
861                                SerializedItem::new("Terminal", 1, false),
862                                SerializedItem::new("Terminal", 2, true),
863                            ],
864                            false,
865                        )),
866                        SerializedPaneGroup::Pane(SerializedPane::new(
867                            vec![
868                                SerializedItem::new("Terminal", 4, false),
869                                SerializedItem::new("Terminal", 3, true),
870                            ],
871                            true,
872                        )),
873                    ],
874                },
875                SerializedPaneGroup::Pane(SerializedPane::new(
876                    vec![
877                        SerializedItem::new("Terminal", 5, false),
878                        SerializedItem::new("Terminal", 6, true),
879                    ],
880                    false,
881                )),
882            ],
883        };
884
885        let id = &["/tmp"];
886
887        let mut workspace = default_workspace(id, &center_pane);
888
889        db.save_workspace(workspace.clone()).await;
890
891        workspace.center_group = SerializedPaneGroup::Group {
892            axis: gpui::Axis::Vertical,
893            children: vec![
894                SerializedPaneGroup::Pane(SerializedPane::new(
895                    vec![
896                        SerializedItem::new("Terminal", 1, false),
897                        SerializedItem::new("Terminal", 2, true),
898                    ],
899                    false,
900                )),
901                SerializedPaneGroup::Pane(SerializedPane::new(
902                    vec![
903                        SerializedItem::new("Terminal", 4, true),
904                        SerializedItem::new("Terminal", 3, false),
905                    ],
906                    true,
907                )),
908            ],
909        };
910
911        db.save_workspace(workspace.clone()).await;
912
913        let new_workspace = db.workspace_for_roots(id).unwrap();
914
915        assert_eq!(workspace.center_group, new_workspace.center_group);
916    }
917}