persistence.rs

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