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    //   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 db::open_test_db;
556
557    #[gpui::test]
558    async fn test_next_id_stability() {
559        env_logger::try_init().ok();
560
561        let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
562
563        db.write(|conn| {
564            conn.migrate(
565                "test_table",
566                &[sql!(
567                    CREATE TABLE test_table(
568                        text TEXT,
569                        workspace_id INTEGER,
570                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
571                        ON DELETE CASCADE
572                    ) STRICT;
573                )],
574            )
575            .unwrap();
576        })
577        .await;
578
579        let id = db.next_id().await.unwrap();
580        // Assert the empty row got inserted
581        assert_eq!(
582            Some(id),
583            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
584                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
585            ))
586            .unwrap()(id)
587            .unwrap()
588        );
589
590        db.write(move |conn| {
591            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
592                .unwrap()(("test-text-1", id))
593            .unwrap()
594        })
595        .await;
596
597        let test_text_1 = db
598            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
599            .unwrap()(1)
600        .unwrap()
601        .unwrap();
602        assert_eq!(test_text_1, "test-text-1");
603    }
604
605    #[gpui::test]
606    async fn test_workspace_id_stability() {
607        env_logger::try_init().ok();
608
609        let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
610
611        db.write(|conn| {
612            conn.migrate(
613                "test_table",
614                &[sql!(
615                    CREATE TABLE test_table(
616                        text TEXT,
617                        workspace_id INTEGER,
618                        FOREIGN KEY(workspace_id)
619                            REFERENCES workspaces(workspace_id)
620                        ON DELETE CASCADE
621                    ) STRICT;)],
622            )
623        })
624        .await
625        .unwrap();
626
627        let mut workspace_1 = SerializedWorkspace {
628            id: 1,
629            location: (["/tmp", "/tmp2"]).into(),
630            center_group: Default::default(),
631            bounds: Default::default(),
632            display: Default::default(),
633            docks: Default::default(),
634        };
635
636        let workspace_2 = SerializedWorkspace {
637            id: 2,
638            location: (["/tmp"]).into(),
639            center_group: Default::default(),
640            bounds: Default::default(),
641            display: Default::default(),
642            docks: Default::default(),
643        };
644
645        db.save_workspace(workspace_1.clone()).await;
646
647        db.write(|conn| {
648            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
649                .unwrap()(("test-text-1", 1))
650            .unwrap();
651        })
652        .await;
653
654        db.save_workspace(workspace_2.clone()).await;
655
656        db.write(|conn| {
657            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
658                .unwrap()(("test-text-2", 2))
659            .unwrap();
660        })
661        .await;
662
663        workspace_1.location = (["/tmp", "/tmp3"]).into();
664        db.save_workspace(workspace_1.clone()).await;
665        db.save_workspace(workspace_1).await;
666        db.save_workspace(workspace_2).await;
667
668        let test_text_2 = db
669            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
670            .unwrap()(2)
671        .unwrap()
672        .unwrap();
673        assert_eq!(test_text_2, "test-text-2");
674
675        let test_text_1 = db
676            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
677            .unwrap()(1)
678        .unwrap()
679        .unwrap();
680        assert_eq!(test_text_1, "test-text-1");
681    }
682
683    fn group(axis: gpui::Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
684        SerializedPaneGroup::Group {
685            axis,
686            flexes: None,
687            children,
688        }
689    }
690
691    #[gpui::test]
692    async fn test_full_workspace_serialization() {
693        env_logger::try_init().ok();
694
695        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
696
697        //  -----------------
698        //  | 1,2   | 5,6   |
699        //  | - - - |       |
700        //  | 3,4   |       |
701        //  -----------------
702        let center_group = group(
703            gpui::Axis::Horizontal,
704            vec![
705                group(
706                    gpui::Axis::Vertical,
707                    vec![
708                        SerializedPaneGroup::Pane(SerializedPane::new(
709                            vec![
710                                SerializedItem::new("Terminal", 5, false),
711                                SerializedItem::new("Terminal", 6, true),
712                            ],
713                            false,
714                        )),
715                        SerializedPaneGroup::Pane(SerializedPane::new(
716                            vec![
717                                SerializedItem::new("Terminal", 7, true),
718                                SerializedItem::new("Terminal", 8, false),
719                            ],
720                            false,
721                        )),
722                    ],
723                ),
724                SerializedPaneGroup::Pane(SerializedPane::new(
725                    vec![
726                        SerializedItem::new("Terminal", 9, false),
727                        SerializedItem::new("Terminal", 10, true),
728                    ],
729                    false,
730                )),
731            ],
732        );
733
734        let workspace = SerializedWorkspace {
735            id: 5,
736            location: (["/tmp", "/tmp2"]).into(),
737            center_group,
738            bounds: Default::default(),
739            display: Default::default(),
740            docks: Default::default(),
741        };
742
743        db.save_workspace(workspace.clone()).await;
744        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
745
746        assert_eq!(workspace, round_trip_workspace.unwrap());
747
748        // Test guaranteed duplicate IDs
749        db.save_workspace(workspace.clone()).await;
750        db.save_workspace(workspace.clone()).await;
751
752        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
753        assert_eq!(workspace, round_trip_workspace.unwrap());
754    }
755
756    #[gpui::test]
757    async fn test_workspace_assignment() {
758        env_logger::try_init().ok();
759
760        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
761
762        let workspace_1 = SerializedWorkspace {
763            id: 1,
764            location: (["/tmp", "/tmp2"]).into(),
765            center_group: Default::default(),
766            bounds: Default::default(),
767            display: Default::default(),
768            docks: Default::default(),
769        };
770
771        let mut workspace_2 = SerializedWorkspace {
772            id: 2,
773            location: (["/tmp"]).into(),
774            center_group: Default::default(),
775            bounds: Default::default(),
776            display: Default::default(),
777            docks: Default::default(),
778        };
779
780        db.save_workspace(workspace_1.clone()).await;
781        db.save_workspace(workspace_2.clone()).await;
782
783        // Test that paths are treated as a set
784        assert_eq!(
785            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
786            workspace_1
787        );
788        assert_eq!(
789            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
790            workspace_1
791        );
792
793        // Make sure that other keys work
794        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
795        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
796
797        // Test 'mutate' case of updating a pre-existing id
798        workspace_2.location = (["/tmp", "/tmp2"]).into();
799
800        db.save_workspace(workspace_2.clone()).await;
801        assert_eq!(
802            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
803            workspace_2
804        );
805
806        // Test other mechanism for mutating
807        let mut workspace_3 = SerializedWorkspace {
808            id: 3,
809            location: (&["/tmp", "/tmp2"]).into(),
810            center_group: Default::default(),
811            bounds: Default::default(),
812            display: Default::default(),
813            docks: Default::default(),
814        };
815
816        db.save_workspace(workspace_3.clone()).await;
817        assert_eq!(
818            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
819            workspace_3
820        );
821
822        // Make sure that updating paths differently also works
823        workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
824        db.save_workspace(workspace_3.clone()).await;
825        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
826        assert_eq!(
827            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
828                .unwrap(),
829            workspace_3
830        );
831    }
832
833    use crate::persistence::model::SerializedWorkspace;
834    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
835
836    fn default_workspace<P: AsRef<Path>>(
837        workspace_id: &[P],
838        center_group: &SerializedPaneGroup,
839    ) -> SerializedWorkspace {
840        SerializedWorkspace {
841            id: 4,
842            location: workspace_id.into(),
843            center_group: center_group.clone(),
844            bounds: Default::default(),
845            display: Default::default(),
846            docks: Default::default(),
847        }
848    }
849
850    #[gpui::test]
851    async fn test_simple_split() {
852        env_logger::try_init().ok();
853
854        let db = WorkspaceDb(open_test_db("simple_split").await);
855
856        //  -----------------
857        //  | 1,2   | 5,6   |
858        //  | - - - |       |
859        //  | 3,4   |       |
860        //  -----------------
861        let center_pane = group(
862            gpui::Axis::Horizontal,
863            vec![
864                group(
865                    gpui::Axis::Vertical,
866                    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, true),
886                        SerializedItem::new("Terminal", 6, false),
887                    ],
888                    false,
889                )),
890            ],
891        );
892
893        let workspace = default_workspace(&["/tmp"], &center_pane);
894
895        db.save_workspace(workspace.clone()).await;
896
897        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
898
899        assert_eq!(workspace.center_group, new_workspace.center_group);
900    }
901
902    #[gpui::test]
903    async fn test_cleanup_panes() {
904        env_logger::try_init().ok();
905
906        let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
907
908        let center_pane = group(
909            gpui::Axis::Horizontal,
910            vec![
911                group(
912                    gpui::Axis::Vertical,
913                    vec![
914                        SerializedPaneGroup::Pane(SerializedPane::new(
915                            vec![
916                                SerializedItem::new("Terminal", 1, false),
917                                SerializedItem::new("Terminal", 2, true),
918                            ],
919                            false,
920                        )),
921                        SerializedPaneGroup::Pane(SerializedPane::new(
922                            vec![
923                                SerializedItem::new("Terminal", 4, false),
924                                SerializedItem::new("Terminal", 3, true),
925                            ],
926                            true,
927                        )),
928                    ],
929                ),
930                SerializedPaneGroup::Pane(SerializedPane::new(
931                    vec![
932                        SerializedItem::new("Terminal", 5, false),
933                        SerializedItem::new("Terminal", 6, true),
934                    ],
935                    false,
936                )),
937            ],
938        );
939
940        let id = &["/tmp"];
941
942        let mut workspace = default_workspace(id, &center_pane);
943
944        db.save_workspace(workspace.clone()).await;
945
946        workspace.center_group = group(
947            gpui::Axis::Vertical,
948            vec![
949                SerializedPaneGroup::Pane(SerializedPane::new(
950                    vec![
951                        SerializedItem::new("Terminal", 1, false),
952                        SerializedItem::new("Terminal", 2, true),
953                    ],
954                    false,
955                )),
956                SerializedPaneGroup::Pane(SerializedPane::new(
957                    vec![
958                        SerializedItem::new("Terminal", 4, true),
959                        SerializedItem::new("Terminal", 3, false),
960                    ],
961                    true,
962                )),
963            ],
964        );
965
966        db.save_workspace(workspace.clone()).await;
967
968        let new_workspace = db.workspace_for_roots(id).unwrap();
969
970        assert_eq!(workspace.center_group, new_workspace.center_group);
971    }
972}