persistence.rs

   1pub mod model;
   2
   3use std::path::Path;
   4
   5use anyhow::{anyhow, bail, Context, Result};
   6use client::DevServerProjectId;
   7use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
   8use gpui::{point, size, Axis, Bounds, WindowBounds};
   9
  10use sqlez::{
  11    bindable::{Bind, Column, StaticColumnCount},
  12    statement::Statement,
  13};
  14
  15use util::ResultExt;
  16use uuid::Uuid;
  17
  18use crate::WorkspaceId;
  19
  20use model::{
  21    GroupId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
  22    SerializedWorkspace,
  23};
  24
  25use self::model::{
  26    DockStructure, LocalPathsOrder, SerializedDevServerProject, SerializedWorkspaceLocation,
  27};
  28
  29#[derive(Copy, Clone, Debug, PartialEq)]
  30pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
  31impl sqlez::bindable::StaticColumnCount for SerializedAxis {}
  32impl sqlez::bindable::Bind for SerializedAxis {
  33    fn bind(
  34        &self,
  35        statement: &sqlez::statement::Statement,
  36        start_index: i32,
  37    ) -> anyhow::Result<i32> {
  38        match self.0 {
  39            gpui::Axis::Horizontal => "Horizontal",
  40            gpui::Axis::Vertical => "Vertical",
  41        }
  42        .bind(statement, start_index)
  43    }
  44}
  45
  46impl sqlez::bindable::Column for SerializedAxis {
  47    fn column(
  48        statement: &mut sqlez::statement::Statement,
  49        start_index: i32,
  50    ) -> anyhow::Result<(Self, i32)> {
  51        String::column(statement, start_index).and_then(|(axis_text, next_index)| {
  52            Ok((
  53                match axis_text.as_str() {
  54                    "Horizontal" => Self(Axis::Horizontal),
  55                    "Vertical" => Self(Axis::Vertical),
  56                    _ => anyhow::bail!("Stored serialized item kind is incorrect"),
  57                },
  58                next_index,
  59            ))
  60        })
  61    }
  62}
  63
  64#[derive(Copy, Clone, Debug, PartialEq, Default)]
  65pub(crate) struct SerializedWindowBounds(pub(crate) WindowBounds);
  66
  67impl StaticColumnCount for SerializedWindowBounds {
  68    fn column_count() -> usize {
  69        5
  70    }
  71}
  72
  73impl Bind for SerializedWindowBounds {
  74    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
  75        match self.0 {
  76            WindowBounds::Windowed(bounds) => {
  77                let next_index = statement.bind(&"Windowed", start_index)?;
  78                statement.bind(
  79                    &(
  80                        SerializedDevicePixels(bounds.origin.x),
  81                        SerializedDevicePixels(bounds.origin.y),
  82                        SerializedDevicePixels(bounds.size.width),
  83                        SerializedDevicePixels(bounds.size.height),
  84                    ),
  85                    next_index,
  86                )
  87            }
  88            WindowBounds::Maximized(bounds) => {
  89                let next_index = statement.bind(&"Maximized", start_index)?;
  90                statement.bind(
  91                    &(
  92                        SerializedDevicePixels(bounds.origin.x),
  93                        SerializedDevicePixels(bounds.origin.y),
  94                        SerializedDevicePixels(bounds.size.width),
  95                        SerializedDevicePixels(bounds.size.height),
  96                    ),
  97                    next_index,
  98                )
  99            }
 100            WindowBounds::Fullscreen(bounds) => {
 101                let next_index = statement.bind(&"FullScreen", start_index)?;
 102                statement.bind(
 103                    &(
 104                        SerializedDevicePixels(bounds.origin.x),
 105                        SerializedDevicePixels(bounds.origin.y),
 106                        SerializedDevicePixels(bounds.size.width),
 107                        SerializedDevicePixels(bounds.size.height),
 108                    ),
 109                    next_index,
 110                )
 111            }
 112        }
 113    }
 114}
 115
 116impl Column for SerializedWindowBounds {
 117    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 118        let (window_state, next_index) = String::column(statement, start_index)?;
 119        let status = match window_state.as_str() {
 120            "Windowed" | "Fixed" => {
 121                let ((x, y, width, height), _) = Column::column(statement, next_index)?;
 122                let x: i32 = x;
 123                let y: i32 = y;
 124                let width: i32 = width;
 125                let height: i32 = height;
 126                SerializedWindowBounds(WindowBounds::Windowed(Bounds {
 127                    origin: point(x.into(), y.into()),
 128                    size: size(width.into(), height.into()),
 129                }))
 130            }
 131            "Maximized" => {
 132                let ((x, y, width, height), _) = Column::column(statement, next_index)?;
 133                let x: i32 = x;
 134                let y: i32 = y;
 135                let width: i32 = width;
 136                let height: i32 = height;
 137                SerializedWindowBounds(WindowBounds::Maximized(Bounds {
 138                    origin: point(x.into(), y.into()),
 139                    size: size(width.into(), height.into()),
 140                }))
 141            }
 142            "FullScreen" => {
 143                let ((x, y, width, height), _) = Column::column(statement, next_index)?;
 144                let x: i32 = x;
 145                let y: i32 = y;
 146                let width: i32 = width;
 147                let height: i32 = height;
 148                SerializedWindowBounds(WindowBounds::Fullscreen(Bounds {
 149                    origin: point(x.into(), y.into()),
 150                    size: size(width.into(), height.into()),
 151                }))
 152            }
 153            _ => bail!("Window State did not have a valid string"),
 154        };
 155
 156        Ok((status, next_index + 4))
 157    }
 158}
 159
 160#[derive(Clone, Debug, PartialEq)]
 161struct SerializedDevicePixels(gpui::DevicePixels);
 162impl sqlez::bindable::StaticColumnCount for SerializedDevicePixels {}
 163
 164impl sqlez::bindable::Bind for SerializedDevicePixels {
 165    fn bind(
 166        &self,
 167        statement: &sqlez::statement::Statement,
 168        start_index: i32,
 169    ) -> anyhow::Result<i32> {
 170        let this: i32 = self.0.into();
 171        this.bind(statement, start_index)
 172    }
 173}
 174
 175define_connection! {
 176    // Current schema shape using pseudo-rust syntax:
 177    //
 178    // workspaces(
 179    //   workspace_id: usize, // Primary key for workspaces
 180    //   local_paths: Bincode<Vec<PathBuf>>,
 181    //   local_paths_order: Bincode<Vec<usize>>,
 182    //   dock_visible: bool, // Deprecated
 183    //   dock_anchor: DockAnchor, // Deprecated
 184    //   dock_pane: Option<usize>, // Deprecated
 185    //   left_sidebar_open: boolean,
 186    //   timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
 187    //   window_state: String, // WindowBounds Discriminant
 188    //   window_x: Option<f32>, // WindowBounds::Fixed RectF x
 189    //   window_y: Option<f32>, // WindowBounds::Fixed RectF y
 190    //   window_width: Option<f32>, // WindowBounds::Fixed RectF width
 191    //   window_height: Option<f32>, // WindowBounds::Fixed RectF height
 192    //   display: Option<Uuid>, // Display id
 193    //   fullscreen: Option<bool>, // Is the window fullscreen?
 194    //   centered_layout: Option<bool>, // Is the Centered Layout mode activated?
 195    // )
 196    //
 197    // pane_groups(
 198    //   group_id: usize, // Primary key for pane_groups
 199    //   workspace_id: usize, // References workspaces table
 200    //   parent_group_id: Option<usize>, // None indicates that this is the root node
 201    //   position: Optiopn<usize>, // None indicates that this is the root node
 202    //   axis: Option<Axis>, // 'Vertical', 'Horizontal'
 203    //   flexes: Option<Vec<f32>>, // A JSON array of floats
 204    // )
 205    //
 206    // panes(
 207    //     pane_id: usize, // Primary key for panes
 208    //     workspace_id: usize, // References workspaces table
 209    //     active: bool,
 210    // )
 211    //
 212    // center_panes(
 213    //     pane_id: usize, // Primary key for center_panes
 214    //     parent_group_id: Option<usize>, // References pane_groups. If none, this is the root
 215    //     position: Option<usize>, // None indicates this is the root
 216    // )
 217    //
 218    // CREATE TABLE items(
 219    //     item_id: usize, // This is the item's view id, so this is not unique
 220    //     workspace_id: usize, // References workspaces table
 221    //     pane_id: usize, // References panes table
 222    //     kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global
 223    //     position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column
 224    //     active: bool, // Indicates if this item is the active one in the pane
 225    //     preview: bool // Indicates if this item is a preview item
 226    // )
 227    pub static ref DB: WorkspaceDb<()> =
 228    &[sql!(
 229        CREATE TABLE workspaces(
 230            workspace_id INTEGER PRIMARY KEY,
 231            workspace_location BLOB UNIQUE,
 232            dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
 233            dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
 234            dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
 235            left_sidebar_open INTEGER, // Boolean
 236            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 237            FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
 238        ) STRICT;
 239
 240        CREATE TABLE pane_groups(
 241            group_id INTEGER PRIMARY KEY,
 242            workspace_id INTEGER NOT NULL,
 243            parent_group_id INTEGER, // NULL indicates that this is a root node
 244            position INTEGER, // NULL indicates that this is a root node
 245            axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
 246            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 247            ON DELETE CASCADE
 248            ON UPDATE CASCADE,
 249            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 250        ) STRICT;
 251
 252        CREATE TABLE panes(
 253            pane_id INTEGER PRIMARY KEY,
 254            workspace_id INTEGER NOT NULL,
 255            active INTEGER NOT NULL, // Boolean
 256            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 257            ON DELETE CASCADE
 258            ON UPDATE CASCADE
 259        ) STRICT;
 260
 261        CREATE TABLE center_panes(
 262            pane_id INTEGER PRIMARY KEY,
 263            parent_group_id INTEGER, // NULL means that this is a root pane
 264            position INTEGER, // NULL means that this is a root pane
 265            FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 266            ON DELETE CASCADE,
 267            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 268        ) STRICT;
 269
 270        CREATE TABLE items(
 271            item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
 272            workspace_id INTEGER NOT NULL,
 273            pane_id INTEGER NOT NULL,
 274            kind TEXT NOT NULL,
 275            position INTEGER NOT NULL,
 276            active INTEGER NOT NULL,
 277            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 278            ON DELETE CASCADE
 279            ON UPDATE CASCADE,
 280            FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 281            ON DELETE CASCADE,
 282            PRIMARY KEY(item_id, workspace_id)
 283        ) STRICT;
 284    ),
 285    sql!(
 286        ALTER TABLE workspaces ADD COLUMN window_state TEXT;
 287        ALTER TABLE workspaces ADD COLUMN window_x REAL;
 288        ALTER TABLE workspaces ADD COLUMN window_y REAL;
 289        ALTER TABLE workspaces ADD COLUMN window_width REAL;
 290        ALTER TABLE workspaces ADD COLUMN window_height REAL;
 291        ALTER TABLE workspaces ADD COLUMN display BLOB;
 292    ),
 293    // Drop foreign key constraint from workspaces.dock_pane to panes table.
 294    sql!(
 295        CREATE TABLE workspaces_2(
 296            workspace_id INTEGER PRIMARY KEY,
 297            workspace_location BLOB UNIQUE,
 298            dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
 299            dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
 300            dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
 301            left_sidebar_open INTEGER, // Boolean
 302            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 303            window_state TEXT,
 304            window_x REAL,
 305            window_y REAL,
 306            window_width REAL,
 307            window_height REAL,
 308            display BLOB
 309        ) STRICT;
 310        INSERT INTO workspaces_2 SELECT * FROM workspaces;
 311        DROP TABLE workspaces;
 312        ALTER TABLE workspaces_2 RENAME TO workspaces;
 313    ),
 314    // Add panels related information
 315    sql!(
 316        ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
 317        ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
 318        ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
 319        ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
 320        ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
 321        ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
 322    ),
 323    // Add panel zoom persistence
 324    sql!(
 325        ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
 326        ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
 327        ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
 328    ),
 329    // Add pane group flex data
 330    sql!(
 331        ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
 332    ),
 333    // Add fullscreen field to workspace
 334    // Deprecated, `WindowBounds` holds the fullscreen state now.
 335    // Preserving so users can downgrade Zed.
 336    sql!(
 337        ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
 338    ),
 339    // Add preview field to items
 340    sql!(
 341        ALTER TABLE items ADD COLUMN preview INTEGER; //bool
 342    ),
 343    // Add centered_layout field to workspace
 344    sql!(
 345        ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
 346    ),
 347    sql!(
 348        CREATE TABLE remote_projects (
 349            remote_project_id INTEGER NOT NULL UNIQUE,
 350            path TEXT,
 351            dev_server_name TEXT
 352        );
 353        ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
 354        ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
 355    ),
 356    sql!(
 357        DROP TABLE remote_projects;
 358        CREATE TABLE dev_server_projects (
 359            id INTEGER NOT NULL UNIQUE,
 360            path TEXT,
 361            dev_server_name TEXT
 362        );
 363        ALTER TABLE workspaces DROP COLUMN remote_project_id;
 364        ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
 365    ),
 366    sql!(
 367        ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
 368    ),
 369    ];
 370}
 371
 372impl WorkspaceDb {
 373    /// Returns a serialized workspace for the given worktree_roots. If the passed array
 374    /// is empty, the most recent workspace is returned instead. If no workspace for the
 375    /// passed roots is stored, returns none.
 376    pub(crate) fn workspace_for_roots<P: AsRef<Path>>(
 377        &self,
 378        worktree_roots: &[P],
 379    ) -> Option<SerializedWorkspace> {
 380        let local_paths = LocalPaths::new(worktree_roots);
 381
 382        // Note that we re-assign the workspace_id here in case it's empty
 383        // and we've grabbed the most recent workspace
 384        let (
 385            workspace_id,
 386            local_paths,
 387            local_paths_order,
 388            dev_server_project_id,
 389            window_bounds,
 390            display,
 391            centered_layout,
 392            docks,
 393        ): (
 394            WorkspaceId,
 395            Option<LocalPaths>,
 396            Option<LocalPathsOrder>,
 397            Option<u64>,
 398            Option<SerializedWindowBounds>,
 399            Option<Uuid>,
 400            Option<bool>,
 401            DockStructure,
 402        ) = self
 403            .select_row_bound(sql! {
 404                SELECT
 405                    workspace_id,
 406                    local_paths,
 407                    local_paths_order,
 408                    dev_server_project_id,
 409                    window_state,
 410                    window_x,
 411                    window_y,
 412                    window_width,
 413                    window_height,
 414                    display,
 415                    centered_layout,
 416                    left_dock_visible,
 417                    left_dock_active_panel,
 418                    left_dock_zoom,
 419                    right_dock_visible,
 420                    right_dock_active_panel,
 421                    right_dock_zoom,
 422                    bottom_dock_visible,
 423                    bottom_dock_active_panel,
 424                    bottom_dock_zoom
 425                FROM workspaces
 426                WHERE local_paths = ?
 427            })
 428            .and_then(|mut prepared_statement| (prepared_statement)(&local_paths))
 429            .context("No workspaces found")
 430            .warn_on_err()
 431            .flatten()?;
 432
 433        let location = if let Some(dev_server_project_id) = dev_server_project_id {
 434            let dev_server_project: SerializedDevServerProject = self
 435                .select_row_bound(sql! {
 436                    SELECT id, path, dev_server_name
 437                    FROM dev_server_projects
 438                    WHERE id = ?
 439                })
 440                .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id))
 441                .context("No remote project found")
 442                .warn_on_err()
 443                .flatten()?;
 444            SerializedWorkspaceLocation::DevServer(dev_server_project)
 445        } else if let Some(local_paths) = local_paths {
 446            match local_paths_order {
 447                Some(order) => SerializedWorkspaceLocation::Local(local_paths, order),
 448                None => {
 449                    let order = LocalPathsOrder::default_for_paths(&local_paths);
 450                    SerializedWorkspaceLocation::Local(local_paths, order)
 451                }
 452            }
 453        } else {
 454            return None;
 455        };
 456
 457        Some(SerializedWorkspace {
 458            id: workspace_id,
 459            location,
 460            center_group: self
 461                .get_center_pane_group(workspace_id)
 462                .context("Getting center group")
 463                .log_err()?,
 464            window_bounds,
 465            centered_layout: centered_layout.unwrap_or(false),
 466            display,
 467            docks,
 468        })
 469    }
 470
 471    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
 472    /// that used this workspace previously
 473    pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
 474        self.write(move |conn| {
 475            conn.with_savepoint("update_worktrees", || {
 476                // Clear out panes and pane_groups
 477                conn.exec_bound(sql!(
 478                    DELETE FROM pane_groups WHERE workspace_id = ?1;
 479                    DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
 480                .context("Clearing old panes")?;
 481
 482                match workspace.location {
 483                    SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => {
 484                        conn.exec_bound(sql!(
 485                            DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ?
 486                        ))?((&local_paths, workspace.id))
 487                        .context("clearing out old locations")?;
 488
 489                        // Upsert
 490                        conn.exec_bound(sql!(
 491                            INSERT INTO workspaces(
 492                                workspace_id,
 493                                local_paths,
 494                                local_paths_order,
 495                                left_dock_visible,
 496                                left_dock_active_panel,
 497                                left_dock_zoom,
 498                                right_dock_visible,
 499                                right_dock_active_panel,
 500                                right_dock_zoom,
 501                                bottom_dock_visible,
 502                                bottom_dock_active_panel,
 503                                bottom_dock_zoom,
 504                                timestamp
 505                            )
 506                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, CURRENT_TIMESTAMP)
 507                            ON CONFLICT DO
 508                            UPDATE SET
 509                                local_paths = ?2,
 510                                local_paths_order = ?3,
 511                                left_dock_visible = ?4,
 512                                left_dock_active_panel = ?5,
 513                                left_dock_zoom = ?6,
 514                                right_dock_visible = ?7,
 515                                right_dock_active_panel = ?8,
 516                                right_dock_zoom = ?9,
 517                                bottom_dock_visible = ?10,
 518                                bottom_dock_active_panel = ?11,
 519                                bottom_dock_zoom = ?12,
 520                                timestamp = CURRENT_TIMESTAMP
 521                        ))?((workspace.id, &local_paths, &local_paths_order, workspace.docks))
 522                        .context("Updating workspace")?;
 523                    }
 524                    SerializedWorkspaceLocation::DevServer(dev_server_project) => {
 525                        conn.exec_bound(sql!(
 526                            DELETE FROM workspaces WHERE dev_server_project_id = ? AND workspace_id != ?
 527                        ))?((dev_server_project.id.0, workspace.id))
 528                        .context("clearing out old locations")?;
 529
 530                        conn.exec_bound(sql!(
 531                            INSERT INTO dev_server_projects(
 532                                id,
 533                                path,
 534                                dev_server_name
 535                            ) VALUES (?1, ?2, ?3)
 536                            ON CONFLICT DO
 537                            UPDATE SET
 538                                path = ?2,
 539                                dev_server_name = ?3
 540                        ))?(&dev_server_project)?;
 541
 542                        // Upsert
 543                        conn.exec_bound(sql!(
 544                            INSERT INTO workspaces(
 545                                workspace_id,
 546                                dev_server_project_id,
 547                                left_dock_visible,
 548                                left_dock_active_panel,
 549                                left_dock_zoom,
 550                                right_dock_visible,
 551                                right_dock_active_panel,
 552                                right_dock_zoom,
 553                                bottom_dock_visible,
 554                                bottom_dock_active_panel,
 555                                bottom_dock_zoom,
 556                                timestamp
 557                            )
 558                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
 559                            ON CONFLICT DO
 560                            UPDATE SET
 561                                dev_server_project_id = ?2,
 562                                left_dock_visible = ?3,
 563                                left_dock_active_panel = ?4,
 564                                left_dock_zoom = ?5,
 565                                right_dock_visible = ?6,
 566                                right_dock_active_panel = ?7,
 567                                right_dock_zoom = ?8,
 568                                bottom_dock_visible = ?9,
 569                                bottom_dock_active_panel = ?10,
 570                                bottom_dock_zoom = ?11,
 571                                timestamp = CURRENT_TIMESTAMP
 572                        ))?((
 573                            workspace.id,
 574                            dev_server_project.id.0,
 575                            workspace.docks,
 576                        ))
 577                        .context("Updating workspace")?;
 578                    }
 579                }
 580
 581                // Save center pane group
 582                Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
 583                    .context("save pane group in save workspace")?;
 584
 585                Ok(())
 586            })
 587            .log_err();
 588        })
 589        .await;
 590    }
 591
 592    query! {
 593        pub async fn next_id() -> Result<WorkspaceId> {
 594            INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
 595        }
 596    }
 597
 598    query! {
 599        fn recent_workspaces() -> Result<Vec<(WorkspaceId, LocalPaths, Option<u64>)>> {
 600            SELECT workspace_id, local_paths, dev_server_project_id
 601            FROM workspaces
 602            WHERE local_paths IS NOT NULL OR dev_server_project_id IS NOT NULL
 603            ORDER BY timestamp DESC
 604        }
 605    }
 606
 607    query! {
 608        fn dev_server_projects() -> Result<Vec<SerializedDevServerProject>> {
 609            SELECT id, path, dev_server_name
 610            FROM dev_server_projects
 611        }
 612    }
 613
 614    pub(crate) fn last_window(
 615        &self,
 616    ) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
 617        let mut prepared_query =
 618            self.select::<(Option<Uuid>, Option<SerializedWindowBounds>)>(sql!(
 619                SELECT
 620                display,
 621                window_state, window_x, window_y, window_width, window_height
 622                FROM workspaces
 623                WHERE local_paths
 624                IS NOT NULL
 625                ORDER BY timestamp DESC
 626                LIMIT 1
 627            ))?;
 628        let result = prepared_query()?;
 629        Ok(result.into_iter().next().unwrap_or_else(|| (None, None)))
 630    }
 631
 632    query! {
 633        pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
 634            DELETE FROM workspaces
 635            WHERE workspace_id IS ?
 636        }
 637    }
 638
 639    pub async fn delete_workspace_by_dev_server_project_id(
 640        &self,
 641        id: DevServerProjectId,
 642    ) -> Result<()> {
 643        self.write(move |conn| {
 644            conn.exec_bound(sql!(
 645                DELETE FROM dev_server_projects WHERE id = ?
 646            ))?(id.0)?;
 647            conn.exec_bound(sql!(
 648                DELETE FROM workspaces
 649                WHERE dev_server_project_id IS ?
 650            ))?(id.0)
 651        })
 652        .await
 653    }
 654
 655    // Returns the recent locations which are still valid on disk and deletes ones which no longer
 656    // exist.
 657    pub async fn recent_workspaces_on_disk(
 658        &self,
 659    ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation)>> {
 660        let mut result = Vec::new();
 661        let mut delete_tasks = Vec::new();
 662        let dev_server_projects = self.dev_server_projects()?;
 663
 664        for (id, location, dev_server_project_id) in self.recent_workspaces()? {
 665            if let Some(dev_server_project_id) = dev_server_project_id.map(DevServerProjectId) {
 666                if let Some(dev_server_project) = dev_server_projects
 667                    .iter()
 668                    .find(|rp| rp.id == dev_server_project_id)
 669                {
 670                    result.push((id, dev_server_project.clone().into()));
 671                } else {
 672                    delete_tasks.push(self.delete_workspace_by_id(id));
 673                }
 674                continue;
 675            }
 676
 677            if location.paths().iter().all(|path| path.exists())
 678                && location.paths().iter().any(|path| path.is_dir())
 679            {
 680                result.push((id, location.into()));
 681            } else {
 682                delete_tasks.push(self.delete_workspace_by_id(id));
 683            }
 684        }
 685
 686        futures::future::join_all(delete_tasks).await;
 687        Ok(result)
 688    }
 689
 690    pub async fn last_workspace(&self) -> Result<Option<LocalPaths>> {
 691        Ok(self
 692            .recent_workspaces_on_disk()
 693            .await?
 694            .into_iter()
 695            .filter_map(|(_, location)| match location {
 696                SerializedWorkspaceLocation::Local(local_paths, _) => Some(local_paths),
 697                SerializedWorkspaceLocation::DevServer(_) => None,
 698            })
 699            .next())
 700    }
 701
 702    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
 703        Ok(self
 704            .get_pane_group(workspace_id, None)?
 705            .into_iter()
 706            .next()
 707            .unwrap_or_else(|| {
 708                SerializedPaneGroup::Pane(SerializedPane {
 709                    active: true,
 710                    children: vec![],
 711                })
 712            }))
 713    }
 714
 715    fn get_pane_group(
 716        &self,
 717        workspace_id: WorkspaceId,
 718        group_id: Option<GroupId>,
 719    ) -> Result<Vec<SerializedPaneGroup>> {
 720        type GroupKey = (Option<GroupId>, WorkspaceId);
 721        type GroupOrPane = (
 722            Option<GroupId>,
 723            Option<SerializedAxis>,
 724            Option<PaneId>,
 725            Option<bool>,
 726            Option<String>,
 727        );
 728        self.select_bound::<GroupKey, GroupOrPane>(sql!(
 729            SELECT group_id, axis, pane_id, active, flexes
 730                FROM (SELECT
 731                        group_id,
 732                        axis,
 733                        NULL as pane_id,
 734                        NULL as active,
 735                        position,
 736                        parent_group_id,
 737                        workspace_id,
 738                        flexes
 739                      FROM pane_groups
 740                    UNION
 741                      SELECT
 742                        NULL,
 743                        NULL,
 744                        center_panes.pane_id,
 745                        panes.active as active,
 746                        position,
 747                        parent_group_id,
 748                        panes.workspace_id as workspace_id,
 749                        NULL
 750                      FROM center_panes
 751                      JOIN panes ON center_panes.pane_id = panes.pane_id)
 752                WHERE parent_group_id IS ? AND workspace_id = ?
 753                ORDER BY position
 754        ))?((group_id, workspace_id))?
 755        .into_iter()
 756        .map(|(group_id, axis, pane_id, active, flexes)| {
 757            if let Some((group_id, axis)) = group_id.zip(axis) {
 758                let flexes = flexes
 759                    .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
 760                    .transpose()?;
 761
 762                Ok(SerializedPaneGroup::Group {
 763                    axis,
 764                    children: self.get_pane_group(workspace_id, Some(group_id))?,
 765                    flexes,
 766                })
 767            } else if let Some((pane_id, active)) = pane_id.zip(active) {
 768                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
 769                    self.get_items(pane_id)?,
 770                    active,
 771                )))
 772            } else {
 773                bail!("Pane Group Child was neither a pane group or a pane");
 774            }
 775        })
 776        // Filter out panes and pane groups which don't have any children or items
 777        .filter(|pane_group| match pane_group {
 778            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
 779            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
 780            _ => true,
 781        })
 782        .collect::<Result<_>>()
 783    }
 784
 785    fn save_pane_group(
 786        conn: &Connection,
 787        workspace_id: WorkspaceId,
 788        pane_group: &SerializedPaneGroup,
 789        parent: Option<(GroupId, usize)>,
 790    ) -> Result<()> {
 791        match pane_group {
 792            SerializedPaneGroup::Group {
 793                axis,
 794                children,
 795                flexes,
 796            } => {
 797                let (parent_id, position) = parent.unzip();
 798
 799                let flex_string = flexes
 800                    .as_ref()
 801                    .map(|flexes| serde_json::json!(flexes).to_string());
 802
 803                let group_id = conn.select_row_bound::<_, i64>(sql!(
 804                    INSERT INTO pane_groups(
 805                        workspace_id,
 806                        parent_group_id,
 807                        position,
 808                        axis,
 809                        flexes
 810                    )
 811                    VALUES (?, ?, ?, ?, ?)
 812                    RETURNING group_id
 813                ))?((
 814                    workspace_id,
 815                    parent_id,
 816                    position,
 817                    *axis,
 818                    flex_string,
 819                ))?
 820                .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
 821
 822                for (position, group) in children.iter().enumerate() {
 823                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
 824                }
 825
 826                Ok(())
 827            }
 828            SerializedPaneGroup::Pane(pane) => {
 829                Self::save_pane(conn, workspace_id, pane, parent)?;
 830                Ok(())
 831            }
 832        }
 833    }
 834
 835    fn save_pane(
 836        conn: &Connection,
 837        workspace_id: WorkspaceId,
 838        pane: &SerializedPane,
 839        parent: Option<(GroupId, usize)>,
 840    ) -> Result<PaneId> {
 841        let pane_id = conn.select_row_bound::<_, i64>(sql!(
 842            INSERT INTO panes(workspace_id, active)
 843            VALUES (?, ?)
 844            RETURNING pane_id
 845        ))?((workspace_id, pane.active))?
 846        .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
 847
 848        let (parent_id, order) = parent.unzip();
 849        conn.exec_bound(sql!(
 850            INSERT INTO center_panes(pane_id, parent_group_id, position)
 851            VALUES (?, ?, ?)
 852        ))?((pane_id, parent_id, order))?;
 853
 854        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
 855
 856        Ok(pane_id)
 857    }
 858
 859    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
 860        self.select_bound(sql!(
 861            SELECT kind, item_id, active, preview FROM items
 862            WHERE pane_id = ?
 863                ORDER BY position
 864        ))?(pane_id)
 865    }
 866
 867    fn save_items(
 868        conn: &Connection,
 869        workspace_id: WorkspaceId,
 870        pane_id: PaneId,
 871        items: &[SerializedItem],
 872    ) -> Result<()> {
 873        let mut insert = conn.exec_bound(sql!(
 874            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
 875        )).context("Preparing insertion")?;
 876        for (position, item) in items.iter().enumerate() {
 877            insert((workspace_id, pane_id, position, item))?;
 878        }
 879
 880        Ok(())
 881    }
 882
 883    query! {
 884        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
 885            UPDATE workspaces
 886            SET timestamp = CURRENT_TIMESTAMP
 887            WHERE workspace_id = ?
 888        }
 889    }
 890
 891    query! {
 892        pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
 893            UPDATE workspaces
 894            SET window_state = ?2,
 895                window_x = ?3,
 896                window_y = ?4,
 897                window_width = ?5,
 898                window_height = ?6,
 899                display = ?7
 900            WHERE workspace_id = ?1
 901        }
 902    }
 903
 904    query! {
 905        pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
 906            UPDATE workspaces
 907            SET centered_layout = ?2
 908            WHERE workspace_id = ?1
 909        }
 910    }
 911}
 912
 913#[cfg(test)]
 914mod tests {
 915    use super::*;
 916    use db::open_test_db;
 917    use gpui;
 918
 919    #[gpui::test]
 920    async fn test_next_id_stability() {
 921        env_logger::try_init().ok();
 922
 923        let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
 924
 925        db.write(|conn| {
 926            conn.migrate(
 927                "test_table",
 928                &[sql!(
 929                    CREATE TABLE test_table(
 930                        text TEXT,
 931                        workspace_id INTEGER,
 932                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 933                        ON DELETE CASCADE
 934                    ) STRICT;
 935                )],
 936            )
 937            .unwrap();
 938        })
 939        .await;
 940
 941        let id = db.next_id().await.unwrap();
 942        // Assert the empty row got inserted
 943        assert_eq!(
 944            Some(id),
 945            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
 946                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
 947            ))
 948            .unwrap()(id)
 949            .unwrap()
 950        );
 951
 952        db.write(move |conn| {
 953            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
 954                .unwrap()(("test-text-1", id))
 955            .unwrap()
 956        })
 957        .await;
 958
 959        let test_text_1 = db
 960            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
 961            .unwrap()(1)
 962        .unwrap()
 963        .unwrap();
 964        assert_eq!(test_text_1, "test-text-1");
 965    }
 966
 967    #[gpui::test]
 968    async fn test_workspace_id_stability() {
 969        env_logger::try_init().ok();
 970
 971        let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
 972
 973        db.write(|conn| {
 974            conn.migrate(
 975                "test_table",
 976                &[sql!(
 977                        CREATE TABLE test_table(
 978                            text TEXT,
 979                            workspace_id INTEGER,
 980                            FOREIGN KEY(workspace_id)
 981                                REFERENCES workspaces(workspace_id)
 982                            ON DELETE CASCADE
 983                        ) STRICT;)],
 984            )
 985        })
 986        .await
 987        .unwrap();
 988
 989        let mut workspace_1 = SerializedWorkspace {
 990            id: WorkspaceId(1),
 991            location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
 992            center_group: Default::default(),
 993            window_bounds: Default::default(),
 994            display: Default::default(),
 995            docks: Default::default(),
 996            centered_layout: false,
 997        };
 998
 999        let workspace_2 = SerializedWorkspace {
1000            id: WorkspaceId(2),
1001            location: LocalPaths::new(["/tmp"]).into(),
1002            center_group: Default::default(),
1003            window_bounds: Default::default(),
1004            display: Default::default(),
1005            docks: Default::default(),
1006            centered_layout: false,
1007        };
1008
1009        db.save_workspace(workspace_1.clone()).await;
1010
1011        db.write(|conn| {
1012            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1013                .unwrap()(("test-text-1", 1))
1014            .unwrap();
1015        })
1016        .await;
1017
1018        db.save_workspace(workspace_2.clone()).await;
1019
1020        db.write(|conn| {
1021            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1022                .unwrap()(("test-text-2", 2))
1023            .unwrap();
1024        })
1025        .await;
1026
1027        workspace_1.location = LocalPaths::new(["/tmp", "/tmp3"]).into();
1028        db.save_workspace(workspace_1.clone()).await;
1029        db.save_workspace(workspace_1).await;
1030        db.save_workspace(workspace_2).await;
1031
1032        let test_text_2 = db
1033            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1034            .unwrap()(2)
1035        .unwrap()
1036        .unwrap();
1037        assert_eq!(test_text_2, "test-text-2");
1038
1039        let test_text_1 = db
1040            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1041            .unwrap()(1)
1042        .unwrap()
1043        .unwrap();
1044        assert_eq!(test_text_1, "test-text-1");
1045    }
1046
1047    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
1048        SerializedPaneGroup::Group {
1049            axis: SerializedAxis(axis),
1050            flexes: None,
1051            children,
1052        }
1053    }
1054
1055    #[gpui::test]
1056    async fn test_full_workspace_serialization() {
1057        env_logger::try_init().ok();
1058
1059        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
1060
1061        //  -----------------
1062        //  | 1,2   | 5,6   |
1063        //  | - - - |       |
1064        //  | 3,4   |       |
1065        //  -----------------
1066        let center_group = group(
1067            Axis::Horizontal,
1068            vec![
1069                group(
1070                    Axis::Vertical,
1071                    vec![
1072                        SerializedPaneGroup::Pane(SerializedPane::new(
1073                            vec![
1074                                SerializedItem::new("Terminal", 5, false, false),
1075                                SerializedItem::new("Terminal", 6, true, false),
1076                            ],
1077                            false,
1078                        )),
1079                        SerializedPaneGroup::Pane(SerializedPane::new(
1080                            vec![
1081                                SerializedItem::new("Terminal", 7, true, false),
1082                                SerializedItem::new("Terminal", 8, false, false),
1083                            ],
1084                            false,
1085                        )),
1086                    ],
1087                ),
1088                SerializedPaneGroup::Pane(SerializedPane::new(
1089                    vec![
1090                        SerializedItem::new("Terminal", 9, false, false),
1091                        SerializedItem::new("Terminal", 10, true, false),
1092                    ],
1093                    false,
1094                )),
1095            ],
1096        );
1097
1098        let workspace = SerializedWorkspace {
1099            id: WorkspaceId(5),
1100            location: SerializedWorkspaceLocation::Local(
1101                LocalPaths::new(["/tmp", "/tmp2"]),
1102                LocalPathsOrder::new([1, 0]),
1103            ),
1104            center_group,
1105            window_bounds: Default::default(),
1106            display: Default::default(),
1107            docks: Default::default(),
1108            centered_layout: false,
1109        };
1110
1111        db.save_workspace(workspace.clone()).await;
1112
1113        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
1114        assert_eq!(workspace, round_trip_workspace.unwrap());
1115
1116        // Test guaranteed duplicate IDs
1117        db.save_workspace(workspace.clone()).await;
1118        db.save_workspace(workspace.clone()).await;
1119
1120        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
1121        assert_eq!(workspace, round_trip_workspace.unwrap());
1122    }
1123
1124    #[gpui::test]
1125    async fn test_workspace_assignment() {
1126        env_logger::try_init().ok();
1127
1128        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
1129
1130        let workspace_1 = SerializedWorkspace {
1131            id: WorkspaceId(1),
1132            location: SerializedWorkspaceLocation::Local(
1133                LocalPaths::new(["/tmp", "/tmp2"]),
1134                LocalPathsOrder::new([0, 1]),
1135            ),
1136            center_group: Default::default(),
1137            window_bounds: Default::default(),
1138            display: Default::default(),
1139            docks: Default::default(),
1140            centered_layout: false,
1141        };
1142
1143        let mut workspace_2 = SerializedWorkspace {
1144            id: WorkspaceId(2),
1145            location: LocalPaths::new(["/tmp"]).into(),
1146            center_group: Default::default(),
1147            window_bounds: Default::default(),
1148            display: Default::default(),
1149            docks: Default::default(),
1150            centered_layout: false,
1151        };
1152
1153        db.save_workspace(workspace_1.clone()).await;
1154        db.save_workspace(workspace_2.clone()).await;
1155
1156        // Test that paths are treated as a set
1157        assert_eq!(
1158            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1159            workspace_1
1160        );
1161        assert_eq!(
1162            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
1163            workspace_1
1164        );
1165
1166        // Make sure that other keys work
1167        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
1168        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
1169
1170        // Test 'mutate' case of updating a pre-existing id
1171        workspace_2.location = LocalPaths::new(["/tmp", "/tmp2"]).into();
1172
1173        db.save_workspace(workspace_2.clone()).await;
1174        assert_eq!(
1175            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1176            workspace_2
1177        );
1178
1179        // Test other mechanism for mutating
1180        let mut workspace_3 = SerializedWorkspace {
1181            id: WorkspaceId(3),
1182            location: SerializedWorkspaceLocation::Local(
1183                LocalPaths::new(&["/tmp", "/tmp2"]),
1184                LocalPathsOrder::new([1, 0]),
1185            ),
1186            center_group: Default::default(),
1187            window_bounds: Default::default(),
1188            display: Default::default(),
1189            docks: Default::default(),
1190            centered_layout: false,
1191        };
1192
1193        db.save_workspace(workspace_3.clone()).await;
1194        assert_eq!(
1195            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1196            workspace_3
1197        );
1198
1199        // Make sure that updating paths differently also works
1200        workspace_3.location = LocalPaths::new(["/tmp3", "/tmp4", "/tmp2"]).into();
1201        db.save_workspace(workspace_3.clone()).await;
1202        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
1203        assert_eq!(
1204            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
1205                .unwrap(),
1206            workspace_3
1207        );
1208    }
1209
1210    use crate::persistence::model::SerializedWorkspace;
1211    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
1212
1213    fn default_workspace<P: AsRef<Path>>(
1214        workspace_id: &[P],
1215        center_group: &SerializedPaneGroup,
1216    ) -> SerializedWorkspace {
1217        SerializedWorkspace {
1218            id: WorkspaceId(4),
1219            location: LocalPaths::new(workspace_id).into(),
1220            center_group: center_group.clone(),
1221            window_bounds: Default::default(),
1222            display: Default::default(),
1223            docks: Default::default(),
1224            centered_layout: false,
1225        }
1226    }
1227
1228    #[gpui::test]
1229    async fn test_simple_split() {
1230        env_logger::try_init().ok();
1231
1232        let db = WorkspaceDb(open_test_db("simple_split").await);
1233
1234        //  -----------------
1235        //  | 1,2   | 5,6   |
1236        //  | - - - |       |
1237        //  | 3,4   |       |
1238        //  -----------------
1239        let center_pane = group(
1240            Axis::Horizontal,
1241            vec![
1242                group(
1243                    Axis::Vertical,
1244                    vec![
1245                        SerializedPaneGroup::Pane(SerializedPane::new(
1246                            vec![
1247                                SerializedItem::new("Terminal", 1, false, false),
1248                                SerializedItem::new("Terminal", 2, true, false),
1249                            ],
1250                            false,
1251                        )),
1252                        SerializedPaneGroup::Pane(SerializedPane::new(
1253                            vec![
1254                                SerializedItem::new("Terminal", 4, false, false),
1255                                SerializedItem::new("Terminal", 3, true, false),
1256                            ],
1257                            true,
1258                        )),
1259                    ],
1260                ),
1261                SerializedPaneGroup::Pane(SerializedPane::new(
1262                    vec![
1263                        SerializedItem::new("Terminal", 5, true, false),
1264                        SerializedItem::new("Terminal", 6, false, false),
1265                    ],
1266                    false,
1267                )),
1268            ],
1269        );
1270
1271        let workspace = default_workspace(&["/tmp"], &center_pane);
1272
1273        db.save_workspace(workspace.clone()).await;
1274
1275        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
1276
1277        assert_eq!(workspace.center_group, new_workspace.center_group);
1278    }
1279
1280    #[gpui::test]
1281    async fn test_cleanup_panes() {
1282        env_logger::try_init().ok();
1283
1284        let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
1285
1286        let center_pane = group(
1287            Axis::Horizontal,
1288            vec![
1289                group(
1290                    Axis::Vertical,
1291                    vec![
1292                        SerializedPaneGroup::Pane(SerializedPane::new(
1293                            vec![
1294                                SerializedItem::new("Terminal", 1, false, false),
1295                                SerializedItem::new("Terminal", 2, true, false),
1296                            ],
1297                            false,
1298                        )),
1299                        SerializedPaneGroup::Pane(SerializedPane::new(
1300                            vec![
1301                                SerializedItem::new("Terminal", 4, false, false),
1302                                SerializedItem::new("Terminal", 3, true, false),
1303                            ],
1304                            true,
1305                        )),
1306                    ],
1307                ),
1308                SerializedPaneGroup::Pane(SerializedPane::new(
1309                    vec![
1310                        SerializedItem::new("Terminal", 5, false, false),
1311                        SerializedItem::new("Terminal", 6, true, false),
1312                    ],
1313                    false,
1314                )),
1315            ],
1316        );
1317
1318        let id = &["/tmp"];
1319
1320        let mut workspace = default_workspace(id, &center_pane);
1321
1322        db.save_workspace(workspace.clone()).await;
1323
1324        workspace.center_group = group(
1325            Axis::Vertical,
1326            vec![
1327                SerializedPaneGroup::Pane(SerializedPane::new(
1328                    vec![
1329                        SerializedItem::new("Terminal", 1, false, false),
1330                        SerializedItem::new("Terminal", 2, true, false),
1331                    ],
1332                    false,
1333                )),
1334                SerializedPaneGroup::Pane(SerializedPane::new(
1335                    vec![
1336                        SerializedItem::new("Terminal", 4, true, false),
1337                        SerializedItem::new("Terminal", 3, false, false),
1338                    ],
1339                    true,
1340                )),
1341            ],
1342        );
1343
1344        db.save_workspace(workspace.clone()).await;
1345
1346        let new_workspace = db.workspace_for_roots(id).unwrap();
1347
1348        assert_eq!(workspace.center_group, new_workspace.center_group);
1349    }
1350}