persistence.rs

   1pub mod model;
   2
   3use std::{
   4    borrow::Cow,
   5    collections::BTreeMap,
   6    path::{Path, PathBuf},
   7    str::FromStr,
   8    sync::Arc,
   9};
  10
  11use anyhow::{Context as _, Result, bail};
  12use collections::{HashMap, HashSet, IndexSet};
  13use db::{
  14    kvp::KEY_VALUE_STORE,
  15    query,
  16    sqlez::{connection::Connection, domain::Domain},
  17    sqlez_macros::sql,
  18};
  19use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size};
  20use project::{
  21    debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
  22    trusted_worktrees::{DbTrustedPaths, RemoteHostLocation},
  23};
  24
  25use language::{LanguageName, Toolchain, ToolchainScope};
  26use remote::{
  27    DockerConnectionOptions, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions,
  28};
  29use serde::{Deserialize, Serialize};
  30use sqlez::{
  31    bindable::{Bind, Column, StaticColumnCount},
  32    statement::Statement,
  33    thread_safe_connection::ThreadSafeConnection,
  34};
  35
  36use ui::{App, SharedString, px};
  37use util::{ResultExt, maybe, rel_path::RelPath};
  38use uuid::Uuid;
  39
  40use crate::{
  41    WorkspaceId,
  42    path_list::{PathList, SerializedPathList},
  43    persistence::model::RemoteConnectionKind,
  44};
  45
  46use model::{
  47    GroupId, ItemId, PaneId, RemoteConnectionId, SerializedItem, SerializedPane,
  48    SerializedPaneGroup, SerializedWorkspace,
  49};
  50
  51use self::model::{DockStructure, SerializedWorkspaceLocation};
  52
  53// https://www.sqlite.org/limits.html
  54// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
  55// > which defaults to <..> 32766 for SQLite versions after 3.32.0.
  56const MAX_QUERY_PLACEHOLDERS: usize = 32000;
  57
  58#[derive(Copy, Clone, Debug, PartialEq)]
  59pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
  60impl sqlez::bindable::StaticColumnCount for SerializedAxis {}
  61impl sqlez::bindable::Bind for SerializedAxis {
  62    fn bind(
  63        &self,
  64        statement: &sqlez::statement::Statement,
  65        start_index: i32,
  66    ) -> anyhow::Result<i32> {
  67        match self.0 {
  68            gpui::Axis::Horizontal => "Horizontal",
  69            gpui::Axis::Vertical => "Vertical",
  70        }
  71        .bind(statement, start_index)
  72    }
  73}
  74
  75impl sqlez::bindable::Column for SerializedAxis {
  76    fn column(
  77        statement: &mut sqlez::statement::Statement,
  78        start_index: i32,
  79    ) -> anyhow::Result<(Self, i32)> {
  80        String::column(statement, start_index).and_then(|(axis_text, next_index)| {
  81            Ok((
  82                match axis_text.as_str() {
  83                    "Horizontal" => Self(Axis::Horizontal),
  84                    "Vertical" => Self(Axis::Vertical),
  85                    _ => anyhow::bail!("Stored serialized item kind is incorrect"),
  86                },
  87                next_index,
  88            ))
  89        })
  90    }
  91}
  92
  93#[derive(Copy, Clone, Debug, PartialEq, Default)]
  94pub(crate) struct SerializedWindowBounds(pub(crate) WindowBounds);
  95
  96impl StaticColumnCount for SerializedWindowBounds {
  97    fn column_count() -> usize {
  98        5
  99    }
 100}
 101
 102impl Bind for SerializedWindowBounds {
 103    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
 104        match self.0 {
 105            WindowBounds::Windowed(bounds) => {
 106                let next_index = statement.bind(&"Windowed", start_index)?;
 107                statement.bind(
 108                    &(
 109                        SerializedPixels(bounds.origin.x),
 110                        SerializedPixels(bounds.origin.y),
 111                        SerializedPixels(bounds.size.width),
 112                        SerializedPixels(bounds.size.height),
 113                    ),
 114                    next_index,
 115                )
 116            }
 117            WindowBounds::Maximized(bounds) => {
 118                let next_index = statement.bind(&"Maximized", start_index)?;
 119                statement.bind(
 120                    &(
 121                        SerializedPixels(bounds.origin.x),
 122                        SerializedPixels(bounds.origin.y),
 123                        SerializedPixels(bounds.size.width),
 124                        SerializedPixels(bounds.size.height),
 125                    ),
 126                    next_index,
 127                )
 128            }
 129            WindowBounds::Fullscreen(bounds) => {
 130                let next_index = statement.bind(&"FullScreen", start_index)?;
 131                statement.bind(
 132                    &(
 133                        SerializedPixels(bounds.origin.x),
 134                        SerializedPixels(bounds.origin.y),
 135                        SerializedPixels(bounds.size.width),
 136                        SerializedPixels(bounds.size.height),
 137                    ),
 138                    next_index,
 139                )
 140            }
 141        }
 142    }
 143}
 144
 145impl Column for SerializedWindowBounds {
 146    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 147        let (window_state, next_index) = String::column(statement, start_index)?;
 148        let ((x, y, width, height), _): ((i32, i32, i32, i32), _) =
 149            Column::column(statement, next_index)?;
 150        let bounds = Bounds {
 151            origin: point(px(x as f32), px(y as f32)),
 152            size: size(px(width as f32), px(height as f32)),
 153        };
 154
 155        let status = match window_state.as_str() {
 156            "Windowed" | "Fixed" => SerializedWindowBounds(WindowBounds::Windowed(bounds)),
 157            "Maximized" => SerializedWindowBounds(WindowBounds::Maximized(bounds)),
 158            "FullScreen" => SerializedWindowBounds(WindowBounds::Fullscreen(bounds)),
 159            _ => bail!("Window State did not have a valid string"),
 160        };
 161
 162        Ok((status, next_index + 4))
 163    }
 164}
 165
 166const DEFAULT_WINDOW_BOUNDS_KEY: &str = "default_window_bounds";
 167
 168pub fn read_default_window_bounds() -> Option<(Uuid, WindowBounds)> {
 169    let json_str = KEY_VALUE_STORE
 170        .read_kvp(DEFAULT_WINDOW_BOUNDS_KEY)
 171        .log_err()
 172        .flatten()?;
 173
 174    let (display_uuid, persisted) =
 175        serde_json::from_str::<(Uuid, WindowBoundsJson)>(&json_str).ok()?;
 176    Some((display_uuid, persisted.into()))
 177}
 178
 179pub async fn write_default_window_bounds(
 180    bounds: WindowBounds,
 181    display_uuid: Uuid,
 182) -> anyhow::Result<()> {
 183    let persisted = WindowBoundsJson::from(bounds);
 184    let json_str = serde_json::to_string(&(display_uuid, persisted))?;
 185    KEY_VALUE_STORE
 186        .write_kvp(DEFAULT_WINDOW_BOUNDS_KEY.to_string(), json_str)
 187        .await?;
 188    Ok(())
 189}
 190
 191#[derive(Serialize, Deserialize)]
 192pub enum WindowBoundsJson {
 193    Windowed {
 194        x: i32,
 195        y: i32,
 196        width: i32,
 197        height: i32,
 198    },
 199    Maximized {
 200        x: i32,
 201        y: i32,
 202        width: i32,
 203        height: i32,
 204    },
 205    Fullscreen {
 206        x: i32,
 207        y: i32,
 208        width: i32,
 209        height: i32,
 210    },
 211}
 212
 213impl From<WindowBounds> for WindowBoundsJson {
 214    fn from(b: WindowBounds) -> Self {
 215        match b {
 216            WindowBounds::Windowed(bounds) => {
 217                let origin = bounds.origin;
 218                let size = bounds.size;
 219                WindowBoundsJson::Windowed {
 220                    x: f32::from(origin.x).round() as i32,
 221                    y: f32::from(origin.y).round() as i32,
 222                    width: f32::from(size.width).round() as i32,
 223                    height: f32::from(size.height).round() as i32,
 224                }
 225            }
 226            WindowBounds::Maximized(bounds) => {
 227                let origin = bounds.origin;
 228                let size = bounds.size;
 229                WindowBoundsJson::Maximized {
 230                    x: f32::from(origin.x).round() as i32,
 231                    y: f32::from(origin.y).round() as i32,
 232                    width: f32::from(size.width).round() as i32,
 233                    height: f32::from(size.height).round() as i32,
 234                }
 235            }
 236            WindowBounds::Fullscreen(bounds) => {
 237                let origin = bounds.origin;
 238                let size = bounds.size;
 239                WindowBoundsJson::Fullscreen {
 240                    x: f32::from(origin.x).round() as i32,
 241                    y: f32::from(origin.y).round() as i32,
 242                    width: f32::from(size.width).round() as i32,
 243                    height: f32::from(size.height).round() as i32,
 244                }
 245            }
 246        }
 247    }
 248}
 249
 250impl From<WindowBoundsJson> for WindowBounds {
 251    fn from(n: WindowBoundsJson) -> Self {
 252        match n {
 253            WindowBoundsJson::Windowed {
 254                x,
 255                y,
 256                width,
 257                height,
 258            } => WindowBounds::Windowed(Bounds {
 259                origin: point(px(x as f32), px(y as f32)),
 260                size: size(px(width as f32), px(height as f32)),
 261            }),
 262            WindowBoundsJson::Maximized {
 263                x,
 264                y,
 265                width,
 266                height,
 267            } => WindowBounds::Maximized(Bounds {
 268                origin: point(px(x as f32), px(y as f32)),
 269                size: size(px(width as f32), px(height as f32)),
 270            }),
 271            WindowBoundsJson::Fullscreen {
 272                x,
 273                y,
 274                width,
 275                height,
 276            } => WindowBounds::Fullscreen(Bounds {
 277                origin: point(px(x as f32), px(y as f32)),
 278                size: size(px(width as f32), px(height as f32)),
 279            }),
 280        }
 281    }
 282}
 283
 284#[derive(Debug)]
 285pub struct Breakpoint {
 286    pub position: u32,
 287    pub message: Option<Arc<str>>,
 288    pub condition: Option<Arc<str>>,
 289    pub hit_condition: Option<Arc<str>>,
 290    pub state: BreakpointState,
 291}
 292
 293/// Wrapper for DB type of a breakpoint
 294struct BreakpointStateWrapper<'a>(Cow<'a, BreakpointState>);
 295
 296impl From<BreakpointState> for BreakpointStateWrapper<'static> {
 297    fn from(kind: BreakpointState) -> Self {
 298        BreakpointStateWrapper(Cow::Owned(kind))
 299    }
 300}
 301
 302impl StaticColumnCount for BreakpointStateWrapper<'_> {
 303    fn column_count() -> usize {
 304        1
 305    }
 306}
 307
 308impl Bind for BreakpointStateWrapper<'_> {
 309    fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
 310        statement.bind(&self.0.to_int(), start_index)
 311    }
 312}
 313
 314impl Column for BreakpointStateWrapper<'_> {
 315    fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
 316        let state = statement.column_int(start_index)?;
 317
 318        match state {
 319            0 => Ok((BreakpointState::Enabled.into(), start_index + 1)),
 320            1 => Ok((BreakpointState::Disabled.into(), start_index + 1)),
 321            _ => anyhow::bail!("Invalid BreakpointState discriminant {state}"),
 322        }
 323    }
 324}
 325
 326impl sqlez::bindable::StaticColumnCount for Breakpoint {
 327    fn column_count() -> usize {
 328        // Position, log message, condition message, and hit condition message
 329        4 + BreakpointStateWrapper::column_count()
 330    }
 331}
 332
 333impl sqlez::bindable::Bind for Breakpoint {
 334    fn bind(
 335        &self,
 336        statement: &sqlez::statement::Statement,
 337        start_index: i32,
 338    ) -> anyhow::Result<i32> {
 339        let next_index = statement.bind(&self.position, start_index)?;
 340        let next_index = statement.bind(&self.message, next_index)?;
 341        let next_index = statement.bind(&self.condition, next_index)?;
 342        let next_index = statement.bind(&self.hit_condition, next_index)?;
 343        statement.bind(
 344            &BreakpointStateWrapper(Cow::Borrowed(&self.state)),
 345            next_index,
 346        )
 347    }
 348}
 349
 350impl Column for Breakpoint {
 351    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 352        let position = statement
 353            .column_int(start_index)
 354            .with_context(|| format!("Failed to read BreakPoint at index {start_index}"))?
 355            as u32;
 356        let (message, next_index) = Option::<String>::column(statement, start_index + 1)?;
 357        let (condition, next_index) = Option::<String>::column(statement, next_index)?;
 358        let (hit_condition, next_index) = Option::<String>::column(statement, next_index)?;
 359        let (state, next_index) = BreakpointStateWrapper::column(statement, next_index)?;
 360
 361        Ok((
 362            Breakpoint {
 363                position,
 364                message: message.map(Arc::from),
 365                condition: condition.map(Arc::from),
 366                hit_condition: hit_condition.map(Arc::from),
 367                state: state.0.into_owned(),
 368            },
 369            next_index,
 370        ))
 371    }
 372}
 373
 374#[derive(Clone, Debug, PartialEq)]
 375struct SerializedPixels(gpui::Pixels);
 376impl sqlez::bindable::StaticColumnCount for SerializedPixels {}
 377
 378impl sqlez::bindable::Bind for SerializedPixels {
 379    fn bind(
 380        &self,
 381        statement: &sqlez::statement::Statement,
 382        start_index: i32,
 383    ) -> anyhow::Result<i32> {
 384        let this: i32 = u32::from(self.0) as _;
 385        this.bind(statement, start_index)
 386    }
 387}
 388
 389pub struct WorkspaceDb(ThreadSafeConnection);
 390
 391impl Domain for WorkspaceDb {
 392    const NAME: &str = stringify!(WorkspaceDb);
 393
 394    const MIGRATIONS: &[&str] = &[
 395        sql!(
 396            CREATE TABLE workspaces(
 397                workspace_id INTEGER PRIMARY KEY,
 398                workspace_location BLOB UNIQUE,
 399                dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
 400                dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
 401                dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
 402                left_sidebar_open INTEGER, // Boolean
 403                timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 404                FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
 405            ) STRICT;
 406
 407            CREATE TABLE pane_groups(
 408                group_id INTEGER PRIMARY KEY,
 409                workspace_id INTEGER NOT NULL,
 410                parent_group_id INTEGER, // NULL indicates that this is a root node
 411                position INTEGER, // NULL indicates that this is a root node
 412                axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
 413                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 414                ON DELETE CASCADE
 415                ON UPDATE CASCADE,
 416                FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 417            ) STRICT;
 418
 419            CREATE TABLE panes(
 420                pane_id INTEGER PRIMARY KEY,
 421                workspace_id INTEGER NOT NULL,
 422                active INTEGER NOT NULL, // Boolean
 423                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 424                ON DELETE CASCADE
 425                ON UPDATE CASCADE
 426            ) STRICT;
 427
 428            CREATE TABLE center_panes(
 429                pane_id INTEGER PRIMARY KEY,
 430                parent_group_id INTEGER, // NULL means that this is a root pane
 431                position INTEGER, // NULL means that this is a root pane
 432                FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 433                ON DELETE CASCADE,
 434                FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 435            ) STRICT;
 436
 437            CREATE TABLE items(
 438                item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
 439                workspace_id INTEGER NOT NULL,
 440                pane_id INTEGER NOT NULL,
 441                kind TEXT NOT NULL,
 442                position INTEGER NOT NULL,
 443                active INTEGER NOT NULL,
 444                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 445                ON DELETE CASCADE
 446                ON UPDATE CASCADE,
 447                FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 448                ON DELETE CASCADE,
 449                PRIMARY KEY(item_id, workspace_id)
 450            ) STRICT;
 451        ),
 452        sql!(
 453            ALTER TABLE workspaces ADD COLUMN window_state TEXT;
 454            ALTER TABLE workspaces ADD COLUMN window_x REAL;
 455            ALTER TABLE workspaces ADD COLUMN window_y REAL;
 456            ALTER TABLE workspaces ADD COLUMN window_width REAL;
 457            ALTER TABLE workspaces ADD COLUMN window_height REAL;
 458            ALTER TABLE workspaces ADD COLUMN display BLOB;
 459        ),
 460        // Drop foreign key constraint from workspaces.dock_pane to panes table.
 461        sql!(
 462            CREATE TABLE workspaces_2(
 463                workspace_id INTEGER PRIMARY KEY,
 464                workspace_location BLOB UNIQUE,
 465                dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
 466                dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
 467                dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
 468                left_sidebar_open INTEGER, // Boolean
 469                timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 470                window_state TEXT,
 471                window_x REAL,
 472                window_y REAL,
 473                window_width REAL,
 474                window_height REAL,
 475                display BLOB
 476            ) STRICT;
 477            INSERT INTO workspaces_2 SELECT * FROM workspaces;
 478            DROP TABLE workspaces;
 479            ALTER TABLE workspaces_2 RENAME TO workspaces;
 480        ),
 481        // Add panels related information
 482        sql!(
 483            ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
 484            ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
 485            ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
 486            ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
 487            ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
 488            ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
 489        ),
 490        // Add panel zoom persistence
 491        sql!(
 492            ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
 493            ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
 494            ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
 495        ),
 496        // Add pane group flex data
 497        sql!(
 498            ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
 499        ),
 500        // Add fullscreen field to workspace
 501        // Deprecated, `WindowBounds` holds the fullscreen state now.
 502        // Preserving so users can downgrade Zed.
 503        sql!(
 504            ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
 505        ),
 506        // Add preview field to items
 507        sql!(
 508            ALTER TABLE items ADD COLUMN preview INTEGER; //bool
 509        ),
 510        // Add centered_layout field to workspace
 511        sql!(
 512            ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
 513        ),
 514        sql!(
 515            CREATE TABLE remote_projects (
 516                remote_project_id INTEGER NOT NULL UNIQUE,
 517                path TEXT,
 518                dev_server_name TEXT
 519            );
 520            ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
 521            ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
 522        ),
 523        sql!(
 524            DROP TABLE remote_projects;
 525            CREATE TABLE dev_server_projects (
 526                id INTEGER NOT NULL UNIQUE,
 527                path TEXT,
 528                dev_server_name TEXT
 529            );
 530            ALTER TABLE workspaces DROP COLUMN remote_project_id;
 531            ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
 532        ),
 533        sql!(
 534            ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
 535        ),
 536        sql!(
 537            ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
 538        ),
 539        sql!(
 540            ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
 541        ),
 542        sql!(
 543            ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
 544        ),
 545        sql!(
 546            CREATE TABLE ssh_projects (
 547                id INTEGER PRIMARY KEY,
 548                host TEXT NOT NULL,
 549                port INTEGER,
 550                path TEXT NOT NULL,
 551                user TEXT
 552            );
 553            ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
 554        ),
 555        sql!(
 556            ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
 557        ),
 558        sql!(
 559            CREATE TABLE toolchains (
 560                workspace_id INTEGER,
 561                worktree_id INTEGER,
 562                language_name TEXT NOT NULL,
 563                name TEXT NOT NULL,
 564                path TEXT NOT NULL,
 565                PRIMARY KEY (workspace_id, worktree_id, language_name)
 566            );
 567        ),
 568        sql!(
 569            ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}";
 570        ),
 571        sql!(
 572            CREATE TABLE breakpoints (
 573                workspace_id INTEGER NOT NULL,
 574                path TEXT NOT NULL,
 575                breakpoint_location INTEGER NOT NULL,
 576                kind INTEGER NOT NULL,
 577                log_message TEXT,
 578                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 579                ON DELETE CASCADE
 580                ON UPDATE CASCADE
 581            );
 582        ),
 583        sql!(
 584            ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT;
 585            CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array);
 586            ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT;
 587        ),
 588        sql!(
 589            ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL
 590        ),
 591        sql!(
 592            ALTER TABLE breakpoints DROP COLUMN kind
 593        ),
 594        sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL),
 595        sql!(
 596            ALTER TABLE breakpoints ADD COLUMN condition TEXT;
 597            ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT;
 598        ),
 599        sql!(CREATE TABLE toolchains2 (
 600            workspace_id INTEGER,
 601            worktree_id INTEGER,
 602            language_name TEXT NOT NULL,
 603            name TEXT NOT NULL,
 604            path TEXT NOT NULL,
 605            raw_json TEXT NOT NULL,
 606            relative_worktree_path TEXT NOT NULL,
 607            PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT;
 608            INSERT INTO toolchains2
 609                SELECT * FROM toolchains;
 610            DROP TABLE toolchains;
 611            ALTER TABLE toolchains2 RENAME TO toolchains;
 612        ),
 613        sql!(
 614            CREATE TABLE ssh_connections (
 615                id INTEGER PRIMARY KEY,
 616                host TEXT NOT NULL,
 617                port INTEGER,
 618                user TEXT
 619            );
 620
 621            INSERT INTO ssh_connections (host, port, user)
 622            SELECT DISTINCT host, port, user
 623            FROM ssh_projects;
 624
 625            CREATE TABLE workspaces_2(
 626                workspace_id INTEGER PRIMARY KEY,
 627                paths TEXT,
 628                paths_order TEXT,
 629                ssh_connection_id INTEGER REFERENCES ssh_connections(id),
 630                timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 631                window_state TEXT,
 632                window_x REAL,
 633                window_y REAL,
 634                window_width REAL,
 635                window_height REAL,
 636                display BLOB,
 637                left_dock_visible INTEGER,
 638                left_dock_active_panel TEXT,
 639                right_dock_visible INTEGER,
 640                right_dock_active_panel TEXT,
 641                bottom_dock_visible INTEGER,
 642                bottom_dock_active_panel TEXT,
 643                left_dock_zoom INTEGER,
 644                right_dock_zoom INTEGER,
 645                bottom_dock_zoom INTEGER,
 646                fullscreen INTEGER,
 647                centered_layout INTEGER,
 648                session_id TEXT,
 649                window_id INTEGER
 650            ) STRICT;
 651
 652            INSERT
 653            INTO workspaces_2
 654            SELECT
 655                workspaces.workspace_id,
 656                CASE
 657                    WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths
 658                    ELSE
 659                        CASE
 660                            WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN
 661                                NULL
 662                            ELSE
 663                                replace(workspaces.local_paths_array, ',', CHAR(10))
 664                        END
 665                END as paths,
 666
 667                CASE
 668                    WHEN ssh_projects.id IS NOT NULL THEN ""
 669                    ELSE workspaces.local_paths_order_array
 670                END as paths_order,
 671
 672                CASE
 673                    WHEN ssh_projects.id IS NOT NULL THEN (
 674                        SELECT ssh_connections.id
 675                        FROM ssh_connections
 676                        WHERE
 677                            ssh_connections.host IS ssh_projects.host AND
 678                            ssh_connections.port IS ssh_projects.port AND
 679                            ssh_connections.user IS ssh_projects.user
 680                    )
 681                    ELSE NULL
 682                END as ssh_connection_id,
 683
 684                workspaces.timestamp,
 685                workspaces.window_state,
 686                workspaces.window_x,
 687                workspaces.window_y,
 688                workspaces.window_width,
 689                workspaces.window_height,
 690                workspaces.display,
 691                workspaces.left_dock_visible,
 692                workspaces.left_dock_active_panel,
 693                workspaces.right_dock_visible,
 694                workspaces.right_dock_active_panel,
 695                workspaces.bottom_dock_visible,
 696                workspaces.bottom_dock_active_panel,
 697                workspaces.left_dock_zoom,
 698                workspaces.right_dock_zoom,
 699                workspaces.bottom_dock_zoom,
 700                workspaces.fullscreen,
 701                workspaces.centered_layout,
 702                workspaces.session_id,
 703                workspaces.window_id
 704            FROM
 705                workspaces LEFT JOIN
 706                ssh_projects ON
 707                workspaces.ssh_project_id = ssh_projects.id;
 708
 709            DELETE FROM workspaces_2
 710            WHERE workspace_id NOT IN (
 711                SELECT MAX(workspace_id)
 712                FROM workspaces_2
 713                GROUP BY ssh_connection_id, paths
 714            );
 715
 716            DROP TABLE ssh_projects;
 717            DROP TABLE workspaces;
 718            ALTER TABLE workspaces_2 RENAME TO workspaces;
 719
 720            CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths);
 721        ),
 722        // Fix any data from when workspaces.paths were briefly encoded as JSON arrays
 723        sql!(
 724            UPDATE workspaces
 725            SET paths = CASE
 726                WHEN substr(paths, 1, 2) = '[' || '"' AND substr(paths, -2, 2) = '"' || ']' THEN
 727                    replace(
 728                        substr(paths, 3, length(paths) - 4),
 729                        '"' || ',' || '"',
 730                        CHAR(10)
 731                    )
 732                ELSE
 733                    replace(paths, ',', CHAR(10))
 734            END
 735            WHERE paths IS NOT NULL
 736        ),
 737        sql!(
 738            CREATE TABLE remote_connections(
 739                id INTEGER PRIMARY KEY,
 740                kind TEXT NOT NULL,
 741                host TEXT,
 742                port INTEGER,
 743                user TEXT,
 744                distro TEXT
 745            );
 746
 747            CREATE TABLE workspaces_2(
 748                workspace_id INTEGER PRIMARY KEY,
 749                paths TEXT,
 750                paths_order TEXT,
 751                remote_connection_id INTEGER REFERENCES remote_connections(id),
 752                timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 753                window_state TEXT,
 754                window_x REAL,
 755                window_y REAL,
 756                window_width REAL,
 757                window_height REAL,
 758                display BLOB,
 759                left_dock_visible INTEGER,
 760                left_dock_active_panel TEXT,
 761                right_dock_visible INTEGER,
 762                right_dock_active_panel TEXT,
 763                bottom_dock_visible INTEGER,
 764                bottom_dock_active_panel TEXT,
 765                left_dock_zoom INTEGER,
 766                right_dock_zoom INTEGER,
 767                bottom_dock_zoom INTEGER,
 768                fullscreen INTEGER,
 769                centered_layout INTEGER,
 770                session_id TEXT,
 771                window_id INTEGER
 772            ) STRICT;
 773
 774            INSERT INTO remote_connections
 775            SELECT
 776                id,
 777                "ssh" as kind,
 778                host,
 779                port,
 780                user,
 781                NULL as distro
 782            FROM ssh_connections;
 783
 784            INSERT
 785            INTO workspaces_2
 786            SELECT
 787                workspace_id,
 788                paths,
 789                paths_order,
 790                ssh_connection_id as remote_connection_id,
 791                timestamp,
 792                window_state,
 793                window_x,
 794                window_y,
 795                window_width,
 796                window_height,
 797                display,
 798                left_dock_visible,
 799                left_dock_active_panel,
 800                right_dock_visible,
 801                right_dock_active_panel,
 802                bottom_dock_visible,
 803                bottom_dock_active_panel,
 804                left_dock_zoom,
 805                right_dock_zoom,
 806                bottom_dock_zoom,
 807                fullscreen,
 808                centered_layout,
 809                session_id,
 810                window_id
 811            FROM
 812                workspaces;
 813
 814            DROP TABLE workspaces;
 815            ALTER TABLE workspaces_2 RENAME TO workspaces;
 816
 817            CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(remote_connection_id, paths);
 818        ),
 819        sql!(CREATE TABLE user_toolchains (
 820            remote_connection_id INTEGER,
 821            workspace_id INTEGER NOT NULL,
 822            worktree_id INTEGER NOT NULL,
 823            relative_worktree_path TEXT NOT NULL,
 824            language_name TEXT NOT NULL,
 825            name TEXT NOT NULL,
 826            path TEXT NOT NULL,
 827            raw_json TEXT NOT NULL,
 828
 829            PRIMARY KEY (workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json)
 830        ) STRICT;),
 831        sql!(
 832            DROP TABLE ssh_connections;
 833        ),
 834        sql!(
 835            ALTER TABLE remote_connections ADD COLUMN name TEXT;
 836            ALTER TABLE remote_connections ADD COLUMN container_id TEXT;
 837        ),
 838        sql!(
 839            CREATE TABLE IF NOT EXISTS trusted_worktrees (
 840                trust_id INTEGER PRIMARY KEY AUTOINCREMENT,
 841                absolute_path TEXT,
 842                user_name TEXT,
 843                host_name TEXT
 844            ) STRICT;
 845        ),
 846        sql!(CREATE TABLE toolchains2 (
 847            workspace_id INTEGER,
 848            worktree_root_path TEXT NOT NULL,
 849            language_name TEXT NOT NULL,
 850            name TEXT NOT NULL,
 851            path TEXT NOT NULL,
 852            raw_json TEXT NOT NULL,
 853            relative_worktree_path TEXT NOT NULL,
 854            PRIMARY KEY (workspace_id, worktree_root_path, language_name, relative_worktree_path)) STRICT;
 855            INSERT OR REPLACE INTO toolchains2
 856                // The `instr(paths, '\n') = 0` part allows us to find all
 857                // workspaces that have a single worktree, as `\n` is used as a
 858                // separator when serializing the workspace paths, so if no `\n` is
 859                // found, we know we have a single worktree.
 860                SELECT toolchains.workspace_id, paths, language_name, name, path, raw_json, relative_worktree_path FROM toolchains INNER JOIN workspaces ON toolchains.workspace_id = workspaces.workspace_id AND instr(paths, '\n') = 0;
 861            DROP TABLE toolchains;
 862            ALTER TABLE toolchains2 RENAME TO toolchains;
 863        ),
 864        sql!(CREATE TABLE user_toolchains2 (
 865            remote_connection_id INTEGER,
 866            workspace_id INTEGER NOT NULL,
 867            worktree_root_path TEXT NOT NULL,
 868            relative_worktree_path TEXT NOT NULL,
 869            language_name TEXT NOT NULL,
 870            name TEXT NOT NULL,
 871            path TEXT NOT NULL,
 872            raw_json TEXT NOT NULL,
 873
 874            PRIMARY KEY (workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json)) STRICT;
 875            INSERT OR REPLACE INTO user_toolchains2
 876                // The `instr(paths, '\n') = 0` part allows us to find all
 877                // workspaces that have a single worktree, as `\n` is used as a
 878                // separator when serializing the workspace paths, so if no `\n` is
 879                // found, we know we have a single worktree.
 880                SELECT user_toolchains.remote_connection_id, user_toolchains.workspace_id, paths, relative_worktree_path, language_name, name, path, raw_json  FROM user_toolchains INNER JOIN workspaces ON user_toolchains.workspace_id = workspaces.workspace_id AND instr(paths, '\n') = 0;
 881            DROP TABLE user_toolchains;
 882            ALTER TABLE user_toolchains2 RENAME TO user_toolchains;
 883        ),
 884    ];
 885
 886    // Allow recovering from bad migration that was initially shipped to nightly
 887    // when introducing the ssh_connections table.
 888    fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool {
 889        old.starts_with("CREATE TABLE ssh_connections")
 890            && new.starts_with("CREATE TABLE ssh_connections")
 891    }
 892}
 893
 894db::static_connection!(DB, WorkspaceDb, []);
 895
 896impl WorkspaceDb {
 897    /// Returns a serialized workspace for the given worktree_roots. If the passed array
 898    /// is empty, the most recent workspace is returned instead. If no workspace for the
 899    /// passed roots is stored, returns none.
 900    pub(crate) fn workspace_for_roots<P: AsRef<Path>>(
 901        &self,
 902        worktree_roots: &[P],
 903    ) -> Option<SerializedWorkspace> {
 904        self.workspace_for_roots_internal(worktree_roots, None)
 905    }
 906
 907    pub(crate) fn remote_workspace_for_roots<P: AsRef<Path>>(
 908        &self,
 909        worktree_roots: &[P],
 910        remote_project_id: RemoteConnectionId,
 911    ) -> Option<SerializedWorkspace> {
 912        self.workspace_for_roots_internal(worktree_roots, Some(remote_project_id))
 913    }
 914
 915    pub(crate) fn workspace_for_roots_internal<P: AsRef<Path>>(
 916        &self,
 917        worktree_roots: &[P],
 918        remote_connection_id: Option<RemoteConnectionId>,
 919    ) -> Option<SerializedWorkspace> {
 920        // paths are sorted before db interactions to ensure that the order of the paths
 921        // doesn't affect the workspace selection for existing workspaces
 922        let root_paths = PathList::new(worktree_roots);
 923
 924        // Note that we re-assign the workspace_id here in case it's empty
 925        // and we've grabbed the most recent workspace
 926        let (
 927            workspace_id,
 928            paths,
 929            paths_order,
 930            window_bounds,
 931            display,
 932            centered_layout,
 933            docks,
 934            window_id,
 935        ): (
 936            WorkspaceId,
 937            String,
 938            String,
 939            Option<SerializedWindowBounds>,
 940            Option<Uuid>,
 941            Option<bool>,
 942            DockStructure,
 943            Option<u64>,
 944        ) = self
 945            .select_row_bound(sql! {
 946                SELECT
 947                    workspace_id,
 948                    paths,
 949                    paths_order,
 950                    window_state,
 951                    window_x,
 952                    window_y,
 953                    window_width,
 954                    window_height,
 955                    display,
 956                    centered_layout,
 957                    left_dock_visible,
 958                    left_dock_active_panel,
 959                    left_dock_zoom,
 960                    right_dock_visible,
 961                    right_dock_active_panel,
 962                    right_dock_zoom,
 963                    bottom_dock_visible,
 964                    bottom_dock_active_panel,
 965                    bottom_dock_zoom,
 966                    window_id
 967                FROM workspaces
 968                WHERE
 969                    paths IS ? AND
 970                    remote_connection_id IS ?
 971                LIMIT 1
 972            })
 973            .and_then(|mut prepared_statement| {
 974                (prepared_statement)((
 975                    root_paths.serialize().paths,
 976                    remote_connection_id.map(|id| id.0 as i32),
 977                ))
 978            })
 979            .context("No workspaces found")
 980            .warn_on_err()
 981            .flatten()?;
 982
 983        let paths = PathList::deserialize(&SerializedPathList {
 984            paths,
 985            order: paths_order,
 986        });
 987
 988        let remote_connection_options = if let Some(remote_connection_id) = remote_connection_id {
 989            self.remote_connection(remote_connection_id)
 990                .context("Get remote connection")
 991                .log_err()
 992        } else {
 993            None
 994        };
 995
 996        Some(SerializedWorkspace {
 997            id: workspace_id,
 998            location: match remote_connection_options {
 999                Some(options) => SerializedWorkspaceLocation::Remote(options),
1000                None => SerializedWorkspaceLocation::Local,
1001            },
1002            paths,
1003            center_group: self
1004                .get_center_pane_group(workspace_id)
1005                .context("Getting center group")
1006                .log_err()?,
1007            window_bounds,
1008            centered_layout: centered_layout.unwrap_or(false),
1009            display,
1010            docks,
1011            session_id: None,
1012            breakpoints: self.breakpoints(workspace_id),
1013            window_id,
1014            user_toolchains: self.user_toolchains(workspace_id, remote_connection_id),
1015        })
1016    }
1017
1018    fn breakpoints(&self, workspace_id: WorkspaceId) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
1019        let breakpoints: Result<Vec<(PathBuf, Breakpoint)>> = self
1020            .select_bound(sql! {
1021                SELECT path, breakpoint_location, log_message, condition, hit_condition, state
1022                FROM breakpoints
1023                WHERE workspace_id = ?
1024            })
1025            .and_then(|mut prepared_statement| (prepared_statement)(workspace_id));
1026
1027        match breakpoints {
1028            Ok(bp) => {
1029                if bp.is_empty() {
1030                    log::debug!("Breakpoints are empty after querying database for them");
1031                }
1032
1033                let mut map: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> = Default::default();
1034
1035                for (path, breakpoint) in bp {
1036                    let path: Arc<Path> = path.into();
1037                    map.entry(path.clone()).or_default().push(SourceBreakpoint {
1038                        row: breakpoint.position,
1039                        path,
1040                        message: breakpoint.message,
1041                        condition: breakpoint.condition,
1042                        hit_condition: breakpoint.hit_condition,
1043                        state: breakpoint.state,
1044                    });
1045                }
1046
1047                for (path, bps) in map.iter() {
1048                    log::info!(
1049                        "Got {} breakpoints from database at path: {}",
1050                        bps.len(),
1051                        path.to_string_lossy()
1052                    );
1053                }
1054
1055                map
1056            }
1057            Err(msg) => {
1058                log::error!("Breakpoints query failed with msg: {msg}");
1059                Default::default()
1060            }
1061        }
1062    }
1063
1064    fn user_toolchains(
1065        &self,
1066        workspace_id: WorkspaceId,
1067        remote_connection_id: Option<RemoteConnectionId>,
1068    ) -> BTreeMap<ToolchainScope, IndexSet<Toolchain>> {
1069        type RowKind = (WorkspaceId, String, String, String, String, String, String);
1070
1071        let toolchains: Vec<RowKind> = self
1072            .select_bound(sql! {
1073                SELECT workspace_id, worktree_root_path, relative_worktree_path,
1074                language_name, name, path, raw_json
1075                FROM user_toolchains WHERE remote_connection_id IS ?1 AND (
1076                      workspace_id IN (0, ?2)
1077                )
1078            })
1079            .and_then(|mut statement| {
1080                (statement)((remote_connection_id.map(|id| id.0), workspace_id))
1081            })
1082            .unwrap_or_default();
1083        let mut ret = BTreeMap::<_, IndexSet<_>>::default();
1084
1085        for (
1086            _workspace_id,
1087            worktree_root_path,
1088            relative_worktree_path,
1089            language_name,
1090            name,
1091            path,
1092            raw_json,
1093        ) in toolchains
1094        {
1095            // INTEGER's that are primary keys (like workspace ids, remote connection ids and such) start at 1, so we're safe to
1096            let scope = if _workspace_id == WorkspaceId(0) {
1097                debug_assert_eq!(worktree_root_path, String::default());
1098                debug_assert_eq!(relative_worktree_path, String::default());
1099                ToolchainScope::Global
1100            } else {
1101                debug_assert_eq!(workspace_id, _workspace_id);
1102                debug_assert_eq!(
1103                    worktree_root_path == String::default(),
1104                    relative_worktree_path == String::default()
1105                );
1106
1107                let Some(relative_path) = RelPath::unix(&relative_worktree_path).log_err() else {
1108                    continue;
1109                };
1110                if worktree_root_path != String::default()
1111                    && relative_worktree_path != String::default()
1112                {
1113                    ToolchainScope::Subproject(
1114                        Arc::from(worktree_root_path.as_ref()),
1115                        relative_path.into(),
1116                    )
1117                } else {
1118                    ToolchainScope::Project
1119                }
1120            };
1121            let Ok(as_json) = serde_json::from_str(&raw_json) else {
1122                continue;
1123            };
1124            let toolchain = Toolchain {
1125                name: SharedString::from(name),
1126                path: SharedString::from(path),
1127                language_name: LanguageName::from_proto(language_name),
1128                as_json,
1129            };
1130            ret.entry(scope).or_default().insert(toolchain);
1131        }
1132
1133        ret
1134    }
1135
1136    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
1137    /// that used this workspace previously
1138    pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
1139        let paths = workspace.paths.serialize();
1140        log::debug!("Saving workspace at location: {:?}", workspace.location);
1141        self.write(move |conn| {
1142            conn.with_savepoint("update_worktrees", || {
1143                let remote_connection_id = match workspace.location.clone() {
1144                    SerializedWorkspaceLocation::Local => None,
1145                    SerializedWorkspaceLocation::Remote(connection_options) => {
1146                        Some(Self::get_or_create_remote_connection_internal(
1147                            conn,
1148                            connection_options
1149                        )?.0)
1150                    }
1151                };
1152
1153                // Clear out panes and pane_groups
1154                conn.exec_bound(sql!(
1155                    DELETE FROM pane_groups WHERE workspace_id = ?1;
1156                    DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
1157                    .context("Clearing old panes")?;
1158
1159                conn.exec_bound(
1160                    sql!(
1161                        DELETE FROM breakpoints WHERE workspace_id = ?1;
1162                    )
1163                )?(workspace.id).context("Clearing old breakpoints")?;
1164
1165                for (path, breakpoints) in workspace.breakpoints {
1166                    for bp in breakpoints {
1167                        let state = BreakpointStateWrapper::from(bp.state);
1168                        match conn.exec_bound(sql!(
1169                            INSERT INTO breakpoints (workspace_id, path, breakpoint_location,  log_message, condition, hit_condition, state)
1170                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);))?
1171
1172                        ((
1173                            workspace.id,
1174                            path.as_ref(),
1175                            bp.row,
1176                            bp.message,
1177                            bp.condition,
1178                            bp.hit_condition,
1179                            state,
1180                        )) {
1181                            Ok(_) => {
1182                                log::debug!("Stored breakpoint at row: {} in path: {}", bp.row, path.to_string_lossy())
1183                            }
1184                            Err(err) => {
1185                                log::error!("{err}");
1186                                continue;
1187                            }
1188                        }
1189                    }
1190                }
1191
1192                conn.exec_bound(
1193                    sql!(
1194                        DELETE FROM user_toolchains WHERE workspace_id = ?1;
1195                    )
1196                )?(workspace.id).context("Clearing old user toolchains")?;
1197
1198                for (scope, toolchains) in workspace.user_toolchains {
1199                    for toolchain in toolchains {
1200                        let query = sql!(INSERT OR REPLACE INTO user_toolchains(remote_connection_id, workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8));
1201                        let (workspace_id, worktree_root_path, relative_worktree_path) = match scope {
1202                            ToolchainScope::Subproject(ref worktree_root_path, ref path) => (Some(workspace.id), Some(worktree_root_path.to_string_lossy().into_owned()), Some(path.as_unix_str().to_owned())),
1203                            ToolchainScope::Project => (Some(workspace.id), None, None),
1204                            ToolchainScope::Global => (None, None, None),
1205                        };
1206                        let args = (remote_connection_id, workspace_id.unwrap_or(WorkspaceId(0)), worktree_root_path.unwrap_or_default(), relative_worktree_path.unwrap_or_default(),
1207                        toolchain.language_name.as_ref().to_owned(), toolchain.name.to_string(), toolchain.path.to_string(), toolchain.as_json.to_string());
1208                        if let Err(err) = conn.exec_bound(query)?(args) {
1209                            log::error!("{err}");
1210                            continue;
1211                        }
1212                    }
1213                }
1214
1215                conn.exec_bound(sql!(
1216                    DELETE
1217                    FROM workspaces
1218                    WHERE
1219                        workspace_id != ?1 AND
1220                        paths IS ?2 AND
1221                        remote_connection_id IS ?3
1222                ))?((
1223                    workspace.id,
1224                    paths.paths.clone(),
1225                    remote_connection_id,
1226                ))
1227                .context("clearing out old locations")?;
1228
1229                // Upsert
1230                let query = sql!(
1231                    INSERT INTO workspaces(
1232                        workspace_id,
1233                        paths,
1234                        paths_order,
1235                        remote_connection_id,
1236                        left_dock_visible,
1237                        left_dock_active_panel,
1238                        left_dock_zoom,
1239                        right_dock_visible,
1240                        right_dock_active_panel,
1241                        right_dock_zoom,
1242                        bottom_dock_visible,
1243                        bottom_dock_active_panel,
1244                        bottom_dock_zoom,
1245                        session_id,
1246                        window_id,
1247                        timestamp
1248                    )
1249                    VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, CURRENT_TIMESTAMP)
1250                    ON CONFLICT DO
1251                    UPDATE SET
1252                        paths = ?2,
1253                        paths_order = ?3,
1254                        remote_connection_id = ?4,
1255                        left_dock_visible = ?5,
1256                        left_dock_active_panel = ?6,
1257                        left_dock_zoom = ?7,
1258                        right_dock_visible = ?8,
1259                        right_dock_active_panel = ?9,
1260                        right_dock_zoom = ?10,
1261                        bottom_dock_visible = ?11,
1262                        bottom_dock_active_panel = ?12,
1263                        bottom_dock_zoom = ?13,
1264                        session_id = ?14,
1265                        window_id = ?15,
1266                        timestamp = CURRENT_TIMESTAMP
1267                );
1268                let mut prepared_query = conn.exec_bound(query)?;
1269                let args = (
1270                    workspace.id,
1271                    paths.paths.clone(),
1272                    paths.order.clone(),
1273                    remote_connection_id,
1274                    workspace.docks,
1275                    workspace.session_id,
1276                    workspace.window_id,
1277                );
1278
1279                prepared_query(args).context("Updating workspace")?;
1280
1281                // Save center pane group
1282                Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
1283                    .context("save pane group in save workspace")?;
1284
1285                Ok(())
1286            })
1287            .log_err();
1288        })
1289        .await;
1290    }
1291
1292    pub(crate) async fn get_or_create_remote_connection(
1293        &self,
1294        options: RemoteConnectionOptions,
1295    ) -> Result<RemoteConnectionId> {
1296        self.write(move |conn| Self::get_or_create_remote_connection_internal(conn, options))
1297            .await
1298    }
1299
1300    fn get_or_create_remote_connection_internal(
1301        this: &Connection,
1302        options: RemoteConnectionOptions,
1303    ) -> Result<RemoteConnectionId> {
1304        let kind;
1305        let mut user = None;
1306        let mut host = None;
1307        let mut port = None;
1308        let mut distro = None;
1309        let mut name = None;
1310        let mut container_id = None;
1311        match options {
1312            RemoteConnectionOptions::Ssh(options) => {
1313                kind = RemoteConnectionKind::Ssh;
1314                host = Some(options.host.to_string());
1315                port = options.port;
1316                user = options.username;
1317            }
1318            RemoteConnectionOptions::Wsl(options) => {
1319                kind = RemoteConnectionKind::Wsl;
1320                distro = Some(options.distro_name);
1321                user = options.user;
1322            }
1323            RemoteConnectionOptions::Docker(options) => {
1324                kind = RemoteConnectionKind::Docker;
1325                container_id = Some(options.container_id);
1326                name = Some(options.name);
1327            }
1328            #[cfg(any(test, feature = "test-support"))]
1329            RemoteConnectionOptions::Mock(options) => {
1330                kind = RemoteConnectionKind::Ssh;
1331                host = Some(format!("mock-{}", options.id));
1332            }
1333        }
1334        Self::get_or_create_remote_connection_query(
1335            this,
1336            kind,
1337            host,
1338            port,
1339            user,
1340            distro,
1341            name,
1342            container_id,
1343        )
1344    }
1345
1346    fn get_or_create_remote_connection_query(
1347        this: &Connection,
1348        kind: RemoteConnectionKind,
1349        host: Option<String>,
1350        port: Option<u16>,
1351        user: Option<String>,
1352        distro: Option<String>,
1353        name: Option<String>,
1354        container_id: Option<String>,
1355    ) -> Result<RemoteConnectionId> {
1356        if let Some(id) = this.select_row_bound(sql!(
1357            SELECT id
1358            FROM remote_connections
1359            WHERE
1360                kind IS ? AND
1361                host IS ? AND
1362                port IS ? AND
1363                user IS ? AND
1364                distro IS ? AND
1365                name IS ? AND
1366                container_id IS ?
1367            LIMIT 1
1368        ))?((
1369            kind.serialize(),
1370            host.clone(),
1371            port,
1372            user.clone(),
1373            distro.clone(),
1374            name.clone(),
1375            container_id.clone(),
1376        ))? {
1377            Ok(RemoteConnectionId(id))
1378        } else {
1379            let id = this.select_row_bound(sql!(
1380                INSERT INTO remote_connections (
1381                    kind,
1382                    host,
1383                    port,
1384                    user,
1385                    distro,
1386                    name,
1387                    container_id
1388                ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
1389                RETURNING id
1390            ))?((
1391                kind.serialize(),
1392                host,
1393                port,
1394                user,
1395                distro,
1396                name,
1397                container_id,
1398            ))?
1399            .context("failed to insert remote project")?;
1400            Ok(RemoteConnectionId(id))
1401        }
1402    }
1403
1404    query! {
1405        pub async fn next_id() -> Result<WorkspaceId> {
1406            INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
1407        }
1408    }
1409
1410    fn recent_workspaces(
1411        &self,
1412    ) -> Result<Vec<(WorkspaceId, PathList, Option<RemoteConnectionId>)>> {
1413        Ok(self
1414            .recent_workspaces_query()?
1415            .into_iter()
1416            .map(|(id, paths, order, remote_connection_id)| {
1417                (
1418                    id,
1419                    PathList::deserialize(&SerializedPathList { paths, order }),
1420                    remote_connection_id.map(RemoteConnectionId),
1421                )
1422            })
1423            .collect())
1424    }
1425
1426    query! {
1427        fn recent_workspaces_query() -> Result<Vec<(WorkspaceId, String, String, Option<u64>)>> {
1428            SELECT workspace_id, paths, paths_order, remote_connection_id
1429            FROM workspaces
1430            WHERE
1431                paths IS NOT NULL OR
1432                remote_connection_id IS NOT NULL
1433            ORDER BY timestamp DESC
1434        }
1435    }
1436
1437    fn session_workspaces(
1438        &self,
1439        session_id: String,
1440    ) -> Result<Vec<(PathList, Option<u64>, Option<RemoteConnectionId>)>> {
1441        Ok(self
1442            .session_workspaces_query(session_id)?
1443            .into_iter()
1444            .map(|(paths, order, window_id, remote_connection_id)| {
1445                (
1446                    PathList::deserialize(&SerializedPathList { paths, order }),
1447                    window_id,
1448                    remote_connection_id.map(RemoteConnectionId),
1449                )
1450            })
1451            .collect())
1452    }
1453
1454    query! {
1455        fn session_workspaces_query(session_id: String) -> Result<Vec<(String, String, Option<u64>, Option<u64>)>> {
1456            SELECT paths, paths_order, window_id, remote_connection_id
1457            FROM workspaces
1458            WHERE session_id = ?1
1459            ORDER BY timestamp DESC
1460        }
1461    }
1462
1463    query! {
1464        pub fn breakpoints_for_file(workspace_id: WorkspaceId, file_path: &Path) -> Result<Vec<Breakpoint>> {
1465            SELECT breakpoint_location
1466            FROM breakpoints
1467            WHERE  workspace_id= ?1 AND path = ?2
1468        }
1469    }
1470
1471    query! {
1472        pub fn clear_breakpoints(file_path: &Path) -> Result<()> {
1473            DELETE FROM breakpoints
1474            WHERE file_path = ?2
1475        }
1476    }
1477
1478    fn remote_connections(&self) -> Result<HashMap<RemoteConnectionId, RemoteConnectionOptions>> {
1479        Ok(self.select(sql!(
1480            SELECT
1481                id, kind, host, port, user, distro, container_id, name
1482            FROM
1483                remote_connections
1484        ))?()?
1485        .into_iter()
1486        .filter_map(|(id, kind, host, port, user, distro, container_id, name)| {
1487            Some((
1488                RemoteConnectionId(id),
1489                Self::remote_connection_from_row(
1490                    kind,
1491                    host,
1492                    port,
1493                    user,
1494                    distro,
1495                    container_id,
1496                    name,
1497                )?,
1498            ))
1499        })
1500        .collect())
1501    }
1502
1503    pub(crate) fn remote_connection(
1504        &self,
1505        id: RemoteConnectionId,
1506    ) -> Result<RemoteConnectionOptions> {
1507        let (kind, host, port, user, distro, container_id, name) = self.select_row_bound(sql!(
1508            SELECT kind, host, port, user, distro, container_id, name
1509            FROM remote_connections
1510            WHERE id = ?
1511        ))?(id.0)?
1512        .context("no such remote connection")?;
1513        Self::remote_connection_from_row(kind, host, port, user, distro, container_id, name)
1514            .context("invalid remote_connection row")
1515    }
1516
1517    fn remote_connection_from_row(
1518        kind: String,
1519        host: Option<String>,
1520        port: Option<u16>,
1521        user: Option<String>,
1522        distro: Option<String>,
1523        container_id: Option<String>,
1524        name: Option<String>,
1525    ) -> Option<RemoteConnectionOptions> {
1526        match RemoteConnectionKind::deserialize(&kind)? {
1527            RemoteConnectionKind::Wsl => Some(RemoteConnectionOptions::Wsl(WslConnectionOptions {
1528                distro_name: distro?,
1529                user: user,
1530            })),
1531            RemoteConnectionKind::Ssh => Some(RemoteConnectionOptions::Ssh(SshConnectionOptions {
1532                host: host?.into(),
1533                port,
1534                username: user,
1535                ..Default::default()
1536            })),
1537            RemoteConnectionKind::Docker => {
1538                Some(RemoteConnectionOptions::Docker(DockerConnectionOptions {
1539                    container_id: container_id?,
1540                    name: name?,
1541                    upload_binary_over_docker_exec: false,
1542                }))
1543            }
1544        }
1545    }
1546
1547    query! {
1548        pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
1549            DELETE FROM workspaces
1550            WHERE workspace_id IS ?
1551        }
1552    }
1553
1554    // Returns the recent locations which are still valid on disk and deletes ones which no longer
1555    // exist.
1556    pub async fn recent_workspaces_on_disk(
1557        &self,
1558    ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
1559        let mut result = Vec::new();
1560        let mut delete_tasks = Vec::new();
1561        let remote_connections = self.remote_connections()?;
1562
1563        for (id, paths, remote_connection_id) in self.recent_workspaces()? {
1564            if let Some(remote_connection_id) = remote_connection_id {
1565                if let Some(connection_options) = remote_connections.get(&remote_connection_id) {
1566                    result.push((
1567                        id,
1568                        SerializedWorkspaceLocation::Remote(connection_options.clone()),
1569                        paths,
1570                    ));
1571                } else {
1572                    delete_tasks.push(self.delete_workspace_by_id(id));
1573                }
1574                continue;
1575            }
1576
1577            let has_wsl_path = if cfg!(windows) {
1578                paths
1579                    .paths()
1580                    .iter()
1581                    .any(|path| util::paths::WslPath::from_path(path).is_some())
1582            } else {
1583                false
1584            };
1585
1586            // Delete the workspace if any of the paths are WSL paths.
1587            // If a local workspace points to WSL, this check will cause us to wait for the
1588            // WSL VM and file server to boot up. This can block for many seconds.
1589            // Supported scenarios use remote workspaces.
1590            if !has_wsl_path && paths.paths().iter().all(|path| path.exists()) {
1591                // Only show directories in recent projects
1592                if paths.paths().iter().any(|path| path.is_dir()) {
1593                    result.push((id, SerializedWorkspaceLocation::Local, paths));
1594                }
1595            } else {
1596                delete_tasks.push(self.delete_workspace_by_id(id));
1597            }
1598        }
1599
1600        futures::future::join_all(delete_tasks).await;
1601        Ok(result)
1602    }
1603
1604    pub async fn last_workspace(&self) -> Result<Option<(SerializedWorkspaceLocation, PathList)>> {
1605        Ok(self
1606            .recent_workspaces_on_disk()
1607            .await?
1608            .into_iter()
1609            .next()
1610            .map(|(_, location, paths)| (location, paths)))
1611    }
1612
1613    // Returns the locations of the workspaces that were still opened when the last
1614    // session was closed (i.e. when Zed was quit).
1615    // If `last_session_window_order` is provided, the returned locations are ordered
1616    // according to that.
1617    pub fn last_session_workspace_locations(
1618        &self,
1619        last_session_id: &str,
1620        last_session_window_stack: Option<Vec<WindowId>>,
1621    ) -> Result<Vec<(SerializedWorkspaceLocation, PathList)>> {
1622        let mut workspaces = Vec::new();
1623
1624        for (paths, window_id, remote_connection_id) in
1625            self.session_workspaces(last_session_id.to_owned())?
1626        {
1627            if let Some(remote_connection_id) = remote_connection_id {
1628                workspaces.push((
1629                    SerializedWorkspaceLocation::Remote(
1630                        self.remote_connection(remote_connection_id)?,
1631                    ),
1632                    paths,
1633                    window_id.map(WindowId::from),
1634                ));
1635            } else if paths.paths().iter().all(|path| path.exists())
1636                && paths.paths().iter().any(|path| path.is_dir())
1637            {
1638                workspaces.push((
1639                    SerializedWorkspaceLocation::Local,
1640                    paths,
1641                    window_id.map(WindowId::from),
1642                ));
1643            }
1644        }
1645
1646        if let Some(stack) = last_session_window_stack {
1647            workspaces.sort_by_key(|(_, _, window_id)| {
1648                window_id
1649                    .and_then(|id| stack.iter().position(|&order_id| order_id == id))
1650                    .unwrap_or(usize::MAX)
1651            });
1652        }
1653
1654        Ok(workspaces
1655            .into_iter()
1656            .map(|(location, paths, _)| (location, paths))
1657            .collect::<Vec<_>>())
1658    }
1659
1660    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1661        Ok(self
1662            .get_pane_group(workspace_id, None)?
1663            .into_iter()
1664            .next()
1665            .unwrap_or_else(|| {
1666                SerializedPaneGroup::Pane(SerializedPane {
1667                    active: true,
1668                    children: vec![],
1669                    pinned_count: 0,
1670                })
1671            }))
1672    }
1673
1674    fn get_pane_group(
1675        &self,
1676        workspace_id: WorkspaceId,
1677        group_id: Option<GroupId>,
1678    ) -> Result<Vec<SerializedPaneGroup>> {
1679        type GroupKey = (Option<GroupId>, WorkspaceId);
1680        type GroupOrPane = (
1681            Option<GroupId>,
1682            Option<SerializedAxis>,
1683            Option<PaneId>,
1684            Option<bool>,
1685            Option<usize>,
1686            Option<String>,
1687        );
1688        self.select_bound::<GroupKey, GroupOrPane>(sql!(
1689            SELECT group_id, axis, pane_id, active, pinned_count, flexes
1690                FROM (SELECT
1691                        group_id,
1692                        axis,
1693                        NULL as pane_id,
1694                        NULL as active,
1695                        NULL as pinned_count,
1696                        position,
1697                        parent_group_id,
1698                        workspace_id,
1699                        flexes
1700                      FROM pane_groups
1701                    UNION
1702                      SELECT
1703                        NULL,
1704                        NULL,
1705                        center_panes.pane_id,
1706                        panes.active as active,
1707                        pinned_count,
1708                        position,
1709                        parent_group_id,
1710                        panes.workspace_id as workspace_id,
1711                        NULL
1712                      FROM center_panes
1713                      JOIN panes ON center_panes.pane_id = panes.pane_id)
1714                WHERE parent_group_id IS ? AND workspace_id = ?
1715                ORDER BY position
1716        ))?((group_id, workspace_id))?
1717        .into_iter()
1718        .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1719            let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1720            if let Some((group_id, axis)) = group_id.zip(axis) {
1721                let flexes = flexes
1722                    .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1723                    .transpose()?;
1724
1725                Ok(SerializedPaneGroup::Group {
1726                    axis,
1727                    children: self.get_pane_group(workspace_id, Some(group_id))?,
1728                    flexes,
1729                })
1730            } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
1731                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
1732                    self.get_items(pane_id)?,
1733                    active,
1734                    pinned_count,
1735                )))
1736            } else {
1737                bail!("Pane Group Child was neither a pane group or a pane");
1738            }
1739        })
1740        // Filter out panes and pane groups which don't have any children or items
1741        .filter(|pane_group| match pane_group {
1742            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
1743            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
1744            _ => true,
1745        })
1746        .collect::<Result<_>>()
1747    }
1748
1749    fn save_pane_group(
1750        conn: &Connection,
1751        workspace_id: WorkspaceId,
1752        pane_group: &SerializedPaneGroup,
1753        parent: Option<(GroupId, usize)>,
1754    ) -> Result<()> {
1755        if parent.is_none() {
1756            log::debug!("Saving a pane group for workspace {workspace_id:?}");
1757        }
1758        match pane_group {
1759            SerializedPaneGroup::Group {
1760                axis,
1761                children,
1762                flexes,
1763            } => {
1764                let (parent_id, position) = parent.unzip();
1765
1766                let flex_string = flexes
1767                    .as_ref()
1768                    .map(|flexes| serde_json::json!(flexes).to_string());
1769
1770                let group_id = conn.select_row_bound::<_, i64>(sql!(
1771                    INSERT INTO pane_groups(
1772                        workspace_id,
1773                        parent_group_id,
1774                        position,
1775                        axis,
1776                        flexes
1777                    )
1778                    VALUES (?, ?, ?, ?, ?)
1779                    RETURNING group_id
1780                ))?((
1781                    workspace_id,
1782                    parent_id,
1783                    position,
1784                    *axis,
1785                    flex_string,
1786                ))?
1787                .context("Couldn't retrieve group_id from inserted pane_group")?;
1788
1789                for (position, group) in children.iter().enumerate() {
1790                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
1791                }
1792
1793                Ok(())
1794            }
1795            SerializedPaneGroup::Pane(pane) => {
1796                Self::save_pane(conn, workspace_id, pane, parent)?;
1797                Ok(())
1798            }
1799        }
1800    }
1801
1802    fn save_pane(
1803        conn: &Connection,
1804        workspace_id: WorkspaceId,
1805        pane: &SerializedPane,
1806        parent: Option<(GroupId, usize)>,
1807    ) -> Result<PaneId> {
1808        let pane_id = conn.select_row_bound::<_, i64>(sql!(
1809            INSERT INTO panes(workspace_id, active, pinned_count)
1810            VALUES (?, ?, ?)
1811            RETURNING pane_id
1812        ))?((workspace_id, pane.active, pane.pinned_count))?
1813        .context("Could not retrieve inserted pane_id")?;
1814
1815        let (parent_id, order) = parent.unzip();
1816        conn.exec_bound(sql!(
1817            INSERT INTO center_panes(pane_id, parent_group_id, position)
1818            VALUES (?, ?, ?)
1819        ))?((pane_id, parent_id, order))?;
1820
1821        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
1822
1823        Ok(pane_id)
1824    }
1825
1826    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
1827        self.select_bound(sql!(
1828            SELECT kind, item_id, active, preview FROM items
1829            WHERE pane_id = ?
1830                ORDER BY position
1831        ))?(pane_id)
1832    }
1833
1834    fn save_items(
1835        conn: &Connection,
1836        workspace_id: WorkspaceId,
1837        pane_id: PaneId,
1838        items: &[SerializedItem],
1839    ) -> Result<()> {
1840        let mut insert = conn.exec_bound(sql!(
1841            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
1842        )).context("Preparing insertion")?;
1843        for (position, item) in items.iter().enumerate() {
1844            insert((workspace_id, pane_id, position, item))?;
1845        }
1846
1847        Ok(())
1848    }
1849
1850    query! {
1851        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
1852            UPDATE workspaces
1853            SET timestamp = CURRENT_TIMESTAMP
1854            WHERE workspace_id = ?
1855        }
1856    }
1857
1858    query! {
1859        pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
1860            UPDATE workspaces
1861            SET window_state = ?2,
1862                window_x = ?3,
1863                window_y = ?4,
1864                window_width = ?5,
1865                window_height = ?6,
1866                display = ?7
1867            WHERE workspace_id = ?1
1868        }
1869    }
1870
1871    query! {
1872        pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
1873            UPDATE workspaces
1874            SET centered_layout = ?2
1875            WHERE workspace_id = ?1
1876        }
1877    }
1878
1879    query! {
1880        pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option<String>) -> Result<()> {
1881            UPDATE workspaces
1882            SET session_id = ?2
1883            WHERE workspace_id = ?1
1884        }
1885    }
1886
1887    pub(crate) async fn toolchains(
1888        &self,
1889        workspace_id: WorkspaceId,
1890    ) -> Result<Vec<(Toolchain, Arc<Path>, Arc<RelPath>)>> {
1891        self.write(move |this| {
1892            let mut select = this
1893                .select_bound(sql!(
1894                    SELECT
1895                        name, path, worktree_root_path, relative_worktree_path, language_name, raw_json
1896                    FROM toolchains
1897                    WHERE workspace_id = ?
1898                ))
1899                .context("select toolchains")?;
1900
1901            let toolchain: Vec<(String, String, String, String, String, String)> =
1902                select(workspace_id)?;
1903
1904            Ok(toolchain
1905                .into_iter()
1906                .filter_map(
1907                    |(name, path, worktree_root_path, relative_worktree_path, language, json)| {
1908                        Some((
1909                            Toolchain {
1910                                name: name.into(),
1911                                path: path.into(),
1912                                language_name: LanguageName::new(&language),
1913                                as_json: serde_json::Value::from_str(&json).ok()?,
1914                            },
1915                           Arc::from(worktree_root_path.as_ref()),
1916                            RelPath::from_proto(&relative_worktree_path).log_err()?,
1917                        ))
1918                    },
1919                )
1920                .collect())
1921        })
1922        .await
1923    }
1924
1925    pub async fn set_toolchain(
1926        &self,
1927        workspace_id: WorkspaceId,
1928        worktree_root_path: Arc<Path>,
1929        relative_worktree_path: Arc<RelPath>,
1930        toolchain: Toolchain,
1931    ) -> Result<()> {
1932        log::debug!(
1933            "Setting toolchain for workspace, worktree: {worktree_root_path:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
1934            toolchain.name
1935        );
1936        self.write(move |conn| {
1937            let mut insert = conn
1938                .exec_bound(sql!(
1939                    INSERT INTO toolchains(workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?,  ?, ?)
1940                    ON CONFLICT DO
1941                    UPDATE SET
1942                        name = ?5,
1943                        path = ?6,
1944                        raw_json = ?7
1945                ))
1946                .context("Preparing insertion")?;
1947
1948            insert((
1949                workspace_id,
1950                worktree_root_path.to_string_lossy().into_owned(),
1951                relative_worktree_path.as_unix_str(),
1952                toolchain.language_name.as_ref(),
1953                toolchain.name.as_ref(),
1954                toolchain.path.as_ref(),
1955                toolchain.as_json.to_string(),
1956            ))?;
1957
1958            Ok(())
1959        }).await
1960    }
1961
1962    pub(crate) async fn save_trusted_worktrees(
1963        &self,
1964        trusted_worktrees: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
1965    ) -> anyhow::Result<()> {
1966        use anyhow::Context as _;
1967        use db::sqlez::statement::Statement;
1968        use itertools::Itertools as _;
1969
1970        DB.clear_trusted_worktrees()
1971            .await
1972            .context("clearing previous trust state")?;
1973
1974        let trusted_worktrees = trusted_worktrees
1975            .into_iter()
1976            .flat_map(|(host, abs_paths)| {
1977                abs_paths
1978                    .into_iter()
1979                    .map(move |abs_path| (Some(abs_path), host.clone()))
1980            })
1981            .collect::<Vec<_>>();
1982        let mut first_worktree;
1983        let mut last_worktree = 0_usize;
1984        for (count, placeholders) in std::iter::once("(?, ?, ?)")
1985            .cycle()
1986            .take(trusted_worktrees.len())
1987            .chunks(MAX_QUERY_PLACEHOLDERS / 3)
1988            .into_iter()
1989            .map(|chunk| {
1990                let mut count = 0;
1991                let placeholders = chunk
1992                    .inspect(|_| {
1993                        count += 1;
1994                    })
1995                    .join(", ");
1996                (count, placeholders)
1997            })
1998            .collect::<Vec<_>>()
1999        {
2000            first_worktree = last_worktree;
2001            last_worktree = last_worktree + count;
2002            let query = format!(
2003                r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name)
2004VALUES {placeholders};"#
2005            );
2006
2007            let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec();
2008            self.write(move |conn| {
2009                let mut statement = Statement::prepare(conn, query)?;
2010                let mut next_index = 1;
2011                for (abs_path, host) in trusted_worktrees {
2012                    let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy());
2013                    next_index = statement.bind(
2014                        &abs_path.as_ref().map(|abs_path| abs_path.as_ref()),
2015                        next_index,
2016                    )?;
2017                    next_index = statement.bind(
2018                        &host
2019                            .as_ref()
2020                            .and_then(|host| Some(host.user_name.as_ref()?.as_str())),
2021                        next_index,
2022                    )?;
2023                    next_index = statement.bind(
2024                        &host.as_ref().map(|host| host.host_identifier.as_str()),
2025                        next_index,
2026                    )?;
2027                }
2028                statement.exec()
2029            })
2030            .await
2031            .context("inserting new trusted state")?;
2032        }
2033        Ok(())
2034    }
2035
2036    pub fn fetch_trusted_worktrees(&self) -> Result<DbTrustedPaths> {
2037        let trusted_worktrees = DB.trusted_worktrees()?;
2038        Ok(trusted_worktrees
2039            .into_iter()
2040            .filter_map(|(abs_path, user_name, host_name)| {
2041                let db_host = match (user_name, host_name) {
2042                    (None, Some(host_name)) => Some(RemoteHostLocation {
2043                        user_name: None,
2044                        host_identifier: SharedString::new(host_name),
2045                    }),
2046                    (Some(user_name), Some(host_name)) => Some(RemoteHostLocation {
2047                        user_name: Some(SharedString::new(user_name)),
2048                        host_identifier: SharedString::new(host_name),
2049                    }),
2050                    _ => None,
2051                };
2052                Some((db_host, abs_path?))
2053            })
2054            .fold(HashMap::default(), |mut acc, (remote_host, abs_path)| {
2055                acc.entry(remote_host)
2056                    .or_insert_with(HashSet::default)
2057                    .insert(abs_path);
2058                acc
2059            }))
2060    }
2061
2062    query! {
2063        fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
2064            SELECT absolute_path, user_name, host_name
2065            FROM trusted_worktrees
2066        }
2067    }
2068
2069    query! {
2070        pub async fn clear_trusted_worktrees() -> Result<()> {
2071            DELETE FROM trusted_worktrees
2072        }
2073    }
2074}
2075
2076pub fn delete_unloaded_items(
2077    alive_items: Vec<ItemId>,
2078    workspace_id: WorkspaceId,
2079    table: &'static str,
2080    db: &ThreadSafeConnection,
2081    cx: &mut App,
2082) -> Task<Result<()>> {
2083    let db = db.clone();
2084    cx.spawn(async move |_| {
2085        let placeholders = alive_items
2086            .iter()
2087            .map(|_| "?")
2088            .collect::<Vec<&str>>()
2089            .join(", ");
2090
2091        let query = format!(
2092            "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
2093        );
2094
2095        db.write(move |conn| {
2096            let mut statement = Statement::prepare(conn, query)?;
2097            let mut next_index = statement.bind(&workspace_id, 1)?;
2098            for id in alive_items {
2099                next_index = statement.bind(&id, next_index)?;
2100            }
2101            statement.exec()
2102        })
2103        .await
2104    })
2105}
2106
2107#[cfg(test)]
2108mod tests {
2109    use super::*;
2110    use crate::persistence::model::{
2111        SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
2112    };
2113    use gpui;
2114    use pretty_assertions::assert_eq;
2115    use remote::SshConnectionOptions;
2116    use std::{thread, time::Duration};
2117
2118    #[gpui::test]
2119    async fn test_breakpoints() {
2120        zlog::init_test();
2121
2122        let db = WorkspaceDb::open_test_db("test_breakpoints").await;
2123        let id = db.next_id().await.unwrap();
2124
2125        let path = Path::new("/tmp/test.rs");
2126
2127        let breakpoint = Breakpoint {
2128            position: 123,
2129            message: None,
2130            state: BreakpointState::Enabled,
2131            condition: None,
2132            hit_condition: None,
2133        };
2134
2135        let log_breakpoint = Breakpoint {
2136            position: 456,
2137            message: Some("Test log message".into()),
2138            state: BreakpointState::Enabled,
2139            condition: None,
2140            hit_condition: None,
2141        };
2142
2143        let disable_breakpoint = Breakpoint {
2144            position: 578,
2145            message: None,
2146            state: BreakpointState::Disabled,
2147            condition: None,
2148            hit_condition: None,
2149        };
2150
2151        let condition_breakpoint = Breakpoint {
2152            position: 789,
2153            message: None,
2154            state: BreakpointState::Enabled,
2155            condition: Some("x > 5".into()),
2156            hit_condition: None,
2157        };
2158
2159        let hit_condition_breakpoint = Breakpoint {
2160            position: 999,
2161            message: None,
2162            state: BreakpointState::Enabled,
2163            condition: None,
2164            hit_condition: Some(">= 3".into()),
2165        };
2166
2167        let workspace = SerializedWorkspace {
2168            id,
2169            paths: PathList::new(&["/tmp"]),
2170            location: SerializedWorkspaceLocation::Local,
2171            center_group: Default::default(),
2172            window_bounds: Default::default(),
2173            display: Default::default(),
2174            docks: Default::default(),
2175            centered_layout: false,
2176            breakpoints: {
2177                let mut map = collections::BTreeMap::default();
2178                map.insert(
2179                    Arc::from(path),
2180                    vec![
2181                        SourceBreakpoint {
2182                            row: breakpoint.position,
2183                            path: Arc::from(path),
2184                            message: breakpoint.message.clone(),
2185                            state: breakpoint.state,
2186                            condition: breakpoint.condition.clone(),
2187                            hit_condition: breakpoint.hit_condition.clone(),
2188                        },
2189                        SourceBreakpoint {
2190                            row: log_breakpoint.position,
2191                            path: Arc::from(path),
2192                            message: log_breakpoint.message.clone(),
2193                            state: log_breakpoint.state,
2194                            condition: log_breakpoint.condition.clone(),
2195                            hit_condition: log_breakpoint.hit_condition.clone(),
2196                        },
2197                        SourceBreakpoint {
2198                            row: disable_breakpoint.position,
2199                            path: Arc::from(path),
2200                            message: disable_breakpoint.message.clone(),
2201                            state: disable_breakpoint.state,
2202                            condition: disable_breakpoint.condition.clone(),
2203                            hit_condition: disable_breakpoint.hit_condition.clone(),
2204                        },
2205                        SourceBreakpoint {
2206                            row: condition_breakpoint.position,
2207                            path: Arc::from(path),
2208                            message: condition_breakpoint.message.clone(),
2209                            state: condition_breakpoint.state,
2210                            condition: condition_breakpoint.condition.clone(),
2211                            hit_condition: condition_breakpoint.hit_condition.clone(),
2212                        },
2213                        SourceBreakpoint {
2214                            row: hit_condition_breakpoint.position,
2215                            path: Arc::from(path),
2216                            message: hit_condition_breakpoint.message.clone(),
2217                            state: hit_condition_breakpoint.state,
2218                            condition: hit_condition_breakpoint.condition.clone(),
2219                            hit_condition: hit_condition_breakpoint.hit_condition.clone(),
2220                        },
2221                    ],
2222                );
2223                map
2224            },
2225            session_id: None,
2226            window_id: None,
2227            user_toolchains: Default::default(),
2228        };
2229
2230        db.save_workspace(workspace.clone()).await;
2231
2232        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2233        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
2234
2235        assert_eq!(loaded_breakpoints.len(), 5);
2236
2237        // normal breakpoint
2238        assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
2239        assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
2240        assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
2241        assert_eq!(
2242            loaded_breakpoints[0].hit_condition,
2243            breakpoint.hit_condition
2244        );
2245        assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
2246        assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
2247
2248        // enabled breakpoint
2249        assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
2250        assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
2251        assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
2252        assert_eq!(
2253            loaded_breakpoints[1].hit_condition,
2254            log_breakpoint.hit_condition
2255        );
2256        assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
2257        assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
2258
2259        // disable breakpoint
2260        assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
2261        assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
2262        assert_eq!(
2263            loaded_breakpoints[2].condition,
2264            disable_breakpoint.condition
2265        );
2266        assert_eq!(
2267            loaded_breakpoints[2].hit_condition,
2268            disable_breakpoint.hit_condition
2269        );
2270        assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
2271        assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
2272
2273        // condition breakpoint
2274        assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
2275        assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
2276        assert_eq!(
2277            loaded_breakpoints[3].condition,
2278            condition_breakpoint.condition
2279        );
2280        assert_eq!(
2281            loaded_breakpoints[3].hit_condition,
2282            condition_breakpoint.hit_condition
2283        );
2284        assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
2285        assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
2286
2287        // hit condition breakpoint
2288        assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
2289        assert_eq!(
2290            loaded_breakpoints[4].message,
2291            hit_condition_breakpoint.message
2292        );
2293        assert_eq!(
2294            loaded_breakpoints[4].condition,
2295            hit_condition_breakpoint.condition
2296        );
2297        assert_eq!(
2298            loaded_breakpoints[4].hit_condition,
2299            hit_condition_breakpoint.hit_condition
2300        );
2301        assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
2302        assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
2303    }
2304
2305    #[gpui::test]
2306    async fn test_remove_last_breakpoint() {
2307        zlog::init_test();
2308
2309        let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
2310        let id = db.next_id().await.unwrap();
2311
2312        let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
2313
2314        let breakpoint_to_remove = Breakpoint {
2315            position: 100,
2316            message: None,
2317            state: BreakpointState::Enabled,
2318            condition: None,
2319            hit_condition: None,
2320        };
2321
2322        let workspace = SerializedWorkspace {
2323            id,
2324            paths: PathList::new(&["/tmp"]),
2325            location: SerializedWorkspaceLocation::Local,
2326            center_group: Default::default(),
2327            window_bounds: Default::default(),
2328            display: Default::default(),
2329            docks: Default::default(),
2330            centered_layout: false,
2331            breakpoints: {
2332                let mut map = collections::BTreeMap::default();
2333                map.insert(
2334                    Arc::from(singular_path),
2335                    vec![SourceBreakpoint {
2336                        row: breakpoint_to_remove.position,
2337                        path: Arc::from(singular_path),
2338                        message: None,
2339                        state: BreakpointState::Enabled,
2340                        condition: None,
2341                        hit_condition: None,
2342                    }],
2343                );
2344                map
2345            },
2346            session_id: None,
2347            window_id: None,
2348            user_toolchains: Default::default(),
2349        };
2350
2351        db.save_workspace(workspace.clone()).await;
2352
2353        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2354        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
2355
2356        assert_eq!(loaded_breakpoints.len(), 1);
2357        assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
2358        assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
2359        assert_eq!(
2360            loaded_breakpoints[0].condition,
2361            breakpoint_to_remove.condition
2362        );
2363        assert_eq!(
2364            loaded_breakpoints[0].hit_condition,
2365            breakpoint_to_remove.hit_condition
2366        );
2367        assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
2368        assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
2369
2370        let workspace_without_breakpoint = SerializedWorkspace {
2371            id,
2372            paths: PathList::new(&["/tmp"]),
2373            location: SerializedWorkspaceLocation::Local,
2374            center_group: Default::default(),
2375            window_bounds: Default::default(),
2376            display: Default::default(),
2377            docks: Default::default(),
2378            centered_layout: false,
2379            breakpoints: collections::BTreeMap::default(),
2380            session_id: None,
2381            window_id: None,
2382            user_toolchains: Default::default(),
2383        };
2384
2385        db.save_workspace(workspace_without_breakpoint.clone())
2386            .await;
2387
2388        let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
2389        let empty_breakpoints = loaded_after_remove
2390            .breakpoints
2391            .get(&Arc::from(singular_path));
2392
2393        assert!(empty_breakpoints.is_none());
2394    }
2395
2396    #[gpui::test]
2397    async fn test_next_id_stability() {
2398        zlog::init_test();
2399
2400        let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
2401
2402        db.write(|conn| {
2403            conn.migrate(
2404                "test_table",
2405                &[sql!(
2406                    CREATE TABLE test_table(
2407                        text TEXT,
2408                        workspace_id INTEGER,
2409                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2410                        ON DELETE CASCADE
2411                    ) STRICT;
2412                )],
2413                |_, _, _| false,
2414            )
2415            .unwrap();
2416        })
2417        .await;
2418
2419        let id = db.next_id().await.unwrap();
2420        // Assert the empty row got inserted
2421        assert_eq!(
2422            Some(id),
2423            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
2424                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
2425            ))
2426            .unwrap()(id)
2427            .unwrap()
2428        );
2429
2430        db.write(move |conn| {
2431            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2432                .unwrap()(("test-text-1", id))
2433            .unwrap()
2434        })
2435        .await;
2436
2437        let test_text_1 = db
2438            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2439            .unwrap()(1)
2440        .unwrap()
2441        .unwrap();
2442        assert_eq!(test_text_1, "test-text-1");
2443    }
2444
2445    #[gpui::test]
2446    async fn test_workspace_id_stability() {
2447        zlog::init_test();
2448
2449        let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
2450
2451        db.write(|conn| {
2452            conn.migrate(
2453                "test_table",
2454                &[sql!(
2455                        CREATE TABLE test_table(
2456                            text TEXT,
2457                            workspace_id INTEGER,
2458                            FOREIGN KEY(workspace_id)
2459                                REFERENCES workspaces(workspace_id)
2460                            ON DELETE CASCADE
2461                        ) STRICT;)],
2462                |_, _, _| false,
2463            )
2464        })
2465        .await
2466        .unwrap();
2467
2468        let mut workspace_1 = SerializedWorkspace {
2469            id: WorkspaceId(1),
2470            paths: PathList::new(&["/tmp", "/tmp2"]),
2471            location: SerializedWorkspaceLocation::Local,
2472            center_group: Default::default(),
2473            window_bounds: Default::default(),
2474            display: Default::default(),
2475            docks: Default::default(),
2476            centered_layout: false,
2477            breakpoints: Default::default(),
2478            session_id: None,
2479            window_id: None,
2480            user_toolchains: Default::default(),
2481        };
2482
2483        let workspace_2 = SerializedWorkspace {
2484            id: WorkspaceId(2),
2485            paths: PathList::new(&["/tmp"]),
2486            location: SerializedWorkspaceLocation::Local,
2487            center_group: Default::default(),
2488            window_bounds: Default::default(),
2489            display: Default::default(),
2490            docks: Default::default(),
2491            centered_layout: false,
2492            breakpoints: Default::default(),
2493            session_id: None,
2494            window_id: None,
2495            user_toolchains: Default::default(),
2496        };
2497
2498        db.save_workspace(workspace_1.clone()).await;
2499
2500        db.write(|conn| {
2501            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2502                .unwrap()(("test-text-1", 1))
2503            .unwrap();
2504        })
2505        .await;
2506
2507        db.save_workspace(workspace_2.clone()).await;
2508
2509        db.write(|conn| {
2510            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2511                .unwrap()(("test-text-2", 2))
2512            .unwrap();
2513        })
2514        .await;
2515
2516        workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
2517        db.save_workspace(workspace_1.clone()).await;
2518        db.save_workspace(workspace_1).await;
2519        db.save_workspace(workspace_2).await;
2520
2521        let test_text_2 = db
2522            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2523            .unwrap()(2)
2524        .unwrap()
2525        .unwrap();
2526        assert_eq!(test_text_2, "test-text-2");
2527
2528        let test_text_1 = db
2529            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2530            .unwrap()(1)
2531        .unwrap()
2532        .unwrap();
2533        assert_eq!(test_text_1, "test-text-1");
2534    }
2535
2536    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
2537        SerializedPaneGroup::Group {
2538            axis: SerializedAxis(axis),
2539            flexes: None,
2540            children,
2541        }
2542    }
2543
2544    #[gpui::test]
2545    async fn test_full_workspace_serialization() {
2546        zlog::init_test();
2547
2548        let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
2549
2550        //  -----------------
2551        //  | 1,2   | 5,6   |
2552        //  | - - - |       |
2553        //  | 3,4   |       |
2554        //  -----------------
2555        let center_group = group(
2556            Axis::Horizontal,
2557            vec![
2558                group(
2559                    Axis::Vertical,
2560                    vec![
2561                        SerializedPaneGroup::Pane(SerializedPane::new(
2562                            vec![
2563                                SerializedItem::new("Terminal", 5, false, false),
2564                                SerializedItem::new("Terminal", 6, true, false),
2565                            ],
2566                            false,
2567                            0,
2568                        )),
2569                        SerializedPaneGroup::Pane(SerializedPane::new(
2570                            vec![
2571                                SerializedItem::new("Terminal", 7, true, false),
2572                                SerializedItem::new("Terminal", 8, false, false),
2573                            ],
2574                            false,
2575                            0,
2576                        )),
2577                    ],
2578                ),
2579                SerializedPaneGroup::Pane(SerializedPane::new(
2580                    vec![
2581                        SerializedItem::new("Terminal", 9, false, false),
2582                        SerializedItem::new("Terminal", 10, true, false),
2583                    ],
2584                    false,
2585                    0,
2586                )),
2587            ],
2588        );
2589
2590        let workspace = SerializedWorkspace {
2591            id: WorkspaceId(5),
2592            paths: PathList::new(&["/tmp", "/tmp2"]),
2593            location: SerializedWorkspaceLocation::Local,
2594            center_group,
2595            window_bounds: Default::default(),
2596            breakpoints: Default::default(),
2597            display: Default::default(),
2598            docks: Default::default(),
2599            centered_layout: false,
2600            session_id: None,
2601            window_id: Some(999),
2602            user_toolchains: Default::default(),
2603        };
2604
2605        db.save_workspace(workspace.clone()).await;
2606
2607        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2608        assert_eq!(workspace, round_trip_workspace.unwrap());
2609
2610        // Test guaranteed duplicate IDs
2611        db.save_workspace(workspace.clone()).await;
2612        db.save_workspace(workspace.clone()).await;
2613
2614        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2615        assert_eq!(workspace, round_trip_workspace.unwrap());
2616    }
2617
2618    #[gpui::test]
2619    async fn test_workspace_assignment() {
2620        zlog::init_test();
2621
2622        let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2623
2624        let workspace_1 = SerializedWorkspace {
2625            id: WorkspaceId(1),
2626            paths: PathList::new(&["/tmp", "/tmp2"]),
2627            location: SerializedWorkspaceLocation::Local,
2628            center_group: Default::default(),
2629            window_bounds: Default::default(),
2630            breakpoints: Default::default(),
2631            display: Default::default(),
2632            docks: Default::default(),
2633            centered_layout: false,
2634            session_id: None,
2635            window_id: Some(1),
2636            user_toolchains: Default::default(),
2637        };
2638
2639        let mut workspace_2 = SerializedWorkspace {
2640            id: WorkspaceId(2),
2641            paths: PathList::new(&["/tmp"]),
2642            location: SerializedWorkspaceLocation::Local,
2643            center_group: Default::default(),
2644            window_bounds: Default::default(),
2645            display: Default::default(),
2646            docks: Default::default(),
2647            centered_layout: false,
2648            breakpoints: Default::default(),
2649            session_id: None,
2650            window_id: Some(2),
2651            user_toolchains: Default::default(),
2652        };
2653
2654        db.save_workspace(workspace_1.clone()).await;
2655        db.save_workspace(workspace_2.clone()).await;
2656
2657        // Test that paths are treated as a set
2658        assert_eq!(
2659            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2660            workspace_1
2661        );
2662        assert_eq!(
2663            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
2664            workspace_1
2665        );
2666
2667        // Make sure that other keys work
2668        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
2669        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
2670
2671        // Test 'mutate' case of updating a pre-existing id
2672        workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
2673
2674        db.save_workspace(workspace_2.clone()).await;
2675        assert_eq!(
2676            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2677            workspace_2
2678        );
2679
2680        // Test other mechanism for mutating
2681        let mut workspace_3 = SerializedWorkspace {
2682            id: WorkspaceId(3),
2683            paths: PathList::new(&["/tmp2", "/tmp"]),
2684            location: SerializedWorkspaceLocation::Local,
2685            center_group: Default::default(),
2686            window_bounds: Default::default(),
2687            breakpoints: Default::default(),
2688            display: Default::default(),
2689            docks: Default::default(),
2690            centered_layout: false,
2691            session_id: None,
2692            window_id: Some(3),
2693            user_toolchains: Default::default(),
2694        };
2695
2696        db.save_workspace(workspace_3.clone()).await;
2697        assert_eq!(
2698            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2699            workspace_3
2700        );
2701
2702        // Make sure that updating paths differently also works
2703        workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
2704        db.save_workspace(workspace_3.clone()).await;
2705        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2706        assert_eq!(
2707            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2708                .unwrap(),
2709            workspace_3
2710        );
2711    }
2712
2713    #[gpui::test]
2714    async fn test_session_workspaces() {
2715        zlog::init_test();
2716
2717        let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
2718
2719        let workspace_1 = SerializedWorkspace {
2720            id: WorkspaceId(1),
2721            paths: PathList::new(&["/tmp1"]),
2722            location: SerializedWorkspaceLocation::Local,
2723            center_group: Default::default(),
2724            window_bounds: Default::default(),
2725            display: Default::default(),
2726            docks: Default::default(),
2727            centered_layout: false,
2728            breakpoints: Default::default(),
2729            session_id: Some("session-id-1".to_owned()),
2730            window_id: Some(10),
2731            user_toolchains: Default::default(),
2732        };
2733
2734        let workspace_2 = SerializedWorkspace {
2735            id: WorkspaceId(2),
2736            paths: PathList::new(&["/tmp2"]),
2737            location: SerializedWorkspaceLocation::Local,
2738            center_group: Default::default(),
2739            window_bounds: Default::default(),
2740            display: Default::default(),
2741            docks: Default::default(),
2742            centered_layout: false,
2743            breakpoints: Default::default(),
2744            session_id: Some("session-id-1".to_owned()),
2745            window_id: Some(20),
2746            user_toolchains: Default::default(),
2747        };
2748
2749        let workspace_3 = SerializedWorkspace {
2750            id: WorkspaceId(3),
2751            paths: PathList::new(&["/tmp3"]),
2752            location: SerializedWorkspaceLocation::Local,
2753            center_group: Default::default(),
2754            window_bounds: Default::default(),
2755            display: Default::default(),
2756            docks: Default::default(),
2757            centered_layout: false,
2758            breakpoints: Default::default(),
2759            session_id: Some("session-id-2".to_owned()),
2760            window_id: Some(30),
2761            user_toolchains: Default::default(),
2762        };
2763
2764        let workspace_4 = SerializedWorkspace {
2765            id: WorkspaceId(4),
2766            paths: PathList::new(&["/tmp4"]),
2767            location: SerializedWorkspaceLocation::Local,
2768            center_group: Default::default(),
2769            window_bounds: Default::default(),
2770            display: Default::default(),
2771            docks: Default::default(),
2772            centered_layout: false,
2773            breakpoints: Default::default(),
2774            session_id: None,
2775            window_id: None,
2776            user_toolchains: Default::default(),
2777        };
2778
2779        let connection_id = db
2780            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2781                host: "my-host".into(),
2782                port: Some(1234),
2783                ..Default::default()
2784            }))
2785            .await
2786            .unwrap();
2787
2788        let workspace_5 = SerializedWorkspace {
2789            id: WorkspaceId(5),
2790            paths: PathList::default(),
2791            location: SerializedWorkspaceLocation::Remote(
2792                db.remote_connection(connection_id).unwrap(),
2793            ),
2794            center_group: Default::default(),
2795            window_bounds: Default::default(),
2796            display: Default::default(),
2797            docks: Default::default(),
2798            centered_layout: false,
2799            breakpoints: Default::default(),
2800            session_id: Some("session-id-2".to_owned()),
2801            window_id: Some(50),
2802            user_toolchains: Default::default(),
2803        };
2804
2805        let workspace_6 = SerializedWorkspace {
2806            id: WorkspaceId(6),
2807            paths: PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2808            location: SerializedWorkspaceLocation::Local,
2809            center_group: Default::default(),
2810            window_bounds: Default::default(),
2811            breakpoints: Default::default(),
2812            display: Default::default(),
2813            docks: Default::default(),
2814            centered_layout: false,
2815            session_id: Some("session-id-3".to_owned()),
2816            window_id: Some(60),
2817            user_toolchains: Default::default(),
2818        };
2819
2820        db.save_workspace(workspace_1.clone()).await;
2821        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2822        db.save_workspace(workspace_2.clone()).await;
2823        db.save_workspace(workspace_3.clone()).await;
2824        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2825        db.save_workspace(workspace_4.clone()).await;
2826        db.save_workspace(workspace_5.clone()).await;
2827        db.save_workspace(workspace_6.clone()).await;
2828
2829        let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2830        assert_eq!(locations.len(), 2);
2831        assert_eq!(locations[0].0, PathList::new(&["/tmp2"]));
2832        assert_eq!(locations[0].1, Some(20));
2833        assert_eq!(locations[1].0, PathList::new(&["/tmp1"]));
2834        assert_eq!(locations[1].1, Some(10));
2835
2836        let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
2837        assert_eq!(locations.len(), 2);
2838        assert_eq!(locations[0].0, PathList::default());
2839        assert_eq!(locations[0].1, Some(50));
2840        assert_eq!(locations[0].2, Some(connection_id));
2841        assert_eq!(locations[1].0, PathList::new(&["/tmp3"]));
2842        assert_eq!(locations[1].1, Some(30));
2843
2844        let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
2845        assert_eq!(locations.len(), 1);
2846        assert_eq!(
2847            locations[0].0,
2848            PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2849        );
2850        assert_eq!(locations[0].1, Some(60));
2851    }
2852
2853    fn default_workspace<P: AsRef<Path>>(
2854        paths: &[P],
2855        center_group: &SerializedPaneGroup,
2856    ) -> SerializedWorkspace {
2857        SerializedWorkspace {
2858            id: WorkspaceId(4),
2859            paths: PathList::new(paths),
2860            location: SerializedWorkspaceLocation::Local,
2861            center_group: center_group.clone(),
2862            window_bounds: Default::default(),
2863            display: Default::default(),
2864            docks: Default::default(),
2865            breakpoints: Default::default(),
2866            centered_layout: false,
2867            session_id: None,
2868            window_id: None,
2869            user_toolchains: Default::default(),
2870        }
2871    }
2872
2873    #[gpui::test]
2874    async fn test_last_session_workspace_locations() {
2875        let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
2876        let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
2877        let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
2878        let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
2879
2880        let db =
2881            WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
2882
2883        let workspaces = [
2884            (1, vec![dir1.path()], 9),
2885            (2, vec![dir2.path()], 5),
2886            (3, vec![dir3.path()], 8),
2887            (4, vec![dir4.path()], 2),
2888            (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
2889            (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
2890        ]
2891        .into_iter()
2892        .map(|(id, paths, window_id)| SerializedWorkspace {
2893            id: WorkspaceId(id),
2894            paths: PathList::new(paths.as_slice()),
2895            location: SerializedWorkspaceLocation::Local,
2896            center_group: Default::default(),
2897            window_bounds: Default::default(),
2898            display: Default::default(),
2899            docks: Default::default(),
2900            centered_layout: false,
2901            session_id: Some("one-session".to_owned()),
2902            breakpoints: Default::default(),
2903            window_id: Some(window_id),
2904            user_toolchains: Default::default(),
2905        })
2906        .collect::<Vec<_>>();
2907
2908        for workspace in workspaces.iter() {
2909            db.save_workspace(workspace.clone()).await;
2910        }
2911
2912        let stack = Some(Vec::from([
2913            WindowId::from(2), // Top
2914            WindowId::from(8),
2915            WindowId::from(5),
2916            WindowId::from(9),
2917            WindowId::from(3),
2918            WindowId::from(4), // Bottom
2919        ]));
2920
2921        let locations = db
2922            .last_session_workspace_locations("one-session", stack)
2923            .unwrap();
2924        assert_eq!(
2925            locations,
2926            [
2927                (
2928                    SerializedWorkspaceLocation::Local,
2929                    PathList::new(&[dir4.path()])
2930                ),
2931                (
2932                    SerializedWorkspaceLocation::Local,
2933                    PathList::new(&[dir3.path()])
2934                ),
2935                (
2936                    SerializedWorkspaceLocation::Local,
2937                    PathList::new(&[dir2.path()])
2938                ),
2939                (
2940                    SerializedWorkspaceLocation::Local,
2941                    PathList::new(&[dir1.path()])
2942                ),
2943                (
2944                    SerializedWorkspaceLocation::Local,
2945                    PathList::new(&[dir1.path(), dir2.path(), dir3.path()])
2946                ),
2947                (
2948                    SerializedWorkspaceLocation::Local,
2949                    PathList::new(&[dir4.path(), dir3.path(), dir2.path()])
2950                ),
2951            ]
2952        );
2953    }
2954
2955    #[gpui::test]
2956    async fn test_last_session_workspace_locations_remote() {
2957        let db =
2958            WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
2959                .await;
2960
2961        let remote_connections = [
2962            ("host-1", "my-user-1"),
2963            ("host-2", "my-user-2"),
2964            ("host-3", "my-user-3"),
2965            ("host-4", "my-user-4"),
2966        ]
2967        .into_iter()
2968        .map(|(host, user)| async {
2969            let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
2970                host: host.into(),
2971                username: Some(user.to_string()),
2972                ..Default::default()
2973            });
2974            db.get_or_create_remote_connection(options.clone())
2975                .await
2976                .unwrap();
2977            options
2978        })
2979        .collect::<Vec<_>>();
2980
2981        let remote_connections = futures::future::join_all(remote_connections).await;
2982
2983        let workspaces = [
2984            (1, remote_connections[0].clone(), 9),
2985            (2, remote_connections[1].clone(), 5),
2986            (3, remote_connections[2].clone(), 8),
2987            (4, remote_connections[3].clone(), 2),
2988        ]
2989        .into_iter()
2990        .map(|(id, remote_connection, window_id)| SerializedWorkspace {
2991            id: WorkspaceId(id),
2992            paths: PathList::default(),
2993            location: SerializedWorkspaceLocation::Remote(remote_connection),
2994            center_group: Default::default(),
2995            window_bounds: Default::default(),
2996            display: Default::default(),
2997            docks: Default::default(),
2998            centered_layout: false,
2999            session_id: Some("one-session".to_owned()),
3000            breakpoints: Default::default(),
3001            window_id: Some(window_id),
3002            user_toolchains: Default::default(),
3003        })
3004        .collect::<Vec<_>>();
3005
3006        for workspace in workspaces.iter() {
3007            db.save_workspace(workspace.clone()).await;
3008        }
3009
3010        let stack = Some(Vec::from([
3011            WindowId::from(2), // Top
3012            WindowId::from(8),
3013            WindowId::from(5),
3014            WindowId::from(9), // Bottom
3015        ]));
3016
3017        let have = db
3018            .last_session_workspace_locations("one-session", stack)
3019            .unwrap();
3020        assert_eq!(have.len(), 4);
3021        assert_eq!(
3022            have[0],
3023            (
3024                SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
3025                PathList::default()
3026            )
3027        );
3028        assert_eq!(
3029            have[1],
3030            (
3031                SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
3032                PathList::default()
3033            )
3034        );
3035        assert_eq!(
3036            have[2],
3037            (
3038                SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
3039                PathList::default()
3040            )
3041        );
3042        assert_eq!(
3043            have[3],
3044            (
3045                SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
3046                PathList::default()
3047            )
3048        );
3049    }
3050
3051    #[gpui::test]
3052    async fn test_get_or_create_ssh_project() {
3053        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
3054
3055        let host = "example.com".to_string();
3056        let port = Some(22_u16);
3057        let user = Some("user".to_string());
3058
3059        let connection_id = db
3060            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3061                host: host.clone().into(),
3062                port,
3063                username: user.clone(),
3064                ..Default::default()
3065            }))
3066            .await
3067            .unwrap();
3068
3069        // Test that calling the function again with the same parameters returns the same project
3070        let same_connection = db
3071            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3072                host: host.clone().into(),
3073                port,
3074                username: user.clone(),
3075                ..Default::default()
3076            }))
3077            .await
3078            .unwrap();
3079
3080        assert_eq!(connection_id, same_connection);
3081
3082        // Test with different parameters
3083        let host2 = "otherexample.com".to_string();
3084        let port2 = None;
3085        let user2 = Some("otheruser".to_string());
3086
3087        let different_connection = db
3088            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3089                host: host2.clone().into(),
3090                port: port2,
3091                username: user2.clone(),
3092                ..Default::default()
3093            }))
3094            .await
3095            .unwrap();
3096
3097        assert_ne!(connection_id, different_connection);
3098    }
3099
3100    #[gpui::test]
3101    async fn test_get_or_create_ssh_project_with_null_user() {
3102        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
3103
3104        let (host, port, user) = ("example.com".to_string(), None, None);
3105
3106        let connection_id = db
3107            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3108                host: host.clone().into(),
3109                port,
3110                username: None,
3111                ..Default::default()
3112            }))
3113            .await
3114            .unwrap();
3115
3116        let same_connection_id = db
3117            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3118                host: host.clone().into(),
3119                port,
3120                username: user.clone(),
3121                ..Default::default()
3122            }))
3123            .await
3124            .unwrap();
3125
3126        assert_eq!(connection_id, same_connection_id);
3127    }
3128
3129    #[gpui::test]
3130    async fn test_get_remote_connections() {
3131        let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
3132
3133        let connections = [
3134            ("example.com".to_string(), None, None),
3135            (
3136                "anotherexample.com".to_string(),
3137                Some(123_u16),
3138                Some("user2".to_string()),
3139            ),
3140            ("yetanother.com".to_string(), Some(345_u16), None),
3141        ];
3142
3143        let mut ids = Vec::new();
3144        for (host, port, user) in connections.iter() {
3145            ids.push(
3146                db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
3147                    SshConnectionOptions {
3148                        host: host.clone().into(),
3149                        port: *port,
3150                        username: user.clone(),
3151                        ..Default::default()
3152                    },
3153                ))
3154                .await
3155                .unwrap(),
3156            );
3157        }
3158
3159        let stored_connections = db.remote_connections().unwrap();
3160        assert_eq!(
3161            stored_connections,
3162            [
3163                (
3164                    ids[0],
3165                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3166                        host: "example.com".into(),
3167                        port: None,
3168                        username: None,
3169                        ..Default::default()
3170                    }),
3171                ),
3172                (
3173                    ids[1],
3174                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3175                        host: "anotherexample.com".into(),
3176                        port: Some(123),
3177                        username: Some("user2".into()),
3178                        ..Default::default()
3179                    }),
3180                ),
3181                (
3182                    ids[2],
3183                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3184                        host: "yetanother.com".into(),
3185                        port: Some(345),
3186                        username: None,
3187                        ..Default::default()
3188                    }),
3189                ),
3190            ]
3191            .into_iter()
3192            .collect::<HashMap<_, _>>(),
3193        );
3194    }
3195
3196    #[gpui::test]
3197    async fn test_simple_split() {
3198        zlog::init_test();
3199
3200        let db = WorkspaceDb::open_test_db("simple_split").await;
3201
3202        //  -----------------
3203        //  | 1,2   | 5,6   |
3204        //  | - - - |       |
3205        //  | 3,4   |       |
3206        //  -----------------
3207        let center_pane = group(
3208            Axis::Horizontal,
3209            vec![
3210                group(
3211                    Axis::Vertical,
3212                    vec![
3213                        SerializedPaneGroup::Pane(SerializedPane::new(
3214                            vec![
3215                                SerializedItem::new("Terminal", 1, false, false),
3216                                SerializedItem::new("Terminal", 2, true, false),
3217                            ],
3218                            false,
3219                            0,
3220                        )),
3221                        SerializedPaneGroup::Pane(SerializedPane::new(
3222                            vec![
3223                                SerializedItem::new("Terminal", 4, false, false),
3224                                SerializedItem::new("Terminal", 3, true, false),
3225                            ],
3226                            true,
3227                            0,
3228                        )),
3229                    ],
3230                ),
3231                SerializedPaneGroup::Pane(SerializedPane::new(
3232                    vec![
3233                        SerializedItem::new("Terminal", 5, true, false),
3234                        SerializedItem::new("Terminal", 6, false, false),
3235                    ],
3236                    false,
3237                    0,
3238                )),
3239            ],
3240        );
3241
3242        let workspace = default_workspace(&["/tmp"], &center_pane);
3243
3244        db.save_workspace(workspace.clone()).await;
3245
3246        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
3247
3248        assert_eq!(workspace.center_group, new_workspace.center_group);
3249    }
3250
3251    #[gpui::test]
3252    async fn test_cleanup_panes() {
3253        zlog::init_test();
3254
3255        let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
3256
3257        let center_pane = group(
3258            Axis::Horizontal,
3259            vec![
3260                group(
3261                    Axis::Vertical,
3262                    vec![
3263                        SerializedPaneGroup::Pane(SerializedPane::new(
3264                            vec![
3265                                SerializedItem::new("Terminal", 1, false, false),
3266                                SerializedItem::new("Terminal", 2, true, false),
3267                            ],
3268                            false,
3269                            0,
3270                        )),
3271                        SerializedPaneGroup::Pane(SerializedPane::new(
3272                            vec![
3273                                SerializedItem::new("Terminal", 4, false, false),
3274                                SerializedItem::new("Terminal", 3, true, false),
3275                            ],
3276                            true,
3277                            0,
3278                        )),
3279                    ],
3280                ),
3281                SerializedPaneGroup::Pane(SerializedPane::new(
3282                    vec![
3283                        SerializedItem::new("Terminal", 5, false, false),
3284                        SerializedItem::new("Terminal", 6, true, false),
3285                    ],
3286                    false,
3287                    0,
3288                )),
3289            ],
3290        );
3291
3292        let id = &["/tmp"];
3293
3294        let mut workspace = default_workspace(id, &center_pane);
3295
3296        db.save_workspace(workspace.clone()).await;
3297
3298        workspace.center_group = group(
3299            Axis::Vertical,
3300            vec![
3301                SerializedPaneGroup::Pane(SerializedPane::new(
3302                    vec![
3303                        SerializedItem::new("Terminal", 1, false, false),
3304                        SerializedItem::new("Terminal", 2, true, false),
3305                    ],
3306                    false,
3307                    0,
3308                )),
3309                SerializedPaneGroup::Pane(SerializedPane::new(
3310                    vec![
3311                        SerializedItem::new("Terminal", 4, true, false),
3312                        SerializedItem::new("Terminal", 3, false, false),
3313                    ],
3314                    true,
3315                    0,
3316                )),
3317            ],
3318        );
3319
3320        db.save_workspace(workspace.clone()).await;
3321
3322        let new_workspace = db.workspace_for_roots(id).unwrap();
3323
3324        assert_eq!(workspace.center_group, new_workspace.center_group);
3325    }
3326
3327    #[gpui::test]
3328    async fn test_empty_workspace_window_bounds() {
3329        zlog::init_test();
3330
3331        let db = WorkspaceDb::open_test_db("test_empty_workspace_window_bounds").await;
3332        let id = db.next_id().await.unwrap();
3333
3334        // Create a workspace with empty paths (empty workspace)
3335        let empty_paths: &[&str] = &[];
3336        let display_uuid = Uuid::new_v4();
3337        let window_bounds = SerializedWindowBounds(WindowBounds::Windowed(Bounds {
3338            origin: point(px(100.0), px(200.0)),
3339            size: size(px(800.0), px(600.0)),
3340        }));
3341
3342        let workspace = SerializedWorkspace {
3343            id,
3344            paths: PathList::new(empty_paths),
3345            location: SerializedWorkspaceLocation::Local,
3346            center_group: Default::default(),
3347            window_bounds: None,
3348            display: None,
3349            docks: Default::default(),
3350            breakpoints: Default::default(),
3351            centered_layout: false,
3352            session_id: None,
3353            window_id: None,
3354            user_toolchains: Default::default(),
3355        };
3356
3357        // Save the workspace (this creates the record with empty paths)
3358        db.save_workspace(workspace.clone()).await;
3359
3360        // Save window bounds separately (as the actual code does via set_window_open_status)
3361        db.set_window_open_status(id, window_bounds, display_uuid)
3362            .await
3363            .unwrap();
3364
3365        // Retrieve it using empty paths
3366        let retrieved = db.workspace_for_roots(empty_paths).unwrap();
3367
3368        // Verify window bounds were persisted
3369        assert_eq!(retrieved.id, id);
3370        assert!(retrieved.window_bounds.is_some());
3371        assert_eq!(retrieved.window_bounds.unwrap().0, window_bounds.0);
3372        assert!(retrieved.display.is_some());
3373        assert_eq!(retrieved.display.unwrap(), display_uuid);
3374    }
3375}