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, WindowId};
   9
  10use remote::ssh_session::SshProjectId;
  11use sqlez::{
  12    bindable::{Bind, Column, StaticColumnCount},
  13    statement::Statement,
  14};
  15
  16use ui::px;
  17use util::{maybe, ResultExt};
  18use uuid::Uuid;
  19
  20use crate::WorkspaceId;
  21
  22use model::{
  23    GroupId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
  24    SerializedSshProject, SerializedWorkspace,
  25};
  26
  27use self::model::{
  28    DockStructure, LocalPathsOrder, SerializedDevServerProject, SerializedWorkspaceLocation,
  29};
  30
  31#[derive(Copy, Clone, Debug, PartialEq)]
  32pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
  33impl sqlez::bindable::StaticColumnCount for SerializedAxis {}
  34impl sqlez::bindable::Bind for SerializedAxis {
  35    fn bind(
  36        &self,
  37        statement: &sqlez::statement::Statement,
  38        start_index: i32,
  39    ) -> anyhow::Result<i32> {
  40        match self.0 {
  41            gpui::Axis::Horizontal => "Horizontal",
  42            gpui::Axis::Vertical => "Vertical",
  43        }
  44        .bind(statement, start_index)
  45    }
  46}
  47
  48impl sqlez::bindable::Column for SerializedAxis {
  49    fn column(
  50        statement: &mut sqlez::statement::Statement,
  51        start_index: i32,
  52    ) -> anyhow::Result<(Self, i32)> {
  53        String::column(statement, start_index).and_then(|(axis_text, next_index)| {
  54            Ok((
  55                match axis_text.as_str() {
  56                    "Horizontal" => Self(Axis::Horizontal),
  57                    "Vertical" => Self(Axis::Vertical),
  58                    _ => anyhow::bail!("Stored serialized item kind is incorrect"),
  59                },
  60                next_index,
  61            ))
  62        })
  63    }
  64}
  65
  66#[derive(Copy, Clone, Debug, PartialEq, Default)]
  67pub(crate) struct SerializedWindowBounds(pub(crate) WindowBounds);
  68
  69impl StaticColumnCount for SerializedWindowBounds {
  70    fn column_count() -> usize {
  71        5
  72    }
  73}
  74
  75impl Bind for SerializedWindowBounds {
  76    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
  77        match self.0 {
  78            WindowBounds::Windowed(bounds) => {
  79                let next_index = statement.bind(&"Windowed", start_index)?;
  80                statement.bind(
  81                    &(
  82                        SerializedPixels(bounds.origin.x),
  83                        SerializedPixels(bounds.origin.y),
  84                        SerializedPixels(bounds.size.width),
  85                        SerializedPixels(bounds.size.height),
  86                    ),
  87                    next_index,
  88                )
  89            }
  90            WindowBounds::Maximized(bounds) => {
  91                let next_index = statement.bind(&"Maximized", start_index)?;
  92                statement.bind(
  93                    &(
  94                        SerializedPixels(bounds.origin.x),
  95                        SerializedPixels(bounds.origin.y),
  96                        SerializedPixels(bounds.size.width),
  97                        SerializedPixels(bounds.size.height),
  98                    ),
  99                    next_index,
 100                )
 101            }
 102            WindowBounds::Fullscreen(bounds) => {
 103                let next_index = statement.bind(&"FullScreen", start_index)?;
 104                statement.bind(
 105                    &(
 106                        SerializedPixels(bounds.origin.x),
 107                        SerializedPixels(bounds.origin.y),
 108                        SerializedPixels(bounds.size.width),
 109                        SerializedPixels(bounds.size.height),
 110                    ),
 111                    next_index,
 112                )
 113            }
 114        }
 115    }
 116}
 117
 118impl Column for SerializedWindowBounds {
 119    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 120        let (window_state, next_index) = String::column(statement, start_index)?;
 121        let ((x, y, width, height), _): ((i32, i32, i32, i32), _) =
 122            Column::column(statement, next_index)?;
 123        let bounds = Bounds {
 124            origin: point(px(x as f32), px(y as f32)),
 125            size: size(px(width as f32), px(height as f32)),
 126        };
 127
 128        let status = match window_state.as_str() {
 129            "Windowed" | "Fixed" => SerializedWindowBounds(WindowBounds::Windowed(bounds)),
 130            "Maximized" => SerializedWindowBounds(WindowBounds::Maximized(bounds)),
 131            "FullScreen" => SerializedWindowBounds(WindowBounds::Fullscreen(bounds)),
 132            _ => bail!("Window State did not have a valid string"),
 133        };
 134
 135        Ok((status, next_index + 4))
 136    }
 137}
 138
 139#[derive(Clone, Debug, PartialEq)]
 140struct SerializedPixels(gpui::Pixels);
 141impl sqlez::bindable::StaticColumnCount for SerializedPixels {}
 142
 143impl sqlez::bindable::Bind for SerializedPixels {
 144    fn bind(
 145        &self,
 146        statement: &sqlez::statement::Statement,
 147        start_index: i32,
 148    ) -> anyhow::Result<i32> {
 149        let this: i32 = self.0 .0 as i32;
 150        this.bind(statement, start_index)
 151    }
 152}
 153
 154define_connection! {
 155    // Current schema shape using pseudo-rust syntax:
 156    //
 157    // workspaces(
 158    //   workspace_id: usize, // Primary key for workspaces
 159    //   local_paths: Bincode<Vec<PathBuf>>,
 160    //   local_paths_order: Bincode<Vec<usize>>,
 161    //   dock_visible: bool, // Deprecated
 162    //   dock_anchor: DockAnchor, // Deprecated
 163    //   dock_pane: Option<usize>, // Deprecated
 164    //   left_sidebar_open: boolean,
 165    //   timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
 166    //   window_state: String, // WindowBounds Discriminant
 167    //   window_x: Option<f32>, // WindowBounds::Fixed RectF x
 168    //   window_y: Option<f32>, // WindowBounds::Fixed RectF y
 169    //   window_width: Option<f32>, // WindowBounds::Fixed RectF width
 170    //   window_height: Option<f32>, // WindowBounds::Fixed RectF height
 171    //   display: Option<Uuid>, // Display id
 172    //   fullscreen: Option<bool>, // Is the window fullscreen?
 173    //   centered_layout: Option<bool>, // Is the Centered Layout mode activated?
 174    //   session_id: Option<String>, // Session id
 175    //   window_id: Option<u64>, // Window Id
 176    // )
 177    //
 178    // pane_groups(
 179    //   group_id: usize, // Primary key for pane_groups
 180    //   workspace_id: usize, // References workspaces table
 181    //   parent_group_id: Option<usize>, // None indicates that this is the root node
 182    //   position: Optiopn<usize>, // None indicates that this is the root node
 183    //   axis: Option<Axis>, // 'Vertical', 'Horizontal'
 184    //   flexes: Option<Vec<f32>>, // A JSON array of floats
 185    // )
 186    //
 187    // panes(
 188    //     pane_id: usize, // Primary key for panes
 189    //     workspace_id: usize, // References workspaces table
 190    //     active: bool,
 191    // )
 192    //
 193    // center_panes(
 194    //     pane_id: usize, // Primary key for center_panes
 195    //     parent_group_id: Option<usize>, // References pane_groups. If none, this is the root
 196    //     position: Option<usize>, // None indicates this is the root
 197    // )
 198    //
 199    // CREATE TABLE items(
 200    //     item_id: usize, // This is the item's view id, so this is not unique
 201    //     workspace_id: usize, // References workspaces table
 202    //     pane_id: usize, // References panes table
 203    //     kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global
 204    //     position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column
 205    //     active: bool, // Indicates if this item is the active one in the pane
 206    //     preview: bool // Indicates if this item is a preview item
 207    // )
 208    pub static ref DB: WorkspaceDb<()> =
 209    &[sql!(
 210        CREATE TABLE workspaces(
 211            workspace_id INTEGER PRIMARY KEY,
 212            workspace_location BLOB UNIQUE,
 213            dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
 214            dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
 215            dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
 216            left_sidebar_open INTEGER, // Boolean
 217            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 218            FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
 219        ) STRICT;
 220
 221        CREATE TABLE pane_groups(
 222            group_id INTEGER PRIMARY KEY,
 223            workspace_id INTEGER NOT NULL,
 224            parent_group_id INTEGER, // NULL indicates that this is a root node
 225            position INTEGER, // NULL indicates that this is a root node
 226            axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
 227            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 228            ON DELETE CASCADE
 229            ON UPDATE CASCADE,
 230            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 231        ) STRICT;
 232
 233        CREATE TABLE panes(
 234            pane_id INTEGER PRIMARY KEY,
 235            workspace_id INTEGER NOT NULL,
 236            active INTEGER NOT NULL, // Boolean
 237            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 238            ON DELETE CASCADE
 239            ON UPDATE CASCADE
 240        ) STRICT;
 241
 242        CREATE TABLE center_panes(
 243            pane_id INTEGER PRIMARY KEY,
 244            parent_group_id INTEGER, // NULL means that this is a root pane
 245            position INTEGER, // NULL means that this is a root pane
 246            FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 247            ON DELETE CASCADE,
 248            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 249        ) STRICT;
 250
 251        CREATE TABLE items(
 252            item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
 253            workspace_id INTEGER NOT NULL,
 254            pane_id INTEGER NOT NULL,
 255            kind TEXT NOT NULL,
 256            position INTEGER NOT NULL,
 257            active INTEGER NOT NULL,
 258            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 259            ON DELETE CASCADE
 260            ON UPDATE CASCADE,
 261            FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 262            ON DELETE CASCADE,
 263            PRIMARY KEY(item_id, workspace_id)
 264        ) STRICT;
 265    ),
 266    sql!(
 267        ALTER TABLE workspaces ADD COLUMN window_state TEXT;
 268        ALTER TABLE workspaces ADD COLUMN window_x REAL;
 269        ALTER TABLE workspaces ADD COLUMN window_y REAL;
 270        ALTER TABLE workspaces ADD COLUMN window_width REAL;
 271        ALTER TABLE workspaces ADD COLUMN window_height REAL;
 272        ALTER TABLE workspaces ADD COLUMN display BLOB;
 273    ),
 274    // Drop foreign key constraint from workspaces.dock_pane to panes table.
 275    sql!(
 276        CREATE TABLE workspaces_2(
 277            workspace_id INTEGER PRIMARY KEY,
 278            workspace_location BLOB UNIQUE,
 279            dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
 280            dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
 281            dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
 282            left_sidebar_open INTEGER, // Boolean
 283            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 284            window_state TEXT,
 285            window_x REAL,
 286            window_y REAL,
 287            window_width REAL,
 288            window_height REAL,
 289            display BLOB
 290        ) STRICT;
 291        INSERT INTO workspaces_2 SELECT * FROM workspaces;
 292        DROP TABLE workspaces;
 293        ALTER TABLE workspaces_2 RENAME TO workspaces;
 294    ),
 295    // Add panels related information
 296    sql!(
 297        ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
 298        ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
 299        ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
 300        ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
 301        ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
 302        ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
 303    ),
 304    // Add panel zoom persistence
 305    sql!(
 306        ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
 307        ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
 308        ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
 309    ),
 310    // Add pane group flex data
 311    sql!(
 312        ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
 313    ),
 314    // Add fullscreen field to workspace
 315    // Deprecated, `WindowBounds` holds the fullscreen state now.
 316    // Preserving so users can downgrade Zed.
 317    sql!(
 318        ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
 319    ),
 320    // Add preview field to items
 321    sql!(
 322        ALTER TABLE items ADD COLUMN preview INTEGER; //bool
 323    ),
 324    // Add centered_layout field to workspace
 325    sql!(
 326        ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
 327    ),
 328    sql!(
 329        CREATE TABLE remote_projects (
 330            remote_project_id INTEGER NOT NULL UNIQUE,
 331            path TEXT,
 332            dev_server_name TEXT
 333        );
 334        ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
 335        ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
 336    ),
 337    sql!(
 338        DROP TABLE remote_projects;
 339        CREATE TABLE dev_server_projects (
 340            id INTEGER NOT NULL UNIQUE,
 341            path TEXT,
 342            dev_server_name TEXT
 343        );
 344        ALTER TABLE workspaces DROP COLUMN remote_project_id;
 345        ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
 346    ),
 347    sql!(
 348        ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
 349    ),
 350    sql!(
 351        ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
 352    ),
 353    sql!(
 354        ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
 355    ),
 356    sql!(
 357        ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
 358    ),
 359    sql!(
 360        CREATE TABLE ssh_projects (
 361            id INTEGER PRIMARY KEY,
 362            host TEXT NOT NULL,
 363            port INTEGER,
 364            path TEXT NOT NULL,
 365            user TEXT
 366        );
 367        ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
 368    ),
 369    sql!(
 370        ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
 371    ),
 372    ];
 373}
 374
 375impl WorkspaceDb {
 376    /// Returns a serialized workspace for the given worktree_roots. If the passed array
 377    /// is empty, the most recent workspace is returned instead. If no workspace for the
 378    /// passed roots is stored, returns none.
 379    pub(crate) fn workspace_for_roots<P: AsRef<Path>>(
 380        &self,
 381        worktree_roots: &[P],
 382    ) -> Option<SerializedWorkspace> {
 383        let local_paths = LocalPaths::new(worktree_roots);
 384
 385        // Note that we re-assign the workspace_id here in case it's empty
 386        // and we've grabbed the most recent workspace
 387        let (
 388            workspace_id,
 389            local_paths,
 390            local_paths_order,
 391            window_bounds,
 392            display,
 393            centered_layout,
 394            docks,
 395            window_id,
 396        ): (
 397            WorkspaceId,
 398            Option<LocalPaths>,
 399            Option<LocalPathsOrder>,
 400            Option<SerializedWindowBounds>,
 401            Option<Uuid>,
 402            Option<bool>,
 403            DockStructure,
 404            Option<u64>,
 405        ) = self
 406            .select_row_bound(sql! {
 407                SELECT
 408                    workspace_id,
 409                    local_paths,
 410                    local_paths_order,
 411                    window_state,
 412                    window_x,
 413                    window_y,
 414                    window_width,
 415                    window_height,
 416                    display,
 417                    centered_layout,
 418                    left_dock_visible,
 419                    left_dock_active_panel,
 420                    left_dock_zoom,
 421                    right_dock_visible,
 422                    right_dock_active_panel,
 423                    right_dock_zoom,
 424                    bottom_dock_visible,
 425                    bottom_dock_active_panel,
 426                    bottom_dock_zoom,
 427                    window_id
 428                FROM workspaces
 429                WHERE local_paths = ?
 430            })
 431            .and_then(|mut prepared_statement| (prepared_statement)(&local_paths))
 432            .context("No workspaces found")
 433            .warn_on_err()
 434            .flatten()?;
 435
 436        let local_paths = local_paths?;
 437        let location = match local_paths_order {
 438            Some(order) => SerializedWorkspaceLocation::Local(local_paths, order),
 439            None => {
 440                let order = LocalPathsOrder::default_for_paths(&local_paths);
 441                SerializedWorkspaceLocation::Local(local_paths, order)
 442            }
 443        };
 444
 445        Some(SerializedWorkspace {
 446            id: workspace_id,
 447            location,
 448            center_group: self
 449                .get_center_pane_group(workspace_id)
 450                .context("Getting center group")
 451                .log_err()?,
 452            window_bounds,
 453            centered_layout: centered_layout.unwrap_or(false),
 454            display,
 455            docks,
 456            session_id: None,
 457            window_id,
 458        })
 459    }
 460
 461    pub(crate) fn workspace_for_dev_server_project(
 462        &self,
 463        dev_server_project_id: DevServerProjectId,
 464    ) -> Option<SerializedWorkspace> {
 465        // Note that we re-assign the workspace_id here in case it's empty
 466        // and we've grabbed the most recent workspace
 467        let (
 468            workspace_id,
 469            dev_server_project_id,
 470            window_bounds,
 471            display,
 472            centered_layout,
 473            docks,
 474            window_id,
 475        ): (
 476            WorkspaceId,
 477            Option<u64>,
 478            Option<SerializedWindowBounds>,
 479            Option<Uuid>,
 480            Option<bool>,
 481            DockStructure,
 482            Option<u64>,
 483        ) = self
 484            .select_row_bound(sql! {
 485                SELECT
 486                    workspace_id,
 487                    dev_server_project_id,
 488                    window_state,
 489                    window_x,
 490                    window_y,
 491                    window_width,
 492                    window_height,
 493                    display,
 494                    centered_layout,
 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                    window_id
 505                FROM workspaces
 506                WHERE dev_server_project_id = ?
 507            })
 508            .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id.0))
 509            .context("No workspaces found")
 510            .warn_on_err()
 511            .flatten()?;
 512
 513        let dev_server_project_id = dev_server_project_id?;
 514
 515        let dev_server_project: SerializedDevServerProject = self
 516            .select_row_bound(sql! {
 517                SELECT id, path, dev_server_name
 518                FROM dev_server_projects
 519                WHERE id = ?
 520            })
 521            .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id))
 522            .context("No remote project found")
 523            .warn_on_err()
 524            .flatten()?;
 525
 526        let location = SerializedWorkspaceLocation::DevServer(dev_server_project);
 527
 528        Some(SerializedWorkspace {
 529            id: workspace_id,
 530            location,
 531            center_group: self
 532                .get_center_pane_group(workspace_id)
 533                .context("Getting center group")
 534                .log_err()?,
 535            window_bounds,
 536            centered_layout: centered_layout.unwrap_or(false),
 537            display,
 538            docks,
 539            session_id: None,
 540            window_id,
 541        })
 542    }
 543
 544    pub(crate) fn workspace_for_ssh_project(
 545        &self,
 546        ssh_project: &SerializedSshProject,
 547    ) -> Option<SerializedWorkspace> {
 548        let (workspace_id, window_bounds, display, centered_layout, docks, window_id): (
 549            WorkspaceId,
 550            Option<SerializedWindowBounds>,
 551            Option<Uuid>,
 552            Option<bool>,
 553            DockStructure,
 554            Option<u64>,
 555        ) = self
 556            .select_row_bound(sql! {
 557                SELECT
 558                    workspace_id,
 559                    window_state,
 560                    window_x,
 561                    window_y,
 562                    window_width,
 563                    window_height,
 564                    display,
 565                    centered_layout,
 566                    left_dock_visible,
 567                    left_dock_active_panel,
 568                    left_dock_zoom,
 569                    right_dock_visible,
 570                    right_dock_active_panel,
 571                    right_dock_zoom,
 572                    bottom_dock_visible,
 573                    bottom_dock_active_panel,
 574                    bottom_dock_zoom,
 575                    window_id
 576                FROM workspaces
 577                WHERE ssh_project_id = ?
 578            })
 579            .and_then(|mut prepared_statement| (prepared_statement)(ssh_project.id.0))
 580            .context("No workspaces found")
 581            .warn_on_err()
 582            .flatten()?;
 583
 584        Some(SerializedWorkspace {
 585            id: workspace_id,
 586            location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()),
 587            center_group: self
 588                .get_center_pane_group(workspace_id)
 589                .context("Getting center group")
 590                .log_err()?,
 591            window_bounds,
 592            centered_layout: centered_layout.unwrap_or(false),
 593            display,
 594            docks,
 595            session_id: None,
 596            window_id,
 597        })
 598    }
 599
 600    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
 601    /// that used this workspace previously
 602    pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
 603        self.write(move |conn| {
 604            conn.with_savepoint("update_worktrees", || {
 605                // Clear out panes and pane_groups
 606                conn.exec_bound(sql!(
 607                    DELETE FROM pane_groups WHERE workspace_id = ?1;
 608                    DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
 609                .context("Clearing old panes")?;
 610
 611                match workspace.location {
 612                    SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => {
 613                        conn.exec_bound(sql!(
 614                            DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ?
 615                        ))?((&local_paths, workspace.id))
 616                        .context("clearing out old locations")?;
 617
 618                        // Upsert
 619                        let query = sql!(
 620                            INSERT INTO workspaces(
 621                                workspace_id,
 622                                local_paths,
 623                                local_paths_order,
 624                                left_dock_visible,
 625                                left_dock_active_panel,
 626                                left_dock_zoom,
 627                                right_dock_visible,
 628                                right_dock_active_panel,
 629                                right_dock_zoom,
 630                                bottom_dock_visible,
 631                                bottom_dock_active_panel,
 632                                bottom_dock_zoom,
 633                                session_id,
 634                                window_id,
 635                                timestamp
 636                            )
 637                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, CURRENT_TIMESTAMP)
 638                            ON CONFLICT DO
 639                            UPDATE SET
 640                                local_paths = ?2,
 641                                local_paths_order = ?3,
 642                                left_dock_visible = ?4,
 643                                left_dock_active_panel = ?5,
 644                                left_dock_zoom = ?6,
 645                                right_dock_visible = ?7,
 646                                right_dock_active_panel = ?8,
 647                                right_dock_zoom = ?9,
 648                                bottom_dock_visible = ?10,
 649                                bottom_dock_active_panel = ?11,
 650                                bottom_dock_zoom = ?12,
 651                                session_id = ?13,
 652                                window_id = ?14,
 653                                timestamp = CURRENT_TIMESTAMP
 654                        );
 655                        let mut prepared_query = conn.exec_bound(query)?;
 656                        let args = (workspace.id, &local_paths, &local_paths_order, workspace.docks, workspace.session_id, workspace.window_id);
 657
 658                        prepared_query(args).context("Updating workspace")?;
 659                    }
 660                    SerializedWorkspaceLocation::DevServer(dev_server_project) => {
 661                        conn.exec_bound(sql!(
 662                            DELETE FROM workspaces WHERE dev_server_project_id = ? AND workspace_id != ?
 663                        ))?((dev_server_project.id.0, workspace.id))
 664                        .context("clearing out old locations")?;
 665
 666                        conn.exec_bound(sql!(
 667                            INSERT INTO dev_server_projects(
 668                                id,
 669                                path,
 670                                dev_server_name
 671                            ) VALUES (?1, ?2, ?3)
 672                            ON CONFLICT DO
 673                            UPDATE SET
 674                                path = ?2,
 675                                dev_server_name = ?3
 676                        ))?(&dev_server_project)?;
 677
 678                        // Upsert
 679                        conn.exec_bound(sql!(
 680                            INSERT INTO workspaces(
 681                                workspace_id,
 682                                dev_server_project_id,
 683                                left_dock_visible,
 684                                left_dock_active_panel,
 685                                left_dock_zoom,
 686                                right_dock_visible,
 687                                right_dock_active_panel,
 688                                right_dock_zoom,
 689                                bottom_dock_visible,
 690                                bottom_dock_active_panel,
 691                                bottom_dock_zoom,
 692                                timestamp
 693                            )
 694                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
 695                            ON CONFLICT DO
 696                            UPDATE SET
 697                                dev_server_project_id = ?2,
 698                                left_dock_visible = ?3,
 699                                left_dock_active_panel = ?4,
 700                                left_dock_zoom = ?5,
 701                                right_dock_visible = ?6,
 702                                right_dock_active_panel = ?7,
 703                                right_dock_zoom = ?8,
 704                                bottom_dock_visible = ?9,
 705                                bottom_dock_active_panel = ?10,
 706                                bottom_dock_zoom = ?11,
 707                                timestamp = CURRENT_TIMESTAMP
 708                        ))?((
 709                            workspace.id,
 710                            dev_server_project.id.0,
 711                            workspace.docks,
 712                        ))
 713                        .context("Updating workspace")?;
 714                    },
 715                    SerializedWorkspaceLocation::Ssh(ssh_project) => {
 716                        conn.exec_bound(sql!(
 717                            DELETE FROM workspaces WHERE ssh_project_id = ? AND workspace_id != ?
 718                        ))?((ssh_project.id.0, workspace.id))
 719                        .context("clearing out old locations")?;
 720
 721                        // Upsert
 722                        conn.exec_bound(sql!(
 723                            INSERT INTO workspaces(
 724                                workspace_id,
 725                                ssh_project_id,
 726                                left_dock_visible,
 727                                left_dock_active_panel,
 728                                left_dock_zoom,
 729                                right_dock_visible,
 730                                right_dock_active_panel,
 731                                right_dock_zoom,
 732                                bottom_dock_visible,
 733                                bottom_dock_active_panel,
 734                                bottom_dock_zoom,
 735                                session_id,
 736                                window_id,
 737                                timestamp
 738                            )
 739                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, CURRENT_TIMESTAMP)
 740                            ON CONFLICT DO
 741                            UPDATE SET
 742                                ssh_project_id = ?2,
 743                                left_dock_visible = ?3,
 744                                left_dock_active_panel = ?4,
 745                                left_dock_zoom = ?5,
 746                                right_dock_visible = ?6,
 747                                right_dock_active_panel = ?7,
 748                                right_dock_zoom = ?8,
 749                                bottom_dock_visible = ?9,
 750                                bottom_dock_active_panel = ?10,
 751                                bottom_dock_zoom = ?11,
 752                                session_id = ?12,
 753                                window_id = ?13,
 754                                timestamp = CURRENT_TIMESTAMP
 755                        ))?((
 756                            workspace.id,
 757                            ssh_project.id.0,
 758                            workspace.docks,
 759                            workspace.session_id,
 760                            workspace.window_id
 761                        ))
 762                        .context("Updating workspace")?;
 763                    }
 764                }
 765
 766                // Save center pane group
 767                Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
 768                    .context("save pane group in save workspace")?;
 769
 770                Ok(())
 771            })
 772            .log_err();
 773        })
 774        .await;
 775    }
 776
 777    pub(crate) async fn get_or_create_ssh_project(
 778        &self,
 779        host: String,
 780        port: Option<u16>,
 781        paths: Vec<String>,
 782        user: Option<String>,
 783    ) -> Result<SerializedSshProject> {
 784        let paths = serde_json::to_string(&paths)?;
 785        if let Some(project) = self
 786            .get_ssh_project(host.clone(), port, paths.clone(), user.clone())
 787            .await?
 788        {
 789            Ok(project)
 790        } else {
 791            self.insert_ssh_project(host, port, paths, user)
 792                .await?
 793                .ok_or_else(|| anyhow!("failed to insert ssh project"))
 794        }
 795    }
 796
 797    query! {
 798        async fn get_ssh_project(host: String, port: Option<u16>, paths: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
 799            SELECT id, host, port, paths, user
 800            FROM ssh_projects
 801            WHERE host IS ? AND port IS ? AND paths IS ? AND user IS ?
 802            LIMIT 1
 803        }
 804    }
 805
 806    query! {
 807        async fn insert_ssh_project(host: String, port: Option<u16>, paths: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
 808            INSERT INTO ssh_projects(
 809                host,
 810                port,
 811                paths,
 812                user
 813            ) VALUES (?1, ?2, ?3, ?4)
 814            RETURNING id, host, port, paths, user
 815        }
 816    }
 817
 818    query! {
 819        pub async fn next_id() -> Result<WorkspaceId> {
 820            INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
 821        }
 822    }
 823
 824    query! {
 825        fn recent_workspaces() -> Result<Vec<(WorkspaceId, LocalPaths, LocalPathsOrder, Option<u64>, Option<u64>)>> {
 826            SELECT workspace_id, local_paths, local_paths_order, dev_server_project_id, ssh_project_id
 827            FROM workspaces
 828            WHERE local_paths IS NOT NULL
 829                OR dev_server_project_id IS NOT NULL
 830                OR ssh_project_id IS NOT NULL
 831            ORDER BY timestamp DESC
 832        }
 833    }
 834
 835    query! {
 836        fn session_workspaces(session_id: String) -> Result<Vec<(LocalPaths, Option<u64>, Option<u64>)>> {
 837            SELECT local_paths, window_id, ssh_project_id
 838            FROM workspaces
 839            WHERE session_id = ?1 AND dev_server_project_id IS NULL
 840            ORDER BY timestamp DESC
 841        }
 842    }
 843
 844    query! {
 845        fn dev_server_projects() -> Result<Vec<SerializedDevServerProject>> {
 846            SELECT id, path, dev_server_name
 847            FROM dev_server_projects
 848        }
 849    }
 850
 851    query! {
 852        fn ssh_projects() -> Result<Vec<SerializedSshProject>> {
 853            SELECT id, host, port, paths, user
 854            FROM ssh_projects
 855        }
 856    }
 857
 858    query! {
 859        fn ssh_project(id: u64) -> Result<SerializedSshProject> {
 860            SELECT id, host, port, paths, user
 861            FROM ssh_projects
 862            WHERE id = ?
 863        }
 864    }
 865
 866    pub(crate) fn last_window(
 867        &self,
 868    ) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
 869        let mut prepared_query =
 870            self.select::<(Option<Uuid>, Option<SerializedWindowBounds>)>(sql!(
 871                SELECT
 872                display,
 873                window_state, window_x, window_y, window_width, window_height
 874                FROM workspaces
 875                WHERE local_paths
 876                IS NOT NULL
 877                ORDER BY timestamp DESC
 878                LIMIT 1
 879            ))?;
 880        let result = prepared_query()?;
 881        Ok(result.into_iter().next().unwrap_or((None, None)))
 882    }
 883
 884    query! {
 885        pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
 886            DELETE FROM workspaces
 887            WHERE workspace_id IS ?
 888        }
 889    }
 890
 891    pub async fn delete_workspace_by_dev_server_project_id(
 892        &self,
 893        id: DevServerProjectId,
 894    ) -> Result<()> {
 895        self.write(move |conn| {
 896            conn.exec_bound(sql!(
 897                DELETE FROM dev_server_projects WHERE id = ?
 898            ))?(id.0)?;
 899            conn.exec_bound(sql!(
 900                DELETE FROM workspaces
 901                WHERE dev_server_project_id IS ?
 902            ))?(id.0)
 903        })
 904        .await
 905    }
 906
 907    // Returns the recent locations which are still valid on disk and deletes ones which no longer
 908    // exist.
 909    pub async fn recent_workspaces_on_disk(
 910        &self,
 911    ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation)>> {
 912        let mut result = Vec::new();
 913        let mut delete_tasks = Vec::new();
 914        let dev_server_projects = self.dev_server_projects()?;
 915        let ssh_projects = self.ssh_projects()?;
 916
 917        for (id, location, order, dev_server_project_id, ssh_project_id) in
 918            self.recent_workspaces()?
 919        {
 920            if let Some(dev_server_project_id) = dev_server_project_id.map(DevServerProjectId) {
 921                if let Some(dev_server_project) = dev_server_projects
 922                    .iter()
 923                    .find(|rp| rp.id == dev_server_project_id)
 924                {
 925                    result.push((id, dev_server_project.clone().into()));
 926                } else {
 927                    delete_tasks.push(self.delete_workspace_by_id(id));
 928                }
 929                continue;
 930            }
 931
 932            if let Some(ssh_project_id) = ssh_project_id.map(SshProjectId) {
 933                if let Some(ssh_project) = ssh_projects.iter().find(|rp| rp.id == ssh_project_id) {
 934                    result.push((id, SerializedWorkspaceLocation::Ssh(ssh_project.clone())));
 935                } else {
 936                    delete_tasks.push(self.delete_workspace_by_id(id));
 937                }
 938                continue;
 939            }
 940
 941            if location.paths().iter().all(|path| path.exists())
 942                && location.paths().iter().any(|path| path.is_dir())
 943            {
 944                result.push((id, SerializedWorkspaceLocation::Local(location, order)));
 945            } else {
 946                delete_tasks.push(self.delete_workspace_by_id(id));
 947            }
 948        }
 949
 950        futures::future::join_all(delete_tasks).await;
 951        Ok(result)
 952    }
 953
 954    pub async fn last_workspace(&self) -> Result<Option<SerializedWorkspaceLocation>> {
 955        Ok(self
 956            .recent_workspaces_on_disk()
 957            .await?
 958            .into_iter()
 959            .next()
 960            .map(|(_, location)| location))
 961    }
 962
 963    // Returns the locations of the workspaces that were still opened when the last
 964    // session was closed (i.e. when Zed was quit).
 965    // If `last_session_window_order` is provided, the returned locations are ordered
 966    // according to that.
 967    pub fn last_session_workspace_locations(
 968        &self,
 969        last_session_id: &str,
 970        last_session_window_stack: Option<Vec<WindowId>>,
 971    ) -> Result<Vec<SerializedWorkspaceLocation>> {
 972        let mut workspaces = Vec::new();
 973
 974        for (location, window_id, ssh_project_id) in
 975            self.session_workspaces(last_session_id.to_owned())?
 976        {
 977            if let Some(ssh_project_id) = ssh_project_id {
 978                let location = SerializedWorkspaceLocation::Ssh(self.ssh_project(ssh_project_id)?);
 979                workspaces.push((location, window_id.map(WindowId::from)));
 980            } else if location.paths().iter().all(|path| path.exists())
 981                && location.paths().iter().any(|path| path.is_dir())
 982            {
 983                let location =
 984                    SerializedWorkspaceLocation::from_local_paths(location.paths().iter());
 985                workspaces.push((location, window_id.map(WindowId::from)));
 986            }
 987        }
 988
 989        if let Some(stack) = last_session_window_stack {
 990            workspaces.sort_by_key(|(_, window_id)| {
 991                window_id
 992                    .and_then(|id| stack.iter().position(|&order_id| order_id == id))
 993                    .unwrap_or(usize::MAX)
 994            });
 995        }
 996
 997        Ok(workspaces
 998            .into_iter()
 999            .map(|(paths, _)| paths)
1000            .collect::<Vec<_>>())
1001    }
1002
1003    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1004        Ok(self
1005            .get_pane_group(workspace_id, None)?
1006            .into_iter()
1007            .next()
1008            .unwrap_or_else(|| {
1009                SerializedPaneGroup::Pane(SerializedPane {
1010                    active: true,
1011                    children: vec![],
1012                    pinned_count: 0,
1013                })
1014            }))
1015    }
1016
1017    fn get_pane_group(
1018        &self,
1019        workspace_id: WorkspaceId,
1020        group_id: Option<GroupId>,
1021    ) -> Result<Vec<SerializedPaneGroup>> {
1022        type GroupKey = (Option<GroupId>, WorkspaceId);
1023        type GroupOrPane = (
1024            Option<GroupId>,
1025            Option<SerializedAxis>,
1026            Option<PaneId>,
1027            Option<bool>,
1028            Option<usize>,
1029            Option<String>,
1030        );
1031        self.select_bound::<GroupKey, GroupOrPane>(sql!(
1032            SELECT group_id, axis, pane_id, active, pinned_count, flexes
1033                FROM (SELECT
1034                        group_id,
1035                        axis,
1036                        NULL as pane_id,
1037                        NULL as active,
1038                        NULL as pinned_count,
1039                        position,
1040                        parent_group_id,
1041                        workspace_id,
1042                        flexes
1043                      FROM pane_groups
1044                    UNION
1045                      SELECT
1046                        NULL,
1047                        NULL,
1048                        center_panes.pane_id,
1049                        panes.active as active,
1050                        pinned_count,
1051                        position,
1052                        parent_group_id,
1053                        panes.workspace_id as workspace_id,
1054                        NULL
1055                      FROM center_panes
1056                      JOIN panes ON center_panes.pane_id = panes.pane_id)
1057                WHERE parent_group_id IS ? AND workspace_id = ?
1058                ORDER BY position
1059        ))?((group_id, workspace_id))?
1060        .into_iter()
1061        .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1062            let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1063            if let Some((group_id, axis)) = group_id.zip(axis) {
1064                let flexes = flexes
1065                    .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1066                    .transpose()?;
1067
1068                Ok(SerializedPaneGroup::Group {
1069                    axis,
1070                    children: self.get_pane_group(workspace_id, Some(group_id))?,
1071                    flexes,
1072                })
1073            } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
1074                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
1075                    self.get_items(pane_id)?,
1076                    active,
1077                    pinned_count,
1078                )))
1079            } else {
1080                bail!("Pane Group Child was neither a pane group or a pane");
1081            }
1082        })
1083        // Filter out panes and pane groups which don't have any children or items
1084        .filter(|pane_group| match pane_group {
1085            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
1086            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
1087            _ => true,
1088        })
1089        .collect::<Result<_>>()
1090    }
1091
1092    fn save_pane_group(
1093        conn: &Connection,
1094        workspace_id: WorkspaceId,
1095        pane_group: &SerializedPaneGroup,
1096        parent: Option<(GroupId, usize)>,
1097    ) -> Result<()> {
1098        match pane_group {
1099            SerializedPaneGroup::Group {
1100                axis,
1101                children,
1102                flexes,
1103            } => {
1104                let (parent_id, position) = parent.unzip();
1105
1106                let flex_string = flexes
1107                    .as_ref()
1108                    .map(|flexes| serde_json::json!(flexes).to_string());
1109
1110                let group_id = conn.select_row_bound::<_, i64>(sql!(
1111                    INSERT INTO pane_groups(
1112                        workspace_id,
1113                        parent_group_id,
1114                        position,
1115                        axis,
1116                        flexes
1117                    )
1118                    VALUES (?, ?, ?, ?, ?)
1119                    RETURNING group_id
1120                ))?((
1121                    workspace_id,
1122                    parent_id,
1123                    position,
1124                    *axis,
1125                    flex_string,
1126                ))?
1127                .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
1128
1129                for (position, group) in children.iter().enumerate() {
1130                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
1131                }
1132
1133                Ok(())
1134            }
1135            SerializedPaneGroup::Pane(pane) => {
1136                Self::save_pane(conn, workspace_id, pane, parent)?;
1137                Ok(())
1138            }
1139        }
1140    }
1141
1142    fn save_pane(
1143        conn: &Connection,
1144        workspace_id: WorkspaceId,
1145        pane: &SerializedPane,
1146        parent: Option<(GroupId, usize)>,
1147    ) -> Result<PaneId> {
1148        let pane_id = conn.select_row_bound::<_, i64>(sql!(
1149            INSERT INTO panes(workspace_id, active, pinned_count)
1150            VALUES (?, ?, ?)
1151            RETURNING pane_id
1152        ))?((workspace_id, pane.active, pane.pinned_count))?
1153        .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
1154
1155        let (parent_id, order) = parent.unzip();
1156        conn.exec_bound(sql!(
1157            INSERT INTO center_panes(pane_id, parent_group_id, position)
1158            VALUES (?, ?, ?)
1159        ))?((pane_id, parent_id, order))?;
1160
1161        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
1162
1163        Ok(pane_id)
1164    }
1165
1166    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
1167        self.select_bound(sql!(
1168            SELECT kind, item_id, active, preview FROM items
1169            WHERE pane_id = ?
1170                ORDER BY position
1171        ))?(pane_id)
1172    }
1173
1174    fn save_items(
1175        conn: &Connection,
1176        workspace_id: WorkspaceId,
1177        pane_id: PaneId,
1178        items: &[SerializedItem],
1179    ) -> Result<()> {
1180        let mut insert = conn.exec_bound(sql!(
1181            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
1182        )).context("Preparing insertion")?;
1183        for (position, item) in items.iter().enumerate() {
1184            insert((workspace_id, pane_id, position, item))?;
1185        }
1186
1187        Ok(())
1188    }
1189
1190    query! {
1191        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
1192            UPDATE workspaces
1193            SET timestamp = CURRENT_TIMESTAMP
1194            WHERE workspace_id = ?
1195        }
1196    }
1197
1198    query! {
1199        pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
1200            UPDATE workspaces
1201            SET window_state = ?2,
1202                window_x = ?3,
1203                window_y = ?4,
1204                window_width = ?5,
1205                window_height = ?6,
1206                display = ?7
1207            WHERE workspace_id = ?1
1208        }
1209    }
1210
1211    query! {
1212        pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
1213            UPDATE workspaces
1214            SET centered_layout = ?2
1215            WHERE workspace_id = ?1
1216        }
1217    }
1218}
1219
1220#[cfg(test)]
1221mod tests {
1222    use super::*;
1223    use crate::persistence::model::SerializedWorkspace;
1224    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
1225    use db::open_test_db;
1226    use gpui::{self};
1227
1228    #[gpui::test]
1229    async fn test_next_id_stability() {
1230        env_logger::try_init().ok();
1231
1232        let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
1233
1234        db.write(|conn| {
1235            conn.migrate(
1236                "test_table",
1237                &[sql!(
1238                    CREATE TABLE test_table(
1239                        text TEXT,
1240                        workspace_id INTEGER,
1241                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1242                        ON DELETE CASCADE
1243                    ) STRICT;
1244                )],
1245            )
1246            .unwrap();
1247        })
1248        .await;
1249
1250        let id = db.next_id().await.unwrap();
1251        // Assert the empty row got inserted
1252        assert_eq!(
1253            Some(id),
1254            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
1255                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
1256            ))
1257            .unwrap()(id)
1258            .unwrap()
1259        );
1260
1261        db.write(move |conn| {
1262            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1263                .unwrap()(("test-text-1", id))
1264            .unwrap()
1265        })
1266        .await;
1267
1268        let test_text_1 = db
1269            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1270            .unwrap()(1)
1271        .unwrap()
1272        .unwrap();
1273        assert_eq!(test_text_1, "test-text-1");
1274    }
1275
1276    #[gpui::test]
1277    async fn test_workspace_id_stability() {
1278        env_logger::try_init().ok();
1279
1280        let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
1281
1282        db.write(|conn| {
1283            conn.migrate(
1284                "test_table",
1285                &[sql!(
1286                        CREATE TABLE test_table(
1287                            text TEXT,
1288                            workspace_id INTEGER,
1289                            FOREIGN KEY(workspace_id)
1290                                REFERENCES workspaces(workspace_id)
1291                            ON DELETE CASCADE
1292                        ) STRICT;)],
1293            )
1294        })
1295        .await
1296        .unwrap();
1297
1298        let mut workspace_1 = SerializedWorkspace {
1299            id: WorkspaceId(1),
1300            location: SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]),
1301            center_group: Default::default(),
1302            window_bounds: Default::default(),
1303            display: Default::default(),
1304            docks: Default::default(),
1305            centered_layout: false,
1306            session_id: None,
1307            window_id: None,
1308        };
1309
1310        let workspace_2 = SerializedWorkspace {
1311            id: WorkspaceId(2),
1312            location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1313            center_group: Default::default(),
1314            window_bounds: Default::default(),
1315            display: Default::default(),
1316            docks: Default::default(),
1317            centered_layout: false,
1318            session_id: None,
1319            window_id: None,
1320        };
1321
1322        db.save_workspace(workspace_1.clone()).await;
1323
1324        db.write(|conn| {
1325            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1326                .unwrap()(("test-text-1", 1))
1327            .unwrap();
1328        })
1329        .await;
1330
1331        db.save_workspace(workspace_2.clone()).await;
1332
1333        db.write(|conn| {
1334            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1335                .unwrap()(("test-text-2", 2))
1336            .unwrap();
1337        })
1338        .await;
1339
1340        workspace_1.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp3"]);
1341        db.save_workspace(workspace_1.clone()).await;
1342        db.save_workspace(workspace_1).await;
1343        db.save_workspace(workspace_2).await;
1344
1345        let test_text_2 = db
1346            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1347            .unwrap()(2)
1348        .unwrap()
1349        .unwrap();
1350        assert_eq!(test_text_2, "test-text-2");
1351
1352        let test_text_1 = db
1353            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1354            .unwrap()(1)
1355        .unwrap()
1356        .unwrap();
1357        assert_eq!(test_text_1, "test-text-1");
1358    }
1359
1360    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
1361        SerializedPaneGroup::Group {
1362            axis: SerializedAxis(axis),
1363            flexes: None,
1364            children,
1365        }
1366    }
1367
1368    #[gpui::test]
1369    async fn test_full_workspace_serialization() {
1370        env_logger::try_init().ok();
1371
1372        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
1373
1374        //  -----------------
1375        //  | 1,2   | 5,6   |
1376        //  | - - - |       |
1377        //  | 3,4   |       |
1378        //  -----------------
1379        let center_group = group(
1380            Axis::Horizontal,
1381            vec![
1382                group(
1383                    Axis::Vertical,
1384                    vec![
1385                        SerializedPaneGroup::Pane(SerializedPane::new(
1386                            vec![
1387                                SerializedItem::new("Terminal", 5, false, false),
1388                                SerializedItem::new("Terminal", 6, true, false),
1389                            ],
1390                            false,
1391                            0,
1392                        )),
1393                        SerializedPaneGroup::Pane(SerializedPane::new(
1394                            vec![
1395                                SerializedItem::new("Terminal", 7, true, false),
1396                                SerializedItem::new("Terminal", 8, false, false),
1397                            ],
1398                            false,
1399                            0,
1400                        )),
1401                    ],
1402                ),
1403                SerializedPaneGroup::Pane(SerializedPane::new(
1404                    vec![
1405                        SerializedItem::new("Terminal", 9, false, false),
1406                        SerializedItem::new("Terminal", 10, true, false),
1407                    ],
1408                    false,
1409                    0,
1410                )),
1411            ],
1412        );
1413
1414        let workspace = SerializedWorkspace {
1415            id: WorkspaceId(5),
1416            location: SerializedWorkspaceLocation::Local(
1417                LocalPaths::new(["/tmp", "/tmp2"]),
1418                LocalPathsOrder::new([1, 0]),
1419            ),
1420            center_group,
1421            window_bounds: Default::default(),
1422            display: Default::default(),
1423            docks: Default::default(),
1424            centered_layout: false,
1425            session_id: None,
1426            window_id: Some(999),
1427        };
1428
1429        db.save_workspace(workspace.clone()).await;
1430
1431        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
1432        assert_eq!(workspace, round_trip_workspace.unwrap());
1433
1434        // Test guaranteed duplicate IDs
1435        db.save_workspace(workspace.clone()).await;
1436        db.save_workspace(workspace.clone()).await;
1437
1438        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
1439        assert_eq!(workspace, round_trip_workspace.unwrap());
1440    }
1441
1442    #[gpui::test]
1443    async fn test_workspace_assignment() {
1444        env_logger::try_init().ok();
1445
1446        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
1447
1448        let workspace_1 = SerializedWorkspace {
1449            id: WorkspaceId(1),
1450            location: SerializedWorkspaceLocation::Local(
1451                LocalPaths::new(["/tmp", "/tmp2"]),
1452                LocalPathsOrder::new([0, 1]),
1453            ),
1454            center_group: Default::default(),
1455            window_bounds: Default::default(),
1456            display: Default::default(),
1457            docks: Default::default(),
1458            centered_layout: false,
1459            session_id: None,
1460            window_id: Some(1),
1461        };
1462
1463        let mut workspace_2 = SerializedWorkspace {
1464            id: WorkspaceId(2),
1465            location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1466            center_group: Default::default(),
1467            window_bounds: Default::default(),
1468            display: Default::default(),
1469            docks: Default::default(),
1470            centered_layout: false,
1471            session_id: None,
1472            window_id: Some(2),
1473        };
1474
1475        db.save_workspace(workspace_1.clone()).await;
1476        db.save_workspace(workspace_2.clone()).await;
1477
1478        // Test that paths are treated as a set
1479        assert_eq!(
1480            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1481            workspace_1
1482        );
1483        assert_eq!(
1484            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
1485            workspace_1
1486        );
1487
1488        // Make sure that other keys work
1489        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
1490        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
1491
1492        // Test 'mutate' case of updating a pre-existing id
1493        workspace_2.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]);
1494
1495        db.save_workspace(workspace_2.clone()).await;
1496        assert_eq!(
1497            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1498            workspace_2
1499        );
1500
1501        // Test other mechanism for mutating
1502        let mut workspace_3 = SerializedWorkspace {
1503            id: WorkspaceId(3),
1504            location: SerializedWorkspaceLocation::Local(
1505                LocalPaths::new(["/tmp", "/tmp2"]),
1506                LocalPathsOrder::new([1, 0]),
1507            ),
1508            center_group: Default::default(),
1509            window_bounds: Default::default(),
1510            display: Default::default(),
1511            docks: Default::default(),
1512            centered_layout: false,
1513            session_id: None,
1514            window_id: Some(3),
1515        };
1516
1517        db.save_workspace(workspace_3.clone()).await;
1518        assert_eq!(
1519            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1520            workspace_3
1521        );
1522
1523        // Make sure that updating paths differently also works
1524        workspace_3.location =
1525            SerializedWorkspaceLocation::from_local_paths(["/tmp3", "/tmp4", "/tmp2"]);
1526        db.save_workspace(workspace_3.clone()).await;
1527        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
1528        assert_eq!(
1529            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
1530                .unwrap(),
1531            workspace_3
1532        );
1533    }
1534
1535    #[gpui::test]
1536    async fn test_session_workspaces() {
1537        env_logger::try_init().ok();
1538
1539        let db = WorkspaceDb(open_test_db("test_serializing_workspaces_session_id").await);
1540
1541        let workspace_1 = SerializedWorkspace {
1542            id: WorkspaceId(1),
1543            location: SerializedWorkspaceLocation::from_local_paths(["/tmp1"]),
1544            center_group: Default::default(),
1545            window_bounds: Default::default(),
1546            display: Default::default(),
1547            docks: Default::default(),
1548            centered_layout: false,
1549            session_id: Some("session-id-1".to_owned()),
1550            window_id: Some(10),
1551        };
1552
1553        let workspace_2 = SerializedWorkspace {
1554            id: WorkspaceId(2),
1555            location: SerializedWorkspaceLocation::from_local_paths(["/tmp2"]),
1556            center_group: Default::default(),
1557            window_bounds: Default::default(),
1558            display: Default::default(),
1559            docks: Default::default(),
1560            centered_layout: false,
1561            session_id: Some("session-id-1".to_owned()),
1562            window_id: Some(20),
1563        };
1564
1565        let workspace_3 = SerializedWorkspace {
1566            id: WorkspaceId(3),
1567            location: SerializedWorkspaceLocation::from_local_paths(["/tmp3"]),
1568            center_group: Default::default(),
1569            window_bounds: Default::default(),
1570            display: Default::default(),
1571            docks: Default::default(),
1572            centered_layout: false,
1573            session_id: Some("session-id-2".to_owned()),
1574            window_id: Some(30),
1575        };
1576
1577        let workspace_4 = SerializedWorkspace {
1578            id: WorkspaceId(4),
1579            location: SerializedWorkspaceLocation::from_local_paths(["/tmp4"]),
1580            center_group: Default::default(),
1581            window_bounds: Default::default(),
1582            display: Default::default(),
1583            docks: Default::default(),
1584            centered_layout: false,
1585            session_id: None,
1586            window_id: None,
1587        };
1588
1589        let ssh_project = db
1590            .get_or_create_ssh_project("my-host".to_string(), Some(1234), vec![], None)
1591            .await
1592            .unwrap();
1593
1594        let workspace_5 = SerializedWorkspace {
1595            id: WorkspaceId(5),
1596            location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()),
1597            center_group: Default::default(),
1598            window_bounds: Default::default(),
1599            display: Default::default(),
1600            docks: Default::default(),
1601            centered_layout: false,
1602            session_id: Some("session-id-2".to_owned()),
1603            window_id: Some(50),
1604        };
1605
1606        db.save_workspace(workspace_1.clone()).await;
1607        db.save_workspace(workspace_2.clone()).await;
1608        db.save_workspace(workspace_3.clone()).await;
1609        db.save_workspace(workspace_4.clone()).await;
1610        db.save_workspace(workspace_5.clone()).await;
1611
1612        let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
1613        assert_eq!(locations.len(), 2);
1614        assert_eq!(locations[0].0, LocalPaths::new(["/tmp1"]));
1615        assert_eq!(locations[0].1, Some(10));
1616        assert_eq!(locations[1].0, LocalPaths::new(["/tmp2"]));
1617        assert_eq!(locations[1].1, Some(20));
1618
1619        let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
1620        assert_eq!(locations.len(), 2);
1621        assert_eq!(locations[0].0, LocalPaths::new(["/tmp3"]));
1622        assert_eq!(locations[0].1, Some(30));
1623        let empty_paths: Vec<&str> = Vec::new();
1624        assert_eq!(locations[1].0, LocalPaths::new(empty_paths.iter()));
1625        assert_eq!(locations[1].1, Some(50));
1626        assert_eq!(locations[1].2, Some(ssh_project.id.0));
1627    }
1628
1629    fn default_workspace<P: AsRef<Path>>(
1630        workspace_id: &[P],
1631        center_group: &SerializedPaneGroup,
1632    ) -> SerializedWorkspace {
1633        SerializedWorkspace {
1634            id: WorkspaceId(4),
1635            location: SerializedWorkspaceLocation::from_local_paths(workspace_id),
1636            center_group: center_group.clone(),
1637            window_bounds: Default::default(),
1638            display: Default::default(),
1639            docks: Default::default(),
1640            centered_layout: false,
1641            session_id: None,
1642            window_id: None,
1643        }
1644    }
1645
1646    #[gpui::test]
1647    async fn test_last_session_workspace_locations() {
1648        let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
1649        let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
1650        let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
1651        let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
1652
1653        let db =
1654            WorkspaceDb(open_test_db("test_serializing_workspaces_last_session_workspaces").await);
1655
1656        let workspaces = [
1657            (1, dir1.path().to_str().unwrap(), 9),
1658            (2, dir2.path().to_str().unwrap(), 5),
1659            (3, dir3.path().to_str().unwrap(), 8),
1660            (4, dir4.path().to_str().unwrap(), 2),
1661        ]
1662        .into_iter()
1663        .map(|(id, location, window_id)| SerializedWorkspace {
1664            id: WorkspaceId(id),
1665            location: SerializedWorkspaceLocation::from_local_paths([location]),
1666            center_group: Default::default(),
1667            window_bounds: Default::default(),
1668            display: Default::default(),
1669            docks: Default::default(),
1670            centered_layout: false,
1671            session_id: Some("one-session".to_owned()),
1672            window_id: Some(window_id),
1673        })
1674        .collect::<Vec<_>>();
1675
1676        for workspace in workspaces.iter() {
1677            db.save_workspace(workspace.clone()).await;
1678        }
1679
1680        let stack = Some(Vec::from([
1681            WindowId::from(2), // Top
1682            WindowId::from(8),
1683            WindowId::from(5),
1684            WindowId::from(9), // Bottom
1685        ]));
1686
1687        let have = db
1688            .last_session_workspace_locations("one-session", stack)
1689            .unwrap();
1690        assert_eq!(have.len(), 4);
1691        assert_eq!(
1692            have[0],
1693            SerializedWorkspaceLocation::from_local_paths(&[dir4.path().to_str().unwrap()])
1694        );
1695        assert_eq!(
1696            have[1],
1697            SerializedWorkspaceLocation::from_local_paths([dir3.path().to_str().unwrap()])
1698        );
1699        assert_eq!(
1700            have[2],
1701            SerializedWorkspaceLocation::from_local_paths([dir2.path().to_str().unwrap()])
1702        );
1703        assert_eq!(
1704            have[3],
1705            SerializedWorkspaceLocation::from_local_paths([dir1.path().to_str().unwrap()])
1706        );
1707    }
1708
1709    #[gpui::test]
1710    async fn test_last_session_workspace_locations_ssh_projects() {
1711        let db = WorkspaceDb(
1712            open_test_db("test_serializing_workspaces_last_session_workspaces_ssh_projects").await,
1713        );
1714
1715        let ssh_projects = [
1716            ("host-1", "my-user-1"),
1717            ("host-2", "my-user-2"),
1718            ("host-3", "my-user-3"),
1719            ("host-4", "my-user-4"),
1720        ]
1721        .into_iter()
1722        .map(|(host, user)| async {
1723            db.get_or_create_ssh_project(host.to_string(), None, vec![], Some(user.to_string()))
1724                .await
1725                .unwrap()
1726        })
1727        .collect::<Vec<_>>();
1728
1729        let ssh_projects = futures::future::join_all(ssh_projects).await;
1730
1731        let workspaces = [
1732            (1, ssh_projects[0].clone(), 9),
1733            (2, ssh_projects[1].clone(), 5),
1734            (3, ssh_projects[2].clone(), 8),
1735            (4, ssh_projects[3].clone(), 2),
1736        ]
1737        .into_iter()
1738        .map(|(id, ssh_project, window_id)| SerializedWorkspace {
1739            id: WorkspaceId(id),
1740            location: SerializedWorkspaceLocation::Ssh(ssh_project),
1741            center_group: Default::default(),
1742            window_bounds: Default::default(),
1743            display: Default::default(),
1744            docks: Default::default(),
1745            centered_layout: false,
1746            session_id: Some("one-session".to_owned()),
1747            window_id: Some(window_id),
1748        })
1749        .collect::<Vec<_>>();
1750
1751        for workspace in workspaces.iter() {
1752            db.save_workspace(workspace.clone()).await;
1753        }
1754
1755        let stack = Some(Vec::from([
1756            WindowId::from(2), // Top
1757            WindowId::from(8),
1758            WindowId::from(5),
1759            WindowId::from(9), // Bottom
1760        ]));
1761
1762        let have = db
1763            .last_session_workspace_locations("one-session", stack)
1764            .unwrap();
1765        assert_eq!(have.len(), 4);
1766        assert_eq!(
1767            have[0],
1768            SerializedWorkspaceLocation::Ssh(ssh_projects[3].clone())
1769        );
1770        assert_eq!(
1771            have[1],
1772            SerializedWorkspaceLocation::Ssh(ssh_projects[2].clone())
1773        );
1774        assert_eq!(
1775            have[2],
1776            SerializedWorkspaceLocation::Ssh(ssh_projects[1].clone())
1777        );
1778        assert_eq!(
1779            have[3],
1780            SerializedWorkspaceLocation::Ssh(ssh_projects[0].clone())
1781        );
1782    }
1783
1784    #[gpui::test]
1785    async fn test_get_or_create_ssh_project() {
1786        let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project").await);
1787
1788        let (host, port, paths, user) = (
1789            "example.com".to_string(),
1790            Some(22_u16),
1791            vec!["/home/user".to_string(), "/etc/nginx".to_string()],
1792            Some("user".to_string()),
1793        );
1794
1795        let project = db
1796            .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
1797            .await
1798            .unwrap();
1799
1800        assert_eq!(project.host, host);
1801        assert_eq!(project.paths, paths);
1802        assert_eq!(project.user, user);
1803
1804        // Test that calling the function again with the same parameters returns the same project
1805        let same_project = db
1806            .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
1807            .await
1808            .unwrap();
1809
1810        assert_eq!(project.id, same_project.id);
1811
1812        // Test with different parameters
1813        let (host2, paths2, user2) = (
1814            "otherexample.com".to_string(),
1815            vec!["/home/otheruser".to_string()],
1816            Some("otheruser".to_string()),
1817        );
1818
1819        let different_project = db
1820            .get_or_create_ssh_project(host2.clone(), None, paths2.clone(), user2.clone())
1821            .await
1822            .unwrap();
1823
1824        assert_ne!(project.id, different_project.id);
1825        assert_eq!(different_project.host, host2);
1826        assert_eq!(different_project.paths, paths2);
1827        assert_eq!(different_project.user, user2);
1828    }
1829
1830    #[gpui::test]
1831    async fn test_get_or_create_ssh_project_with_null_user() {
1832        let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project_with_null_user").await);
1833
1834        let (host, port, paths, user) = (
1835            "example.com".to_string(),
1836            None,
1837            vec!["/home/user".to_string()],
1838            None,
1839        );
1840
1841        let project = db
1842            .get_or_create_ssh_project(host.clone(), port, paths.clone(), None)
1843            .await
1844            .unwrap();
1845
1846        assert_eq!(project.host, host);
1847        assert_eq!(project.paths, paths);
1848        assert_eq!(project.user, None);
1849
1850        // Test that calling the function again with the same parameters returns the same project
1851        let same_project = db
1852            .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
1853            .await
1854            .unwrap();
1855
1856        assert_eq!(project.id, same_project.id);
1857    }
1858
1859    #[gpui::test]
1860    async fn test_get_ssh_projects() {
1861        let db = WorkspaceDb(open_test_db("test_get_ssh_projects").await);
1862
1863        let projects = vec![
1864            (
1865                "example.com".to_string(),
1866                None,
1867                vec!["/home/user".to_string()],
1868                None,
1869            ),
1870            (
1871                "anotherexample.com".to_string(),
1872                Some(123_u16),
1873                vec!["/home/user2".to_string()],
1874                Some("user2".to_string()),
1875            ),
1876            (
1877                "yetanother.com".to_string(),
1878                Some(345_u16),
1879                vec!["/home/user3".to_string(), "/proc/1234/exe".to_string()],
1880                None,
1881            ),
1882        ];
1883
1884        for (host, port, paths, user) in projects.iter() {
1885            let project = db
1886                .get_or_create_ssh_project(host.clone(), *port, paths.clone(), user.clone())
1887                .await
1888                .unwrap();
1889
1890            assert_eq!(&project.host, host);
1891            assert_eq!(&project.port, port);
1892            assert_eq!(&project.paths, paths);
1893            assert_eq!(&project.user, user);
1894        }
1895
1896        let stored_projects = db.ssh_projects().unwrap();
1897        assert_eq!(stored_projects.len(), projects.len());
1898    }
1899
1900    #[gpui::test]
1901    async fn test_simple_split() {
1902        env_logger::try_init().ok();
1903
1904        let db = WorkspaceDb(open_test_db("simple_split").await);
1905
1906        //  -----------------
1907        //  | 1,2   | 5,6   |
1908        //  | - - - |       |
1909        //  | 3,4   |       |
1910        //  -----------------
1911        let center_pane = group(
1912            Axis::Horizontal,
1913            vec![
1914                group(
1915                    Axis::Vertical,
1916                    vec![
1917                        SerializedPaneGroup::Pane(SerializedPane::new(
1918                            vec![
1919                                SerializedItem::new("Terminal", 1, false, false),
1920                                SerializedItem::new("Terminal", 2, true, false),
1921                            ],
1922                            false,
1923                            0,
1924                        )),
1925                        SerializedPaneGroup::Pane(SerializedPane::new(
1926                            vec![
1927                                SerializedItem::new("Terminal", 4, false, false),
1928                                SerializedItem::new("Terminal", 3, true, false),
1929                            ],
1930                            true,
1931                            0,
1932                        )),
1933                    ],
1934                ),
1935                SerializedPaneGroup::Pane(SerializedPane::new(
1936                    vec![
1937                        SerializedItem::new("Terminal", 5, true, false),
1938                        SerializedItem::new("Terminal", 6, false, false),
1939                    ],
1940                    false,
1941                    0,
1942                )),
1943            ],
1944        );
1945
1946        let workspace = default_workspace(&["/tmp"], &center_pane);
1947
1948        db.save_workspace(workspace.clone()).await;
1949
1950        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
1951
1952        assert_eq!(workspace.center_group, new_workspace.center_group);
1953    }
1954
1955    #[gpui::test]
1956    async fn test_cleanup_panes() {
1957        env_logger::try_init().ok();
1958
1959        let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
1960
1961        let center_pane = group(
1962            Axis::Horizontal,
1963            vec![
1964                group(
1965                    Axis::Vertical,
1966                    vec![
1967                        SerializedPaneGroup::Pane(SerializedPane::new(
1968                            vec![
1969                                SerializedItem::new("Terminal", 1, false, false),
1970                                SerializedItem::new("Terminal", 2, true, false),
1971                            ],
1972                            false,
1973                            0,
1974                        )),
1975                        SerializedPaneGroup::Pane(SerializedPane::new(
1976                            vec![
1977                                SerializedItem::new("Terminal", 4, false, false),
1978                                SerializedItem::new("Terminal", 3, true, false),
1979                            ],
1980                            true,
1981                            0,
1982                        )),
1983                    ],
1984                ),
1985                SerializedPaneGroup::Pane(SerializedPane::new(
1986                    vec![
1987                        SerializedItem::new("Terminal", 5, false, false),
1988                        SerializedItem::new("Terminal", 6, true, false),
1989                    ],
1990                    false,
1991                    0,
1992                )),
1993            ],
1994        );
1995
1996        let id = &["/tmp"];
1997
1998        let mut workspace = default_workspace(id, &center_pane);
1999
2000        db.save_workspace(workspace.clone()).await;
2001
2002        workspace.center_group = group(
2003            Axis::Vertical,
2004            vec![
2005                SerializedPaneGroup::Pane(SerializedPane::new(
2006                    vec![
2007                        SerializedItem::new("Terminal", 1, false, false),
2008                        SerializedItem::new("Terminal", 2, true, false),
2009                    ],
2010                    false,
2011                    0,
2012                )),
2013                SerializedPaneGroup::Pane(SerializedPane::new(
2014                    vec![
2015                        SerializedItem::new("Terminal", 4, true, false),
2016                        SerializedItem::new("Terminal", 3, false, false),
2017                    ],
2018                    true,
2019                    0,
2020                )),
2021            ],
2022        );
2023
2024        db.save_workspace(workspace.clone()).await;
2025
2026        let new_workspace = db.workspace_for_roots(id).unwrap();
2027
2028        assert_eq!(workspace.center_group, new_workspace.center_group);
2029    }
2030}