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