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 std::thread;
1160    use std::time::Duration;
1161
1162    use super::*;
1163    use crate::persistence::model::SerializedWorkspace;
1164    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
1165    use db::open_test_db;
1166    use gpui;
1167
1168    #[gpui::test]
1169    async fn test_next_id_stability() {
1170        env_logger::try_init().ok();
1171
1172        let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
1173
1174        db.write(|conn| {
1175            conn.migrate(
1176                "test_table",
1177                &[sql!(
1178                    CREATE TABLE test_table(
1179                        text TEXT,
1180                        workspace_id INTEGER,
1181                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1182                        ON DELETE CASCADE
1183                    ) STRICT;
1184                )],
1185            )
1186            .unwrap();
1187        })
1188        .await;
1189
1190        let id = db.next_id().await.unwrap();
1191        // Assert the empty row got inserted
1192        assert_eq!(
1193            Some(id),
1194            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
1195                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
1196            ))
1197            .unwrap()(id)
1198            .unwrap()
1199        );
1200
1201        db.write(move |conn| {
1202            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1203                .unwrap()(("test-text-1", id))
1204            .unwrap()
1205        })
1206        .await;
1207
1208        let test_text_1 = db
1209            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1210            .unwrap()(1)
1211        .unwrap()
1212        .unwrap();
1213        assert_eq!(test_text_1, "test-text-1");
1214    }
1215
1216    #[gpui::test]
1217    async fn test_workspace_id_stability() {
1218        env_logger::try_init().ok();
1219
1220        let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
1221
1222        db.write(|conn| {
1223            conn.migrate(
1224                "test_table",
1225                &[sql!(
1226                        CREATE TABLE test_table(
1227                            text TEXT,
1228                            workspace_id INTEGER,
1229                            FOREIGN KEY(workspace_id)
1230                                REFERENCES workspaces(workspace_id)
1231                            ON DELETE CASCADE
1232                        ) STRICT;)],
1233            )
1234        })
1235        .await
1236        .unwrap();
1237
1238        let mut workspace_1 = SerializedWorkspace {
1239            id: WorkspaceId(1),
1240            location: SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]),
1241            center_group: Default::default(),
1242            window_bounds: Default::default(),
1243            display: Default::default(),
1244            docks: Default::default(),
1245            centered_layout: false,
1246            session_id: None,
1247            window_id: None,
1248        };
1249
1250        let workspace_2 = SerializedWorkspace {
1251            id: WorkspaceId(2),
1252            location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1253            center_group: Default::default(),
1254            window_bounds: Default::default(),
1255            display: Default::default(),
1256            docks: Default::default(),
1257            centered_layout: false,
1258            session_id: None,
1259            window_id: None,
1260        };
1261
1262        db.save_workspace(workspace_1.clone()).await;
1263
1264        db.write(|conn| {
1265            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1266                .unwrap()(("test-text-1", 1))
1267            .unwrap();
1268        })
1269        .await;
1270
1271        db.save_workspace(workspace_2.clone()).await;
1272
1273        db.write(|conn| {
1274            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1275                .unwrap()(("test-text-2", 2))
1276            .unwrap();
1277        })
1278        .await;
1279
1280        workspace_1.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp3"]);
1281        db.save_workspace(workspace_1.clone()).await;
1282        db.save_workspace(workspace_1).await;
1283        db.save_workspace(workspace_2).await;
1284
1285        let test_text_2 = db
1286            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1287            .unwrap()(2)
1288        .unwrap()
1289        .unwrap();
1290        assert_eq!(test_text_2, "test-text-2");
1291
1292        let test_text_1 = db
1293            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1294            .unwrap()(1)
1295        .unwrap()
1296        .unwrap();
1297        assert_eq!(test_text_1, "test-text-1");
1298    }
1299
1300    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
1301        SerializedPaneGroup::Group {
1302            axis: SerializedAxis(axis),
1303            flexes: None,
1304            children,
1305        }
1306    }
1307
1308    #[gpui::test]
1309    async fn test_full_workspace_serialization() {
1310        env_logger::try_init().ok();
1311
1312        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
1313
1314        //  -----------------
1315        //  | 1,2   | 5,6   |
1316        //  | - - - |       |
1317        //  | 3,4   |       |
1318        //  -----------------
1319        let center_group = group(
1320            Axis::Horizontal,
1321            vec![
1322                group(
1323                    Axis::Vertical,
1324                    vec![
1325                        SerializedPaneGroup::Pane(SerializedPane::new(
1326                            vec![
1327                                SerializedItem::new("Terminal", 5, false, false),
1328                                SerializedItem::new("Terminal", 6, true, false),
1329                            ],
1330                            false,
1331                            0,
1332                        )),
1333                        SerializedPaneGroup::Pane(SerializedPane::new(
1334                            vec![
1335                                SerializedItem::new("Terminal", 7, true, false),
1336                                SerializedItem::new("Terminal", 8, false, false),
1337                            ],
1338                            false,
1339                            0,
1340                        )),
1341                    ],
1342                ),
1343                SerializedPaneGroup::Pane(SerializedPane::new(
1344                    vec![
1345                        SerializedItem::new("Terminal", 9, false, false),
1346                        SerializedItem::new("Terminal", 10, true, false),
1347                    ],
1348                    false,
1349                    0,
1350                )),
1351            ],
1352        );
1353
1354        let workspace = SerializedWorkspace {
1355            id: WorkspaceId(5),
1356            location: SerializedWorkspaceLocation::Local(
1357                LocalPaths::new(["/tmp", "/tmp2"]),
1358                LocalPathsOrder::new([1, 0]),
1359            ),
1360            center_group,
1361            window_bounds: Default::default(),
1362            display: Default::default(),
1363            docks: Default::default(),
1364            centered_layout: false,
1365            session_id: None,
1366            window_id: Some(999),
1367        };
1368
1369        db.save_workspace(workspace.clone()).await;
1370
1371        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
1372        assert_eq!(workspace, round_trip_workspace.unwrap());
1373
1374        // Test guaranteed duplicate IDs
1375        db.save_workspace(workspace.clone()).await;
1376        db.save_workspace(workspace.clone()).await;
1377
1378        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
1379        assert_eq!(workspace, round_trip_workspace.unwrap());
1380    }
1381
1382    #[gpui::test]
1383    async fn test_workspace_assignment() {
1384        env_logger::try_init().ok();
1385
1386        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
1387
1388        let workspace_1 = SerializedWorkspace {
1389            id: WorkspaceId(1),
1390            location: SerializedWorkspaceLocation::Local(
1391                LocalPaths::new(["/tmp", "/tmp2"]),
1392                LocalPathsOrder::new([0, 1]),
1393            ),
1394            center_group: Default::default(),
1395            window_bounds: Default::default(),
1396            display: Default::default(),
1397            docks: Default::default(),
1398            centered_layout: false,
1399            session_id: None,
1400            window_id: Some(1),
1401        };
1402
1403        let mut workspace_2 = SerializedWorkspace {
1404            id: WorkspaceId(2),
1405            location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1406            center_group: Default::default(),
1407            window_bounds: Default::default(),
1408            display: Default::default(),
1409            docks: Default::default(),
1410            centered_layout: false,
1411            session_id: None,
1412            window_id: Some(2),
1413        };
1414
1415        db.save_workspace(workspace_1.clone()).await;
1416        db.save_workspace(workspace_2.clone()).await;
1417
1418        // Test that paths are treated as a set
1419        assert_eq!(
1420            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1421            workspace_1
1422        );
1423        assert_eq!(
1424            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
1425            workspace_1
1426        );
1427
1428        // Make sure that other keys work
1429        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
1430        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
1431
1432        // Test 'mutate' case of updating a pre-existing id
1433        workspace_2.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]);
1434
1435        db.save_workspace(workspace_2.clone()).await;
1436        assert_eq!(
1437            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1438            workspace_2
1439        );
1440
1441        // Test other mechanism for mutating
1442        let mut workspace_3 = SerializedWorkspace {
1443            id: WorkspaceId(3),
1444            location: SerializedWorkspaceLocation::Local(
1445                LocalPaths::new(["/tmp", "/tmp2"]),
1446                LocalPathsOrder::new([1, 0]),
1447            ),
1448            center_group: Default::default(),
1449            window_bounds: Default::default(),
1450            display: Default::default(),
1451            docks: Default::default(),
1452            centered_layout: false,
1453            session_id: None,
1454            window_id: Some(3),
1455        };
1456
1457        db.save_workspace(workspace_3.clone()).await;
1458        assert_eq!(
1459            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1460            workspace_3
1461        );
1462
1463        // Make sure that updating paths differently also works
1464        workspace_3.location =
1465            SerializedWorkspaceLocation::from_local_paths(["/tmp3", "/tmp4", "/tmp2"]);
1466        db.save_workspace(workspace_3.clone()).await;
1467        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
1468        assert_eq!(
1469            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
1470                .unwrap(),
1471            workspace_3
1472        );
1473    }
1474
1475    #[gpui::test]
1476    async fn test_session_workspaces() {
1477        env_logger::try_init().ok();
1478
1479        let db = WorkspaceDb(open_test_db("test_serializing_workspaces_session_id").await);
1480
1481        let workspace_1 = SerializedWorkspace {
1482            id: WorkspaceId(1),
1483            location: SerializedWorkspaceLocation::from_local_paths(["/tmp1"]),
1484            center_group: Default::default(),
1485            window_bounds: Default::default(),
1486            display: Default::default(),
1487            docks: Default::default(),
1488            centered_layout: false,
1489            session_id: Some("session-id-1".to_owned()),
1490            window_id: Some(10),
1491        };
1492
1493        let workspace_2 = SerializedWorkspace {
1494            id: WorkspaceId(2),
1495            location: SerializedWorkspaceLocation::from_local_paths(["/tmp2"]),
1496            center_group: Default::default(),
1497            window_bounds: Default::default(),
1498            display: Default::default(),
1499            docks: Default::default(),
1500            centered_layout: false,
1501            session_id: Some("session-id-1".to_owned()),
1502            window_id: Some(20),
1503        };
1504
1505        let workspace_3 = SerializedWorkspace {
1506            id: WorkspaceId(3),
1507            location: SerializedWorkspaceLocation::from_local_paths(["/tmp3"]),
1508            center_group: Default::default(),
1509            window_bounds: Default::default(),
1510            display: Default::default(),
1511            docks: Default::default(),
1512            centered_layout: false,
1513            session_id: Some("session-id-2".to_owned()),
1514            window_id: Some(30),
1515        };
1516
1517        let workspace_4 = SerializedWorkspace {
1518            id: WorkspaceId(4),
1519            location: SerializedWorkspaceLocation::from_local_paths(["/tmp4"]),
1520            center_group: Default::default(),
1521            window_bounds: Default::default(),
1522            display: Default::default(),
1523            docks: Default::default(),
1524            centered_layout: false,
1525            session_id: None,
1526            window_id: None,
1527        };
1528
1529        let ssh_project = db
1530            .get_or_create_ssh_project("my-host".to_string(), Some(1234), vec![], None)
1531            .await
1532            .unwrap();
1533
1534        let workspace_5 = SerializedWorkspace {
1535            id: WorkspaceId(5),
1536            location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()),
1537            center_group: Default::default(),
1538            window_bounds: Default::default(),
1539            display: Default::default(),
1540            docks: Default::default(),
1541            centered_layout: false,
1542            session_id: Some("session-id-2".to_owned()),
1543            window_id: Some(50),
1544        };
1545
1546        let workspace_6 = SerializedWorkspace {
1547            id: WorkspaceId(6),
1548            location: SerializedWorkspaceLocation::Local(
1549                LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
1550                LocalPathsOrder::new([2, 1, 0]),
1551            ),
1552            center_group: Default::default(),
1553            window_bounds: Default::default(),
1554            display: Default::default(),
1555            docks: Default::default(),
1556            centered_layout: false,
1557            session_id: Some("session-id-3".to_owned()),
1558            window_id: Some(60),
1559        };
1560
1561        db.save_workspace(workspace_1.clone()).await;
1562        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
1563        db.save_workspace(workspace_2.clone()).await;
1564        db.save_workspace(workspace_3.clone()).await;
1565        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
1566        db.save_workspace(workspace_4.clone()).await;
1567        db.save_workspace(workspace_5.clone()).await;
1568        db.save_workspace(workspace_6.clone()).await;
1569
1570        let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
1571        assert_eq!(locations.len(), 2);
1572        assert_eq!(locations[0].0, LocalPaths::new(["/tmp2"]));
1573        assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
1574        assert_eq!(locations[0].2, Some(20));
1575        assert_eq!(locations[1].0, LocalPaths::new(["/tmp1"]));
1576        assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
1577        assert_eq!(locations[1].2, Some(10));
1578
1579        let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
1580        assert_eq!(locations.len(), 2);
1581        let empty_paths: Vec<&str> = Vec::new();
1582        assert_eq!(locations[0].0, LocalPaths::new(empty_paths.iter()));
1583        assert_eq!(locations[0].1, LocalPathsOrder::new([]));
1584        assert_eq!(locations[0].2, Some(50));
1585        assert_eq!(locations[0].3, Some(ssh_project.id.0));
1586        assert_eq!(locations[1].0, LocalPaths::new(["/tmp3"]));
1587        assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
1588        assert_eq!(locations[1].2, Some(30));
1589
1590        let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
1591        assert_eq!(locations.len(), 1);
1592        assert_eq!(
1593            locations[0].0,
1594            LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
1595        );
1596        assert_eq!(locations[0].1, LocalPathsOrder::new([2, 1, 0]));
1597        assert_eq!(locations[0].2, Some(60));
1598    }
1599
1600    fn default_workspace<P: AsRef<Path>>(
1601        workspace_id: &[P],
1602        center_group: &SerializedPaneGroup,
1603    ) -> SerializedWorkspace {
1604        SerializedWorkspace {
1605            id: WorkspaceId(4),
1606            location: SerializedWorkspaceLocation::from_local_paths(workspace_id),
1607            center_group: center_group.clone(),
1608            window_bounds: Default::default(),
1609            display: Default::default(),
1610            docks: Default::default(),
1611            centered_layout: false,
1612            session_id: None,
1613            window_id: None,
1614        }
1615    }
1616
1617    #[gpui::test]
1618    async fn test_last_session_workspace_locations() {
1619        let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
1620        let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
1621        let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
1622        let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
1623
1624        let db =
1625            WorkspaceDb(open_test_db("test_serializing_workspaces_last_session_workspaces").await);
1626
1627        let workspaces = [
1628            (1, vec![dir1.path()], vec![0], 9),
1629            (2, vec![dir2.path()], vec![0], 5),
1630            (3, vec![dir3.path()], vec![0], 8),
1631            (4, vec![dir4.path()], vec![0], 2),
1632            (
1633                5,
1634                vec![dir1.path(), dir2.path(), dir3.path()],
1635                vec![0, 1, 2],
1636                3,
1637            ),
1638            (
1639                6,
1640                vec![dir2.path(), dir3.path(), dir4.path()],
1641                vec![2, 1, 0],
1642                4,
1643            ),
1644        ]
1645        .into_iter()
1646        .map(|(id, locations, order, window_id)| SerializedWorkspace {
1647            id: WorkspaceId(id),
1648            location: SerializedWorkspaceLocation::Local(
1649                LocalPaths::new(locations),
1650                LocalPathsOrder::new(order),
1651            ),
1652            center_group: Default::default(),
1653            window_bounds: Default::default(),
1654            display: Default::default(),
1655            docks: Default::default(),
1656            centered_layout: false,
1657            session_id: Some("one-session".to_owned()),
1658            window_id: Some(window_id),
1659        })
1660        .collect::<Vec<_>>();
1661
1662        for workspace in workspaces.iter() {
1663            db.save_workspace(workspace.clone()).await;
1664        }
1665
1666        let stack = Some(Vec::from([
1667            WindowId::from(2), // Top
1668            WindowId::from(8),
1669            WindowId::from(5),
1670            WindowId::from(9),
1671            WindowId::from(3),
1672            WindowId::from(4), // Bottom
1673        ]));
1674
1675        let have = db
1676            .last_session_workspace_locations("one-session", stack)
1677            .unwrap();
1678        assert_eq!(have.len(), 6);
1679        assert_eq!(
1680            have[0],
1681            SerializedWorkspaceLocation::from_local_paths(&[dir4.path()])
1682        );
1683        assert_eq!(
1684            have[1],
1685            SerializedWorkspaceLocation::from_local_paths([dir3.path()])
1686        );
1687        assert_eq!(
1688            have[2],
1689            SerializedWorkspaceLocation::from_local_paths([dir2.path()])
1690        );
1691        assert_eq!(
1692            have[3],
1693            SerializedWorkspaceLocation::from_local_paths([dir1.path()])
1694        );
1695        assert_eq!(
1696            have[4],
1697            SerializedWorkspaceLocation::Local(
1698                LocalPaths::new([dir1.path(), dir2.path(), dir3.path()]),
1699                LocalPathsOrder::new([0, 1, 2]),
1700            ),
1701        );
1702        assert_eq!(
1703            have[5],
1704            SerializedWorkspaceLocation::Local(
1705                LocalPaths::new([dir2.path(), dir3.path(), dir4.path()]),
1706                LocalPathsOrder::new([2, 1, 0]),
1707            ),
1708        );
1709    }
1710
1711    #[gpui::test]
1712    async fn test_last_session_workspace_locations_ssh_projects() {
1713        let db = WorkspaceDb(
1714            open_test_db("test_serializing_workspaces_last_session_workspaces_ssh_projects").await,
1715        );
1716
1717        let ssh_projects = [
1718            ("host-1", "my-user-1"),
1719            ("host-2", "my-user-2"),
1720            ("host-3", "my-user-3"),
1721            ("host-4", "my-user-4"),
1722        ]
1723        .into_iter()
1724        .map(|(host, user)| async {
1725            db.get_or_create_ssh_project(host.to_string(), None, vec![], Some(user.to_string()))
1726                .await
1727                .unwrap()
1728        })
1729        .collect::<Vec<_>>();
1730
1731        let ssh_projects = futures::future::join_all(ssh_projects).await;
1732
1733        let workspaces = [
1734            (1, ssh_projects[0].clone(), 9),
1735            (2, ssh_projects[1].clone(), 5),
1736            (3, ssh_projects[2].clone(), 8),
1737            (4, ssh_projects[3].clone(), 2),
1738        ]
1739        .into_iter()
1740        .map(|(id, ssh_project, window_id)| SerializedWorkspace {
1741            id: WorkspaceId(id),
1742            location: SerializedWorkspaceLocation::Ssh(ssh_project),
1743            center_group: Default::default(),
1744            window_bounds: Default::default(),
1745            display: Default::default(),
1746            docks: Default::default(),
1747            centered_layout: false,
1748            session_id: Some("one-session".to_owned()),
1749            window_id: Some(window_id),
1750        })
1751        .collect::<Vec<_>>();
1752
1753        for workspace in workspaces.iter() {
1754            db.save_workspace(workspace.clone()).await;
1755        }
1756
1757        let stack = Some(Vec::from([
1758            WindowId::from(2), // Top
1759            WindowId::from(8),
1760            WindowId::from(5),
1761            WindowId::from(9), // Bottom
1762        ]));
1763
1764        let have = db
1765            .last_session_workspace_locations("one-session", stack)
1766            .unwrap();
1767        assert_eq!(have.len(), 4);
1768        assert_eq!(
1769            have[0],
1770            SerializedWorkspaceLocation::Ssh(ssh_projects[3].clone())
1771        );
1772        assert_eq!(
1773            have[1],
1774            SerializedWorkspaceLocation::Ssh(ssh_projects[2].clone())
1775        );
1776        assert_eq!(
1777            have[2],
1778            SerializedWorkspaceLocation::Ssh(ssh_projects[1].clone())
1779        );
1780        assert_eq!(
1781            have[3],
1782            SerializedWorkspaceLocation::Ssh(ssh_projects[0].clone())
1783        );
1784    }
1785
1786    #[gpui::test]
1787    async fn test_get_or_create_ssh_project() {
1788        let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project").await);
1789
1790        let (host, port, paths, user) = (
1791            "example.com".to_string(),
1792            Some(22_u16),
1793            vec!["/home/user".to_string(), "/etc/nginx".to_string()],
1794            Some("user".to_string()),
1795        );
1796
1797        let project = db
1798            .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
1799            .await
1800            .unwrap();
1801
1802        assert_eq!(project.host, host);
1803        assert_eq!(project.paths, paths);
1804        assert_eq!(project.user, user);
1805
1806        // Test that calling the function again with the same parameters returns the same project
1807        let same_project = db
1808            .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
1809            .await
1810            .unwrap();
1811
1812        assert_eq!(project.id, same_project.id);
1813
1814        // Test with different parameters
1815        let (host2, paths2, user2) = (
1816            "otherexample.com".to_string(),
1817            vec!["/home/otheruser".to_string()],
1818            Some("otheruser".to_string()),
1819        );
1820
1821        let different_project = db
1822            .get_or_create_ssh_project(host2.clone(), None, paths2.clone(), user2.clone())
1823            .await
1824            .unwrap();
1825
1826        assert_ne!(project.id, different_project.id);
1827        assert_eq!(different_project.host, host2);
1828        assert_eq!(different_project.paths, paths2);
1829        assert_eq!(different_project.user, user2);
1830    }
1831
1832    #[gpui::test]
1833    async fn test_get_or_create_ssh_project_with_null_user() {
1834        let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project_with_null_user").await);
1835
1836        let (host, port, paths, user) = (
1837            "example.com".to_string(),
1838            None,
1839            vec!["/home/user".to_string()],
1840            None,
1841        );
1842
1843        let project = db
1844            .get_or_create_ssh_project(host.clone(), port, paths.clone(), None)
1845            .await
1846            .unwrap();
1847
1848        assert_eq!(project.host, host);
1849        assert_eq!(project.paths, paths);
1850        assert_eq!(project.user, None);
1851
1852        // Test that calling the function again with the same parameters returns the same project
1853        let same_project = db
1854            .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
1855            .await
1856            .unwrap();
1857
1858        assert_eq!(project.id, same_project.id);
1859    }
1860
1861    #[gpui::test]
1862    async fn test_get_ssh_projects() {
1863        let db = WorkspaceDb(open_test_db("test_get_ssh_projects").await);
1864
1865        let projects = vec![
1866            (
1867                "example.com".to_string(),
1868                None,
1869                vec!["/home/user".to_string()],
1870                None,
1871            ),
1872            (
1873                "anotherexample.com".to_string(),
1874                Some(123_u16),
1875                vec!["/home/user2".to_string()],
1876                Some("user2".to_string()),
1877            ),
1878            (
1879                "yetanother.com".to_string(),
1880                Some(345_u16),
1881                vec!["/home/user3".to_string(), "/proc/1234/exe".to_string()],
1882                None,
1883            ),
1884        ];
1885
1886        for (host, port, paths, user) in projects.iter() {
1887            let project = db
1888                .get_or_create_ssh_project(host.clone(), *port, paths.clone(), user.clone())
1889                .await
1890                .unwrap();
1891
1892            assert_eq!(&project.host, host);
1893            assert_eq!(&project.port, port);
1894            assert_eq!(&project.paths, paths);
1895            assert_eq!(&project.user, user);
1896        }
1897
1898        let stored_projects = db.ssh_projects().unwrap();
1899        assert_eq!(stored_projects.len(), projects.len());
1900    }
1901
1902    #[gpui::test]
1903    async fn test_simple_split() {
1904        env_logger::try_init().ok();
1905
1906        let db = WorkspaceDb(open_test_db("simple_split").await);
1907
1908        //  -----------------
1909        //  | 1,2   | 5,6   |
1910        //  | - - - |       |
1911        //  | 3,4   |       |
1912        //  -----------------
1913        let center_pane = group(
1914            Axis::Horizontal,
1915            vec![
1916                group(
1917                    Axis::Vertical,
1918                    vec![
1919                        SerializedPaneGroup::Pane(SerializedPane::new(
1920                            vec![
1921                                SerializedItem::new("Terminal", 1, false, false),
1922                                SerializedItem::new("Terminal", 2, true, false),
1923                            ],
1924                            false,
1925                            0,
1926                        )),
1927                        SerializedPaneGroup::Pane(SerializedPane::new(
1928                            vec![
1929                                SerializedItem::new("Terminal", 4, false, false),
1930                                SerializedItem::new("Terminal", 3, true, false),
1931                            ],
1932                            true,
1933                            0,
1934                        )),
1935                    ],
1936                ),
1937                SerializedPaneGroup::Pane(SerializedPane::new(
1938                    vec![
1939                        SerializedItem::new("Terminal", 5, true, false),
1940                        SerializedItem::new("Terminal", 6, false, false),
1941                    ],
1942                    false,
1943                    0,
1944                )),
1945            ],
1946        );
1947
1948        let workspace = default_workspace(&["/tmp"], &center_pane);
1949
1950        db.save_workspace(workspace.clone()).await;
1951
1952        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
1953
1954        assert_eq!(workspace.center_group, new_workspace.center_group);
1955    }
1956
1957    #[gpui::test]
1958    async fn test_cleanup_panes() {
1959        env_logger::try_init().ok();
1960
1961        let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
1962
1963        let center_pane = group(
1964            Axis::Horizontal,
1965            vec![
1966                group(
1967                    Axis::Vertical,
1968                    vec![
1969                        SerializedPaneGroup::Pane(SerializedPane::new(
1970                            vec![
1971                                SerializedItem::new("Terminal", 1, false, false),
1972                                SerializedItem::new("Terminal", 2, true, false),
1973                            ],
1974                            false,
1975                            0,
1976                        )),
1977                        SerializedPaneGroup::Pane(SerializedPane::new(
1978                            vec![
1979                                SerializedItem::new("Terminal", 4, false, false),
1980                                SerializedItem::new("Terminal", 3, true, false),
1981                            ],
1982                            true,
1983                            0,
1984                        )),
1985                    ],
1986                ),
1987                SerializedPaneGroup::Pane(SerializedPane::new(
1988                    vec![
1989                        SerializedItem::new("Terminal", 5, false, false),
1990                        SerializedItem::new("Terminal", 6, true, false),
1991                    ],
1992                    false,
1993                    0,
1994                )),
1995            ],
1996        );
1997
1998        let id = &["/tmp"];
1999
2000        let mut workspace = default_workspace(id, &center_pane);
2001
2002        db.save_workspace(workspace.clone()).await;
2003
2004        workspace.center_group = group(
2005            Axis::Vertical,
2006            vec![
2007                SerializedPaneGroup::Pane(SerializedPane::new(
2008                    vec![
2009                        SerializedItem::new("Terminal", 1, false, false),
2010                        SerializedItem::new("Terminal", 2, true, false),
2011                    ],
2012                    false,
2013                    0,
2014                )),
2015                SerializedPaneGroup::Pane(SerializedPane::new(
2016                    vec![
2017                        SerializedItem::new("Terminal", 4, true, false),
2018                        SerializedItem::new("Terminal", 3, false, false),
2019                    ],
2020                    true,
2021                    0,
2022                )),
2023            ],
2024        );
2025
2026        db.save_workspace(workspace.clone()).await;
2027
2028        let new_workspace = db.workspace_for_roots(id).unwrap();
2029
2030        assert_eq!(workspace.center_group, new_workspace.center_group);
2031    }
2032}