persistence.rs

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