persistence.rs

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