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 = serde_json::json!(flexes).to_string();
447                let group_id = conn.select_row_bound::<_, i64>(sql!(
448                    INSERT INTO pane_groups(
449                        workspace_id,
450                        parent_group_id,
451                        position,
452                        axis,
453                        flexes
454                    )
455                    VALUES (?, ?, ?, ?, ?)
456                    RETURNING group_id
457                ))?((
458                    workspace_id,
459                    parent_id,
460                    position,
461                    *axis,
462                    flex_string,
463                ))?
464                .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
465
466                for (position, group) in children.iter().enumerate() {
467                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
468                }
469
470                Ok(())
471            }
472            SerializedPaneGroup::Pane(pane) => {
473                Self::save_pane(conn, workspace_id, &pane, parent)?;
474                Ok(())
475            }
476        }
477    }
478
479    fn save_pane(
480        conn: &Connection,
481        workspace_id: WorkspaceId,
482        pane: &SerializedPane,
483        parent: Option<(GroupId, usize)>,
484    ) -> Result<PaneId> {
485        let pane_id = conn.select_row_bound::<_, i64>(sql!(
486            INSERT INTO panes(workspace_id, active)
487            VALUES (?, ?)
488            RETURNING pane_id
489        ))?((workspace_id, pane.active))?
490        .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
491
492        let (parent_id, order) = unzip_option(parent);
493        conn.exec_bound(sql!(
494            INSERT INTO center_panes(pane_id, parent_group_id, position)
495            VALUES (?, ?, ?)
496        ))?((pane_id, parent_id, order))?;
497
498        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
499
500        Ok(pane_id)
501    }
502
503    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
504        Ok(self.select_bound(sql!(
505            SELECT kind, item_id, active FROM items
506            WHERE pane_id = ?
507                ORDER BY position
508        ))?(pane_id)?)
509    }
510
511    fn save_items(
512        conn: &Connection,
513        workspace_id: WorkspaceId,
514        pane_id: PaneId,
515        items: &[SerializedItem],
516    ) -> Result<()> {
517        let mut insert = conn.exec_bound(sql!(
518            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?)
519        )).context("Preparing insertion")?;
520        for (position, item) in items.iter().enumerate() {
521            insert((workspace_id, pane_id, position, item))?;
522        }
523
524        Ok(())
525    }
526
527    query! {
528        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
529            UPDATE workspaces
530            SET timestamp = CURRENT_TIMESTAMP
531            WHERE workspace_id = ?
532        }
533    }
534
535    query! {
536        pub async fn set_window_bounds(workspace_id: WorkspaceId, bounds: WindowBounds, display: Uuid) -> Result<()> {
537            UPDATE workspaces
538            SET window_state = ?2,
539                window_x = ?3,
540                window_y = ?4,
541                window_width = ?5,
542                window_height = ?6,
543                display = ?7
544            WHERE workspace_id = ?1
545        }
546    }
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552    use db::open_test_db;
553
554    #[gpui::test]
555    async fn test_next_id_stability() {
556        env_logger::try_init().ok();
557
558        let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
559
560        db.write(|conn| {
561            conn.migrate(
562                "test_table",
563                &[sql!(
564                    CREATE TABLE test_table(
565                        text TEXT,
566                        workspace_id INTEGER,
567                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
568                        ON DELETE CASCADE
569                    ) STRICT;
570                )],
571            )
572            .unwrap();
573        })
574        .await;
575
576        let id = db.next_id().await.unwrap();
577        // Assert the empty row got inserted
578        assert_eq!(
579            Some(id),
580            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
581                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
582            ))
583            .unwrap()(id)
584            .unwrap()
585        );
586
587        db.write(move |conn| {
588            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
589                .unwrap()(("test-text-1", id))
590            .unwrap()
591        })
592        .await;
593
594        let test_text_1 = db
595            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
596            .unwrap()(1)
597        .unwrap()
598        .unwrap();
599        assert_eq!(test_text_1, "test-text-1");
600    }
601
602    #[gpui::test]
603    async fn test_workspace_id_stability() {
604        env_logger::try_init().ok();
605
606        let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
607
608        db.write(|conn| {
609            conn.migrate(
610                "test_table",
611                &[sql!(
612                    CREATE TABLE test_table(
613                        text TEXT,
614                        workspace_id INTEGER,
615                        FOREIGN KEY(workspace_id)
616                            REFERENCES workspaces(workspace_id)
617                        ON DELETE CASCADE
618                    ) STRICT;)],
619            )
620        })
621        .await
622        .unwrap();
623
624        let mut workspace_1 = SerializedWorkspace {
625            id: 1,
626            location: (["/tmp", "/tmp2"]).into(),
627            center_group: Default::default(),
628            bounds: Default::default(),
629            display: Default::default(),
630            docks: Default::default(),
631        };
632
633        let workspace_2 = SerializedWorkspace {
634            id: 2,
635            location: (["/tmp"]).into(),
636            center_group: Default::default(),
637            bounds: Default::default(),
638            display: Default::default(),
639            docks: Default::default(),
640        };
641
642        db.save_workspace(workspace_1.clone()).await;
643
644        db.write(|conn| {
645            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
646                .unwrap()(("test-text-1", 1))
647            .unwrap();
648        })
649        .await;
650
651        db.save_workspace(workspace_2.clone()).await;
652
653        db.write(|conn| {
654            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
655                .unwrap()(("test-text-2", 2))
656            .unwrap();
657        })
658        .await;
659
660        workspace_1.location = (["/tmp", "/tmp3"]).into();
661        db.save_workspace(workspace_1.clone()).await;
662        db.save_workspace(workspace_1).await;
663        db.save_workspace(workspace_2).await;
664
665        let test_text_2 = db
666            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
667            .unwrap()(2)
668        .unwrap()
669        .unwrap();
670        assert_eq!(test_text_2, "test-text-2");
671
672        let test_text_1 = db
673            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
674            .unwrap()(1)
675        .unwrap()
676        .unwrap();
677        assert_eq!(test_text_1, "test-text-1");
678    }
679
680    fn group(axis: gpui::Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
681        SerializedPaneGroup::Group {
682            axis,
683            flexes: None,
684            children,
685        }
686    }
687
688    #[gpui::test]
689    async fn test_full_workspace_serialization() {
690        env_logger::try_init().ok();
691
692        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
693
694        //  -----------------
695        //  | 1,2   | 5,6   |
696        //  | - - - |       |
697        //  | 3,4   |       |
698        //  -----------------
699        let center_group = group(
700            gpui::Axis::Horizontal,
701            vec![
702                group(
703                    gpui::Axis::Vertical,
704                    vec![
705                        SerializedPaneGroup::Pane(SerializedPane::new(
706                            vec![
707                                SerializedItem::new("Terminal", 5, false),
708                                SerializedItem::new("Terminal", 6, true),
709                            ],
710                            false,
711                        )),
712                        SerializedPaneGroup::Pane(SerializedPane::new(
713                            vec![
714                                SerializedItem::new("Terminal", 7, true),
715                                SerializedItem::new("Terminal", 8, false),
716                            ],
717                            false,
718                        )),
719                    ],
720                ),
721                SerializedPaneGroup::Pane(SerializedPane::new(
722                    vec![
723                        SerializedItem::new("Terminal", 9, false),
724                        SerializedItem::new("Terminal", 10, true),
725                    ],
726                    false,
727                )),
728            ],
729        );
730
731        let workspace = SerializedWorkspace {
732            id: 5,
733            location: (["/tmp", "/tmp2"]).into(),
734            center_group,
735            bounds: Default::default(),
736            display: Default::default(),
737            docks: Default::default(),
738        };
739
740        db.save_workspace(workspace.clone()).await;
741        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
742
743        assert_eq!(workspace, round_trip_workspace.unwrap());
744
745        // Test guaranteed duplicate IDs
746        db.save_workspace(workspace.clone()).await;
747        db.save_workspace(workspace.clone()).await;
748
749        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
750        assert_eq!(workspace, round_trip_workspace.unwrap());
751    }
752
753    #[gpui::test]
754    async fn test_workspace_assignment() {
755        env_logger::try_init().ok();
756
757        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
758
759        let workspace_1 = SerializedWorkspace {
760            id: 1,
761            location: (["/tmp", "/tmp2"]).into(),
762            center_group: Default::default(),
763            bounds: Default::default(),
764            display: Default::default(),
765            docks: Default::default(),
766        };
767
768        let mut workspace_2 = SerializedWorkspace {
769            id: 2,
770            location: (["/tmp"]).into(),
771            center_group: Default::default(),
772            bounds: Default::default(),
773            display: Default::default(),
774            docks: Default::default(),
775        };
776
777        db.save_workspace(workspace_1.clone()).await;
778        db.save_workspace(workspace_2.clone()).await;
779
780        // Test that paths are treated as a set
781        assert_eq!(
782            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
783            workspace_1
784        );
785        assert_eq!(
786            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
787            workspace_1
788        );
789
790        // Make sure that other keys work
791        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
792        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
793
794        // Test 'mutate' case of updating a pre-existing id
795        workspace_2.location = (["/tmp", "/tmp2"]).into();
796
797        db.save_workspace(workspace_2.clone()).await;
798        assert_eq!(
799            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
800            workspace_2
801        );
802
803        // Test other mechanism for mutating
804        let mut workspace_3 = SerializedWorkspace {
805            id: 3,
806            location: (&["/tmp", "/tmp2"]).into(),
807            center_group: Default::default(),
808            bounds: Default::default(),
809            display: Default::default(),
810            docks: Default::default(),
811        };
812
813        db.save_workspace(workspace_3.clone()).await;
814        assert_eq!(
815            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
816            workspace_3
817        );
818
819        // Make sure that updating paths differently also works
820        workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
821        db.save_workspace(workspace_3.clone()).await;
822        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
823        assert_eq!(
824            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
825                .unwrap(),
826            workspace_3
827        );
828    }
829
830    use crate::persistence::model::SerializedWorkspace;
831    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
832
833    fn default_workspace<P: AsRef<Path>>(
834        workspace_id: &[P],
835        center_group: &SerializedPaneGroup,
836    ) -> SerializedWorkspace {
837        SerializedWorkspace {
838            id: 4,
839            location: workspace_id.into(),
840            center_group: center_group.clone(),
841            bounds: Default::default(),
842            display: Default::default(),
843            docks: Default::default(),
844        }
845    }
846
847    #[gpui::test]
848    async fn test_simple_split() {
849        env_logger::try_init().ok();
850
851        let db = WorkspaceDb(open_test_db("simple_split").await);
852
853        //  -----------------
854        //  | 1,2   | 5,6   |
855        //  | - - - |       |
856        //  | 3,4   |       |
857        //  -----------------
858        let center_pane = group(
859            gpui::Axis::Horizontal,
860            vec![
861                group(
862                    gpui::Axis::Vertical,
863                    vec![
864                        SerializedPaneGroup::Pane(SerializedPane::new(
865                            vec![
866                                SerializedItem::new("Terminal", 1, false),
867                                SerializedItem::new("Terminal", 2, true),
868                            ],
869                            false,
870                        )),
871                        SerializedPaneGroup::Pane(SerializedPane::new(
872                            vec![
873                                SerializedItem::new("Terminal", 4, false),
874                                SerializedItem::new("Terminal", 3, true),
875                            ],
876                            true,
877                        )),
878                    ],
879                ),
880                SerializedPaneGroup::Pane(SerializedPane::new(
881                    vec![
882                        SerializedItem::new("Terminal", 5, true),
883                        SerializedItem::new("Terminal", 6, false),
884                    ],
885                    false,
886                )),
887            ],
888        );
889
890        let workspace = default_workspace(&["/tmp"], &center_pane);
891
892        db.save_workspace(workspace.clone()).await;
893
894        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
895
896        assert_eq!(workspace.center_group, new_workspace.center_group);
897    }
898
899    #[gpui::test]
900    async fn test_cleanup_panes() {
901        env_logger::try_init().ok();
902
903        let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
904
905        let center_pane = group(
906            gpui::Axis::Horizontal,
907            vec![
908                group(
909                    gpui::Axis::Vertical,
910                    vec![
911                        SerializedPaneGroup::Pane(SerializedPane::new(
912                            vec![
913                                SerializedItem::new("Terminal", 1, false),
914                                SerializedItem::new("Terminal", 2, true),
915                            ],
916                            false,
917                        )),
918                        SerializedPaneGroup::Pane(SerializedPane::new(
919                            vec![
920                                SerializedItem::new("Terminal", 4, false),
921                                SerializedItem::new("Terminal", 3, true),
922                            ],
923                            true,
924                        )),
925                    ],
926                ),
927                SerializedPaneGroup::Pane(SerializedPane::new(
928                    vec![
929                        SerializedItem::new("Terminal", 5, false),
930                        SerializedItem::new("Terminal", 6, true),
931                    ],
932                    false,
933                )),
934            ],
935        );
936
937        let id = &["/tmp"];
938
939        let mut workspace = default_workspace(id, &center_pane);
940
941        db.save_workspace(workspace.clone()).await;
942
943        workspace.center_group = group(
944            gpui::Axis::Vertical,
945            vec![
946                SerializedPaneGroup::Pane(SerializedPane::new(
947                    vec![
948                        SerializedItem::new("Terminal", 1, false),
949                        SerializedItem::new("Terminal", 2, true),
950                    ],
951                    false,
952                )),
953                SerializedPaneGroup::Pane(SerializedPane::new(
954                    vec![
955                        SerializedItem::new("Terminal", 4, true),
956                        SerializedItem::new("Terminal", 3, false),
957                    ],
958                    true,
959                )),
960            ],
961        );
962
963        db.save_workspace(workspace.clone()).await;
964
965        let new_workspace = db.workspace_for_roots(id).unwrap();
966
967        assert_eq!(workspace.center_group, new_workspace.center_group);
968    }
969}