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
501    use std::sync::Arc;
502
503    use db::open_test_db;
504    use settings::DockAnchor;
505
506    use super::*;
507
508    #[gpui::test]
509    async fn test_next_id_stability() {
510        env_logger::try_init().ok();
511
512        let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
513
514        db.write(|conn| {
515            conn.migrate(
516                "test_table",
517                &[sql!(
518                    CREATE TABLE test_table(
519                        text TEXT,
520                        workspace_id INTEGER,
521                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
522                        ON DELETE CASCADE
523                    ) STRICT;
524                )],
525            )
526            .unwrap();
527        })
528        .await;
529
530        let id = db.next_id().await.unwrap();
531        // Assert the empty row got inserted
532        assert_eq!(
533            Some(id),
534            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
535                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
536            ))
537            .unwrap()(id)
538            .unwrap()
539        );
540
541        db.write(move |conn| {
542            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
543                .unwrap()(("test-text-1", id))
544            .unwrap()
545        })
546        .await;
547
548        let test_text_1 = db
549            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
550            .unwrap()(1)
551        .unwrap()
552        .unwrap();
553        assert_eq!(test_text_1, "test-text-1");
554    }
555
556    #[gpui::test]
557    async fn test_workspace_id_stability() {
558        env_logger::try_init().ok();
559
560        let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
561
562        db.write(|conn| {
563            conn.migrate(
564                "test_table",
565                &[sql!(
566                    CREATE TABLE test_table(
567                        text TEXT,
568                        workspace_id INTEGER,
569                        FOREIGN KEY(workspace_id)
570                            REFERENCES workspaces(workspace_id)
571                        ON DELETE CASCADE
572                    ) STRICT;)],
573            )
574        })
575        .await
576        .unwrap();
577
578        let mut workspace_1 = SerializedWorkspace {
579            id: 1,
580            location: (["/tmp", "/tmp2"]).into(),
581            dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
582            center_group: Default::default(),
583            dock_pane: Default::default(),
584            left_sidebar_open: true,
585            bounds: Default::default(),
586            display: Default::default(),
587        };
588
589        let mut workspace_2 = SerializedWorkspace {
590            id: 2,
591            location: (["/tmp"]).into(),
592            dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
593            center_group: Default::default(),
594            dock_pane: Default::default(),
595            left_sidebar_open: false,
596            bounds: Default::default(),
597            display: Default::default(),
598        };
599
600        db.save_workspace(workspace_1.clone()).await;
601
602        db.write(|conn| {
603            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
604                .unwrap()(("test-text-1", 1))
605            .unwrap();
606        })
607        .await;
608
609        db.save_workspace(workspace_2.clone()).await;
610
611        db.write(|conn| {
612            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
613                .unwrap()(("test-text-2", 2))
614            .unwrap();
615        })
616        .await;
617
618        workspace_1.location = (["/tmp", "/tmp3"]).into();
619        db.save_workspace(workspace_1.clone()).await;
620        db.save_workspace(workspace_1).await;
621
622        workspace_2.dock_pane.children.push(SerializedItem {
623            kind: Arc::from("Test"),
624            item_id: 10,
625            active: true,
626        });
627        db.save_workspace(workspace_2).await;
628
629        let test_text_2 = db
630            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
631            .unwrap()(2)
632        .unwrap()
633        .unwrap();
634        assert_eq!(test_text_2, "test-text-2");
635
636        let test_text_1 = db
637            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
638            .unwrap()(1)
639        .unwrap()
640        .unwrap();
641        assert_eq!(test_text_1, "test-text-1");
642    }
643
644    #[gpui::test]
645    async fn test_full_workspace_serialization() {
646        env_logger::try_init().ok();
647
648        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
649
650        let dock_pane = crate::persistence::model::SerializedPane {
651            children: vec![
652                SerializedItem::new("Terminal", 1, false),
653                SerializedItem::new("Terminal", 2, false),
654                SerializedItem::new("Terminal", 3, true),
655                SerializedItem::new("Terminal", 4, false),
656            ],
657            active: false,
658        };
659
660        //  -----------------
661        //  | 1,2   | 5,6   |
662        //  | - - - |       |
663        //  | 3,4   |       |
664        //  -----------------
665        let center_group = SerializedPaneGroup::Group {
666            axis: gpui::Axis::Horizontal,
667            children: vec![
668                SerializedPaneGroup::Group {
669                    axis: gpui::Axis::Vertical,
670                    children: vec![
671                        SerializedPaneGroup::Pane(SerializedPane::new(
672                            vec![
673                                SerializedItem::new("Terminal", 5, false),
674                                SerializedItem::new("Terminal", 6, true),
675                            ],
676                            false,
677                        )),
678                        SerializedPaneGroup::Pane(SerializedPane::new(
679                            vec![
680                                SerializedItem::new("Terminal", 7, true),
681                                SerializedItem::new("Terminal", 8, false),
682                            ],
683                            false,
684                        )),
685                    ],
686                },
687                SerializedPaneGroup::Pane(SerializedPane::new(
688                    vec![
689                        SerializedItem::new("Terminal", 9, false),
690                        SerializedItem::new("Terminal", 10, true),
691                    ],
692                    false,
693                )),
694            ],
695        };
696
697        let workspace = SerializedWorkspace {
698            id: 5,
699            location: (["/tmp", "/tmp2"]).into(),
700            dock_position: DockPosition::Shown(DockAnchor::Bottom),
701            center_group,
702            dock_pane,
703            left_sidebar_open: true,
704            bounds: Default::default(),
705            display: Default::default(),
706        };
707
708        db.save_workspace(workspace.clone()).await;
709        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
710
711        assert_eq!(workspace, round_trip_workspace.unwrap());
712
713        // Test guaranteed duplicate IDs
714        db.save_workspace(workspace.clone()).await;
715        db.save_workspace(workspace.clone()).await;
716
717        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
718        assert_eq!(workspace, round_trip_workspace.unwrap());
719    }
720
721    #[gpui::test]
722    async fn test_workspace_assignment() {
723        env_logger::try_init().ok();
724
725        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
726
727        let workspace_1 = SerializedWorkspace {
728            id: 1,
729            location: (["/tmp", "/tmp2"]).into(),
730            dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
731            center_group: Default::default(),
732            dock_pane: Default::default(),
733            left_sidebar_open: true,
734            bounds: Default::default(),
735            display: Default::default(),
736        };
737
738        let mut workspace_2 = SerializedWorkspace {
739            id: 2,
740            location: (["/tmp"]).into(),
741            dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
742            center_group: Default::default(),
743            dock_pane: Default::default(),
744            left_sidebar_open: false,
745            bounds: Default::default(),
746            display: Default::default(),
747        };
748
749        db.save_workspace(workspace_1.clone()).await;
750        db.save_workspace(workspace_2.clone()).await;
751
752        // Test that paths are treated as a set
753        assert_eq!(
754            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
755            workspace_1
756        );
757        assert_eq!(
758            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
759            workspace_1
760        );
761
762        // Make sure that other keys work
763        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
764        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
765
766        // Test 'mutate' case of updating a pre-existing id
767        workspace_2.location = (["/tmp", "/tmp2"]).into();
768
769        db.save_workspace(workspace_2.clone()).await;
770        assert_eq!(
771            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
772            workspace_2
773        );
774
775        // Test other mechanism for mutating
776        let mut workspace_3 = SerializedWorkspace {
777            id: 3,
778            location: (&["/tmp", "/tmp2"]).into(),
779            dock_position: DockPosition::Shown(DockAnchor::Right),
780            center_group: Default::default(),
781            dock_pane: Default::default(),
782            left_sidebar_open: false,
783            bounds: Default::default(),
784            display: Default::default(),
785        };
786
787        db.save_workspace(workspace_3.clone()).await;
788        assert_eq!(
789            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
790            workspace_3
791        );
792
793        // Make sure that updating paths differently also works
794        workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
795        db.save_workspace(workspace_3.clone()).await;
796        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
797        assert_eq!(
798            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
799                .unwrap(),
800            workspace_3
801        );
802    }
803
804    use crate::dock::DockPosition;
805    use crate::persistence::model::SerializedWorkspace;
806    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
807
808    fn default_workspace<P: AsRef<Path>>(
809        workspace_id: &[P],
810        dock_pane: SerializedPane,
811        center_group: &SerializedPaneGroup,
812    ) -> SerializedWorkspace {
813        SerializedWorkspace {
814            id: 4,
815            location: workspace_id.into(),
816            dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
817            center_group: center_group.clone(),
818            dock_pane,
819            left_sidebar_open: true,
820            bounds: Default::default(),
821            display: Default::default(),
822        }
823    }
824
825    #[gpui::test]
826    async fn test_basic_dock_pane() {
827        env_logger::try_init().ok();
828
829        let db = WorkspaceDb(open_test_db("basic_dock_pane").await);
830
831        let dock_pane = crate::persistence::model::SerializedPane::new(
832            vec![
833                SerializedItem::new("Terminal", 1, false),
834                SerializedItem::new("Terminal", 4, false),
835                SerializedItem::new("Terminal", 2, false),
836                SerializedItem::new("Terminal", 3, true),
837            ],
838            false,
839        );
840
841        let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default());
842
843        db.save_workspace(workspace.clone()).await;
844
845        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
846
847        assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
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 = SerializedPaneGroup::Group {
862            axis: gpui::Axis::Horizontal,
863            children: vec![
864                SerializedPaneGroup::Group {
865                    axis: gpui::Axis::Vertical,
866                    children: 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"], Default::default(), &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 = SerializedPaneGroup::Group {
909            axis: gpui::Axis::Horizontal,
910            children: vec![
911                SerializedPaneGroup::Group {
912                    axis: gpui::Axis::Vertical,
913                    children: 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, Default::default(), &center_pane);
943
944        db.save_workspace(workspace.clone()).await;
945
946        workspace.center_group = SerializedPaneGroup::Group {
947            axis: gpui::Axis::Vertical,
948            children: 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}