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