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        }
1329        Self::get_or_create_remote_connection_query(
1330            this,
1331            kind,
1332            host,
1333            port,
1334            user,
1335            distro,
1336            name,
1337            container_id,
1338        )
1339    }
1340
1341    fn get_or_create_remote_connection_query(
1342        this: &Connection,
1343        kind: RemoteConnectionKind,
1344        host: Option<String>,
1345        port: Option<u16>,
1346        user: Option<String>,
1347        distro: Option<String>,
1348        name: Option<String>,
1349        container_id: Option<String>,
1350    ) -> Result<RemoteConnectionId> {
1351        if let Some(id) = this.select_row_bound(sql!(
1352            SELECT id
1353            FROM remote_connections
1354            WHERE
1355                kind IS ? AND
1356                host IS ? AND
1357                port IS ? AND
1358                user IS ? AND
1359                distro IS ? AND
1360                name IS ? AND
1361                container_id IS ?
1362            LIMIT 1
1363        ))?((
1364            kind.serialize(),
1365            host.clone(),
1366            port,
1367            user.clone(),
1368            distro.clone(),
1369            name.clone(),
1370            container_id.clone(),
1371        ))? {
1372            Ok(RemoteConnectionId(id))
1373        } else {
1374            let id = this.select_row_bound(sql!(
1375                INSERT INTO remote_connections (
1376                    kind,
1377                    host,
1378                    port,
1379                    user,
1380                    distro,
1381                    name,
1382                    container_id
1383                ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
1384                RETURNING id
1385            ))?((
1386                kind.serialize(),
1387                host,
1388                port,
1389                user,
1390                distro,
1391                name,
1392                container_id,
1393            ))?
1394            .context("failed to insert remote project")?;
1395            Ok(RemoteConnectionId(id))
1396        }
1397    }
1398
1399    query! {
1400        pub async fn next_id() -> Result<WorkspaceId> {
1401            INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
1402        }
1403    }
1404
1405    fn recent_workspaces(
1406        &self,
1407    ) -> Result<Vec<(WorkspaceId, PathList, Option<RemoteConnectionId>)>> {
1408        Ok(self
1409            .recent_workspaces_query()?
1410            .into_iter()
1411            .map(|(id, paths, order, remote_connection_id)| {
1412                (
1413                    id,
1414                    PathList::deserialize(&SerializedPathList { paths, order }),
1415                    remote_connection_id.map(RemoteConnectionId),
1416                )
1417            })
1418            .collect())
1419    }
1420
1421    query! {
1422        fn recent_workspaces_query() -> Result<Vec<(WorkspaceId, String, String, Option<u64>)>> {
1423            SELECT workspace_id, paths, paths_order, remote_connection_id
1424            FROM workspaces
1425            WHERE
1426                paths IS NOT NULL OR
1427                remote_connection_id IS NOT NULL
1428            ORDER BY timestamp DESC
1429        }
1430    }
1431
1432    fn session_workspaces(
1433        &self,
1434        session_id: String,
1435    ) -> Result<Vec<(PathList, Option<u64>, Option<RemoteConnectionId>)>> {
1436        Ok(self
1437            .session_workspaces_query(session_id)?
1438            .into_iter()
1439            .map(|(paths, order, window_id, remote_connection_id)| {
1440                (
1441                    PathList::deserialize(&SerializedPathList { paths, order }),
1442                    window_id,
1443                    remote_connection_id.map(RemoteConnectionId),
1444                )
1445            })
1446            .collect())
1447    }
1448
1449    query! {
1450        fn session_workspaces_query(session_id: String) -> Result<Vec<(String, String, Option<u64>, Option<u64>)>> {
1451            SELECT paths, paths_order, window_id, remote_connection_id
1452            FROM workspaces
1453            WHERE session_id = ?1
1454            ORDER BY timestamp DESC
1455        }
1456    }
1457
1458    query! {
1459        pub fn breakpoints_for_file(workspace_id: WorkspaceId, file_path: &Path) -> Result<Vec<Breakpoint>> {
1460            SELECT breakpoint_location
1461            FROM breakpoints
1462            WHERE  workspace_id= ?1 AND path = ?2
1463        }
1464    }
1465
1466    query! {
1467        pub fn clear_breakpoints(file_path: &Path) -> Result<()> {
1468            DELETE FROM breakpoints
1469            WHERE file_path = ?2
1470        }
1471    }
1472
1473    fn remote_connections(&self) -> Result<HashMap<RemoteConnectionId, RemoteConnectionOptions>> {
1474        Ok(self.select(sql!(
1475            SELECT
1476                id, kind, host, port, user, distro, container_id, name
1477            FROM
1478                remote_connections
1479        ))?()?
1480        .into_iter()
1481        .filter_map(|(id, kind, host, port, user, distro, container_id, name)| {
1482            Some((
1483                RemoteConnectionId(id),
1484                Self::remote_connection_from_row(
1485                    kind,
1486                    host,
1487                    port,
1488                    user,
1489                    distro,
1490                    container_id,
1491                    name,
1492                )?,
1493            ))
1494        })
1495        .collect())
1496    }
1497
1498    pub(crate) fn remote_connection(
1499        &self,
1500        id: RemoteConnectionId,
1501    ) -> Result<RemoteConnectionOptions> {
1502        let (kind, host, port, user, distro, container_id, name) = self.select_row_bound(sql!(
1503            SELECT kind, host, port, user, distro, container_id, name
1504            FROM remote_connections
1505            WHERE id = ?
1506        ))?(id.0)?
1507        .context("no such remote connection")?;
1508        Self::remote_connection_from_row(kind, host, port, user, distro, container_id, name)
1509            .context("invalid remote_connection row")
1510    }
1511
1512    fn remote_connection_from_row(
1513        kind: String,
1514        host: Option<String>,
1515        port: Option<u16>,
1516        user: Option<String>,
1517        distro: Option<String>,
1518        container_id: Option<String>,
1519        name: Option<String>,
1520    ) -> Option<RemoteConnectionOptions> {
1521        match RemoteConnectionKind::deserialize(&kind)? {
1522            RemoteConnectionKind::Wsl => Some(RemoteConnectionOptions::Wsl(WslConnectionOptions {
1523                distro_name: distro?,
1524                user: user,
1525            })),
1526            RemoteConnectionKind::Ssh => Some(RemoteConnectionOptions::Ssh(SshConnectionOptions {
1527                host: host?.into(),
1528                port,
1529                username: user,
1530                ..Default::default()
1531            })),
1532            RemoteConnectionKind::Docker => {
1533                Some(RemoteConnectionOptions::Docker(DockerConnectionOptions {
1534                    container_id: container_id?,
1535                    name: name?,
1536                    upload_binary_over_docker_exec: false,
1537                }))
1538            }
1539        }
1540    }
1541
1542    query! {
1543        pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
1544            DELETE FROM workspaces
1545            WHERE workspace_id IS ?
1546        }
1547    }
1548
1549    // Returns the recent locations which are still valid on disk and deletes ones which no longer
1550    // exist.
1551    pub async fn recent_workspaces_on_disk(
1552        &self,
1553    ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
1554        let mut result = Vec::new();
1555        let mut delete_tasks = Vec::new();
1556        let remote_connections = self.remote_connections()?;
1557
1558        for (id, paths, remote_connection_id) in self.recent_workspaces()? {
1559            if let Some(remote_connection_id) = remote_connection_id {
1560                if let Some(connection_options) = remote_connections.get(&remote_connection_id) {
1561                    result.push((
1562                        id,
1563                        SerializedWorkspaceLocation::Remote(connection_options.clone()),
1564                        paths,
1565                    ));
1566                } else {
1567                    delete_tasks.push(self.delete_workspace_by_id(id));
1568                }
1569                continue;
1570            }
1571
1572            let has_wsl_path = if cfg!(windows) {
1573                paths
1574                    .paths()
1575                    .iter()
1576                    .any(|path| util::paths::WslPath::from_path(path).is_some())
1577            } else {
1578                false
1579            };
1580
1581            // Delete the workspace if any of the paths are WSL paths.
1582            // If a local workspace points to WSL, this check will cause us to wait for the
1583            // WSL VM and file server to boot up. This can block for many seconds.
1584            // Supported scenarios use remote workspaces.
1585            if !has_wsl_path && paths.paths().iter().all(|path| path.exists()) {
1586                // Only show directories in recent projects
1587                if paths.paths().iter().any(|path| path.is_dir()) {
1588                    result.push((id, SerializedWorkspaceLocation::Local, paths));
1589                }
1590            } else {
1591                delete_tasks.push(self.delete_workspace_by_id(id));
1592            }
1593        }
1594
1595        futures::future::join_all(delete_tasks).await;
1596        Ok(result)
1597    }
1598
1599    pub async fn last_workspace(&self) -> Result<Option<(SerializedWorkspaceLocation, PathList)>> {
1600        Ok(self
1601            .recent_workspaces_on_disk()
1602            .await?
1603            .into_iter()
1604            .next()
1605            .map(|(_, location, paths)| (location, paths)))
1606    }
1607
1608    // Returns the locations of the workspaces that were still opened when the last
1609    // session was closed (i.e. when Zed was quit).
1610    // If `last_session_window_order` is provided, the returned locations are ordered
1611    // according to that.
1612    pub fn last_session_workspace_locations(
1613        &self,
1614        last_session_id: &str,
1615        last_session_window_stack: Option<Vec<WindowId>>,
1616    ) -> Result<Vec<(SerializedWorkspaceLocation, PathList)>> {
1617        let mut workspaces = Vec::new();
1618
1619        for (paths, window_id, remote_connection_id) in
1620            self.session_workspaces(last_session_id.to_owned())?
1621        {
1622            if let Some(remote_connection_id) = remote_connection_id {
1623                workspaces.push((
1624                    SerializedWorkspaceLocation::Remote(
1625                        self.remote_connection(remote_connection_id)?,
1626                    ),
1627                    paths,
1628                    window_id.map(WindowId::from),
1629                ));
1630            } else if paths.paths().iter().all(|path| path.exists())
1631                && paths.paths().iter().any(|path| path.is_dir())
1632            {
1633                workspaces.push((
1634                    SerializedWorkspaceLocation::Local,
1635                    paths,
1636                    window_id.map(WindowId::from),
1637                ));
1638            }
1639        }
1640
1641        if let Some(stack) = last_session_window_stack {
1642            workspaces.sort_by_key(|(_, _, window_id)| {
1643                window_id
1644                    .and_then(|id| stack.iter().position(|&order_id| order_id == id))
1645                    .unwrap_or(usize::MAX)
1646            });
1647        }
1648
1649        Ok(workspaces
1650            .into_iter()
1651            .map(|(location, paths, _)| (location, paths))
1652            .collect::<Vec<_>>())
1653    }
1654
1655    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1656        Ok(self
1657            .get_pane_group(workspace_id, None)?
1658            .into_iter()
1659            .next()
1660            .unwrap_or_else(|| {
1661                SerializedPaneGroup::Pane(SerializedPane {
1662                    active: true,
1663                    children: vec![],
1664                    pinned_count: 0,
1665                })
1666            }))
1667    }
1668
1669    fn get_pane_group(
1670        &self,
1671        workspace_id: WorkspaceId,
1672        group_id: Option<GroupId>,
1673    ) -> Result<Vec<SerializedPaneGroup>> {
1674        type GroupKey = (Option<GroupId>, WorkspaceId);
1675        type GroupOrPane = (
1676            Option<GroupId>,
1677            Option<SerializedAxis>,
1678            Option<PaneId>,
1679            Option<bool>,
1680            Option<usize>,
1681            Option<String>,
1682        );
1683        self.select_bound::<GroupKey, GroupOrPane>(sql!(
1684            SELECT group_id, axis, pane_id, active, pinned_count, flexes
1685                FROM (SELECT
1686                        group_id,
1687                        axis,
1688                        NULL as pane_id,
1689                        NULL as active,
1690                        NULL as pinned_count,
1691                        position,
1692                        parent_group_id,
1693                        workspace_id,
1694                        flexes
1695                      FROM pane_groups
1696                    UNION
1697                      SELECT
1698                        NULL,
1699                        NULL,
1700                        center_panes.pane_id,
1701                        panes.active as active,
1702                        pinned_count,
1703                        position,
1704                        parent_group_id,
1705                        panes.workspace_id as workspace_id,
1706                        NULL
1707                      FROM center_panes
1708                      JOIN panes ON center_panes.pane_id = panes.pane_id)
1709                WHERE parent_group_id IS ? AND workspace_id = ?
1710                ORDER BY position
1711        ))?((group_id, workspace_id))?
1712        .into_iter()
1713        .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1714            let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1715            if let Some((group_id, axis)) = group_id.zip(axis) {
1716                let flexes = flexes
1717                    .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1718                    .transpose()?;
1719
1720                Ok(SerializedPaneGroup::Group {
1721                    axis,
1722                    children: self.get_pane_group(workspace_id, Some(group_id))?,
1723                    flexes,
1724                })
1725            } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
1726                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
1727                    self.get_items(pane_id)?,
1728                    active,
1729                    pinned_count,
1730                )))
1731            } else {
1732                bail!("Pane Group Child was neither a pane group or a pane");
1733            }
1734        })
1735        // Filter out panes and pane groups which don't have any children or items
1736        .filter(|pane_group| match pane_group {
1737            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
1738            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
1739            _ => true,
1740        })
1741        .collect::<Result<_>>()
1742    }
1743
1744    fn save_pane_group(
1745        conn: &Connection,
1746        workspace_id: WorkspaceId,
1747        pane_group: &SerializedPaneGroup,
1748        parent: Option<(GroupId, usize)>,
1749    ) -> Result<()> {
1750        if parent.is_none() {
1751            log::debug!("Saving a pane group for workspace {workspace_id:?}");
1752        }
1753        match pane_group {
1754            SerializedPaneGroup::Group {
1755                axis,
1756                children,
1757                flexes,
1758            } => {
1759                let (parent_id, position) = parent.unzip();
1760
1761                let flex_string = flexes
1762                    .as_ref()
1763                    .map(|flexes| serde_json::json!(flexes).to_string());
1764
1765                let group_id = conn.select_row_bound::<_, i64>(sql!(
1766                    INSERT INTO pane_groups(
1767                        workspace_id,
1768                        parent_group_id,
1769                        position,
1770                        axis,
1771                        flexes
1772                    )
1773                    VALUES (?, ?, ?, ?, ?)
1774                    RETURNING group_id
1775                ))?((
1776                    workspace_id,
1777                    parent_id,
1778                    position,
1779                    *axis,
1780                    flex_string,
1781                ))?
1782                .context("Couldn't retrieve group_id from inserted pane_group")?;
1783
1784                for (position, group) in children.iter().enumerate() {
1785                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
1786                }
1787
1788                Ok(())
1789            }
1790            SerializedPaneGroup::Pane(pane) => {
1791                Self::save_pane(conn, workspace_id, pane, parent)?;
1792                Ok(())
1793            }
1794        }
1795    }
1796
1797    fn save_pane(
1798        conn: &Connection,
1799        workspace_id: WorkspaceId,
1800        pane: &SerializedPane,
1801        parent: Option<(GroupId, usize)>,
1802    ) -> Result<PaneId> {
1803        let pane_id = conn.select_row_bound::<_, i64>(sql!(
1804            INSERT INTO panes(workspace_id, active, pinned_count)
1805            VALUES (?, ?, ?)
1806            RETURNING pane_id
1807        ))?((workspace_id, pane.active, pane.pinned_count))?
1808        .context("Could not retrieve inserted pane_id")?;
1809
1810        let (parent_id, order) = parent.unzip();
1811        conn.exec_bound(sql!(
1812            INSERT INTO center_panes(pane_id, parent_group_id, position)
1813            VALUES (?, ?, ?)
1814        ))?((pane_id, parent_id, order))?;
1815
1816        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
1817
1818        Ok(pane_id)
1819    }
1820
1821    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
1822        self.select_bound(sql!(
1823            SELECT kind, item_id, active, preview FROM items
1824            WHERE pane_id = ?
1825                ORDER BY position
1826        ))?(pane_id)
1827    }
1828
1829    fn save_items(
1830        conn: &Connection,
1831        workspace_id: WorkspaceId,
1832        pane_id: PaneId,
1833        items: &[SerializedItem],
1834    ) -> Result<()> {
1835        let mut insert = conn.exec_bound(sql!(
1836            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
1837        )).context("Preparing insertion")?;
1838        for (position, item) in items.iter().enumerate() {
1839            insert((workspace_id, pane_id, position, item))?;
1840        }
1841
1842        Ok(())
1843    }
1844
1845    query! {
1846        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
1847            UPDATE workspaces
1848            SET timestamp = CURRENT_TIMESTAMP
1849            WHERE workspace_id = ?
1850        }
1851    }
1852
1853    query! {
1854        pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
1855            UPDATE workspaces
1856            SET window_state = ?2,
1857                window_x = ?3,
1858                window_y = ?4,
1859                window_width = ?5,
1860                window_height = ?6,
1861                display = ?7
1862            WHERE workspace_id = ?1
1863        }
1864    }
1865
1866    query! {
1867        pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
1868            UPDATE workspaces
1869            SET centered_layout = ?2
1870            WHERE workspace_id = ?1
1871        }
1872    }
1873
1874    query! {
1875        pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option<String>) -> Result<()> {
1876            UPDATE workspaces
1877            SET session_id = ?2
1878            WHERE workspace_id = ?1
1879        }
1880    }
1881
1882    pub(crate) async fn toolchains(
1883        &self,
1884        workspace_id: WorkspaceId,
1885    ) -> Result<Vec<(Toolchain, Arc<Path>, Arc<RelPath>)>> {
1886        self.write(move |this| {
1887            let mut select = this
1888                .select_bound(sql!(
1889                    SELECT
1890                        name, path, worktree_root_path, relative_worktree_path, language_name, raw_json
1891                    FROM toolchains
1892                    WHERE workspace_id = ?
1893                ))
1894                .context("select toolchains")?;
1895
1896            let toolchain: Vec<(String, String, String, String, String, String)> =
1897                select(workspace_id)?;
1898
1899            Ok(toolchain
1900                .into_iter()
1901                .filter_map(
1902                    |(name, path, worktree_root_path, relative_worktree_path, language, json)| {
1903                        Some((
1904                            Toolchain {
1905                                name: name.into(),
1906                                path: path.into(),
1907                                language_name: LanguageName::new(&language),
1908                                as_json: serde_json::Value::from_str(&json).ok()?,
1909                            },
1910                           Arc::from(worktree_root_path.as_ref()),
1911                            RelPath::from_proto(&relative_worktree_path).log_err()?,
1912                        ))
1913                    },
1914                )
1915                .collect())
1916        })
1917        .await
1918    }
1919
1920    pub async fn set_toolchain(
1921        &self,
1922        workspace_id: WorkspaceId,
1923        worktree_root_path: Arc<Path>,
1924        relative_worktree_path: Arc<RelPath>,
1925        toolchain: Toolchain,
1926    ) -> Result<()> {
1927        log::debug!(
1928            "Setting toolchain for workspace, worktree: {worktree_root_path:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
1929            toolchain.name
1930        );
1931        self.write(move |conn| {
1932            let mut insert = conn
1933                .exec_bound(sql!(
1934                    INSERT INTO toolchains(workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?,  ?, ?)
1935                    ON CONFLICT DO
1936                    UPDATE SET
1937                        name = ?5,
1938                        path = ?6,
1939                        raw_json = ?7
1940                ))
1941                .context("Preparing insertion")?;
1942
1943            insert((
1944                workspace_id,
1945                worktree_root_path.to_string_lossy().into_owned(),
1946                relative_worktree_path.as_unix_str(),
1947                toolchain.language_name.as_ref(),
1948                toolchain.name.as_ref(),
1949                toolchain.path.as_ref(),
1950                toolchain.as_json.to_string(),
1951            ))?;
1952
1953            Ok(())
1954        }).await
1955    }
1956
1957    pub(crate) async fn save_trusted_worktrees(
1958        &self,
1959        trusted_worktrees: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
1960    ) -> anyhow::Result<()> {
1961        use anyhow::Context as _;
1962        use db::sqlez::statement::Statement;
1963        use itertools::Itertools as _;
1964
1965        DB.clear_trusted_worktrees()
1966            .await
1967            .context("clearing previous trust state")?;
1968
1969        let trusted_worktrees = trusted_worktrees
1970            .into_iter()
1971            .flat_map(|(host, abs_paths)| {
1972                abs_paths
1973                    .into_iter()
1974                    .map(move |abs_path| (Some(abs_path), host.clone()))
1975            })
1976            .collect::<Vec<_>>();
1977        let mut first_worktree;
1978        let mut last_worktree = 0_usize;
1979        for (count, placeholders) in std::iter::once("(?, ?, ?)")
1980            .cycle()
1981            .take(trusted_worktrees.len())
1982            .chunks(MAX_QUERY_PLACEHOLDERS / 3)
1983            .into_iter()
1984            .map(|chunk| {
1985                let mut count = 0;
1986                let placeholders = chunk
1987                    .inspect(|_| {
1988                        count += 1;
1989                    })
1990                    .join(", ");
1991                (count, placeholders)
1992            })
1993            .collect::<Vec<_>>()
1994        {
1995            first_worktree = last_worktree;
1996            last_worktree = last_worktree + count;
1997            let query = format!(
1998                r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name)
1999VALUES {placeholders};"#
2000            );
2001
2002            let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec();
2003            self.write(move |conn| {
2004                let mut statement = Statement::prepare(conn, query)?;
2005                let mut next_index = 1;
2006                for (abs_path, host) in trusted_worktrees {
2007                    let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy());
2008                    next_index = statement.bind(
2009                        &abs_path.as_ref().map(|abs_path| abs_path.as_ref()),
2010                        next_index,
2011                    )?;
2012                    next_index = statement.bind(
2013                        &host
2014                            .as_ref()
2015                            .and_then(|host| Some(host.user_name.as_ref()?.as_str())),
2016                        next_index,
2017                    )?;
2018                    next_index = statement.bind(
2019                        &host.as_ref().map(|host| host.host_identifier.as_str()),
2020                        next_index,
2021                    )?;
2022                }
2023                statement.exec()
2024            })
2025            .await
2026            .context("inserting new trusted state")?;
2027        }
2028        Ok(())
2029    }
2030
2031    pub fn fetch_trusted_worktrees(&self) -> Result<DbTrustedPaths> {
2032        let trusted_worktrees = DB.trusted_worktrees()?;
2033        Ok(trusted_worktrees
2034            .into_iter()
2035            .filter_map(|(abs_path, user_name, host_name)| {
2036                let db_host = match (user_name, host_name) {
2037                    (None, Some(host_name)) => Some(RemoteHostLocation {
2038                        user_name: None,
2039                        host_identifier: SharedString::new(host_name),
2040                    }),
2041                    (Some(user_name), Some(host_name)) => Some(RemoteHostLocation {
2042                        user_name: Some(SharedString::new(user_name)),
2043                        host_identifier: SharedString::new(host_name),
2044                    }),
2045                    _ => None,
2046                };
2047                Some((db_host, abs_path?))
2048            })
2049            .fold(HashMap::default(), |mut acc, (remote_host, abs_path)| {
2050                acc.entry(remote_host)
2051                    .or_insert_with(HashSet::default)
2052                    .insert(abs_path);
2053                acc
2054            }))
2055    }
2056
2057    query! {
2058        fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
2059            SELECT absolute_path, user_name, host_name
2060            FROM trusted_worktrees
2061        }
2062    }
2063
2064    query! {
2065        pub async fn clear_trusted_worktrees() -> Result<()> {
2066            DELETE FROM trusted_worktrees
2067        }
2068    }
2069}
2070
2071pub fn delete_unloaded_items(
2072    alive_items: Vec<ItemId>,
2073    workspace_id: WorkspaceId,
2074    table: &'static str,
2075    db: &ThreadSafeConnection,
2076    cx: &mut App,
2077) -> Task<Result<()>> {
2078    let db = db.clone();
2079    cx.spawn(async move |_| {
2080        let placeholders = alive_items
2081            .iter()
2082            .map(|_| "?")
2083            .collect::<Vec<&str>>()
2084            .join(", ");
2085
2086        let query = format!(
2087            "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
2088        );
2089
2090        db.write(move |conn| {
2091            let mut statement = Statement::prepare(conn, query)?;
2092            let mut next_index = statement.bind(&workspace_id, 1)?;
2093            for id in alive_items {
2094                next_index = statement.bind(&id, next_index)?;
2095            }
2096            statement.exec()
2097        })
2098        .await
2099    })
2100}
2101
2102#[cfg(test)]
2103mod tests {
2104    use super::*;
2105    use crate::persistence::model::{
2106        SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
2107    };
2108    use gpui;
2109    use pretty_assertions::assert_eq;
2110    use remote::SshConnectionOptions;
2111    use std::{thread, time::Duration};
2112
2113    #[gpui::test]
2114    async fn test_breakpoints() {
2115        zlog::init_test();
2116
2117        let db = WorkspaceDb::open_test_db("test_breakpoints").await;
2118        let id = db.next_id().await.unwrap();
2119
2120        let path = Path::new("/tmp/test.rs");
2121
2122        let breakpoint = Breakpoint {
2123            position: 123,
2124            message: None,
2125            state: BreakpointState::Enabled,
2126            condition: None,
2127            hit_condition: None,
2128        };
2129
2130        let log_breakpoint = Breakpoint {
2131            position: 456,
2132            message: Some("Test log message".into()),
2133            state: BreakpointState::Enabled,
2134            condition: None,
2135            hit_condition: None,
2136        };
2137
2138        let disable_breakpoint = Breakpoint {
2139            position: 578,
2140            message: None,
2141            state: BreakpointState::Disabled,
2142            condition: None,
2143            hit_condition: None,
2144        };
2145
2146        let condition_breakpoint = Breakpoint {
2147            position: 789,
2148            message: None,
2149            state: BreakpointState::Enabled,
2150            condition: Some("x > 5".into()),
2151            hit_condition: None,
2152        };
2153
2154        let hit_condition_breakpoint = Breakpoint {
2155            position: 999,
2156            message: None,
2157            state: BreakpointState::Enabled,
2158            condition: None,
2159            hit_condition: Some(">= 3".into()),
2160        };
2161
2162        let workspace = SerializedWorkspace {
2163            id,
2164            paths: PathList::new(&["/tmp"]),
2165            location: SerializedWorkspaceLocation::Local,
2166            center_group: Default::default(),
2167            window_bounds: Default::default(),
2168            display: Default::default(),
2169            docks: Default::default(),
2170            centered_layout: false,
2171            breakpoints: {
2172                let mut map = collections::BTreeMap::default();
2173                map.insert(
2174                    Arc::from(path),
2175                    vec![
2176                        SourceBreakpoint {
2177                            row: breakpoint.position,
2178                            path: Arc::from(path),
2179                            message: breakpoint.message.clone(),
2180                            state: breakpoint.state,
2181                            condition: breakpoint.condition.clone(),
2182                            hit_condition: breakpoint.hit_condition.clone(),
2183                        },
2184                        SourceBreakpoint {
2185                            row: log_breakpoint.position,
2186                            path: Arc::from(path),
2187                            message: log_breakpoint.message.clone(),
2188                            state: log_breakpoint.state,
2189                            condition: log_breakpoint.condition.clone(),
2190                            hit_condition: log_breakpoint.hit_condition.clone(),
2191                        },
2192                        SourceBreakpoint {
2193                            row: disable_breakpoint.position,
2194                            path: Arc::from(path),
2195                            message: disable_breakpoint.message.clone(),
2196                            state: disable_breakpoint.state,
2197                            condition: disable_breakpoint.condition.clone(),
2198                            hit_condition: disable_breakpoint.hit_condition.clone(),
2199                        },
2200                        SourceBreakpoint {
2201                            row: condition_breakpoint.position,
2202                            path: Arc::from(path),
2203                            message: condition_breakpoint.message.clone(),
2204                            state: condition_breakpoint.state,
2205                            condition: condition_breakpoint.condition.clone(),
2206                            hit_condition: condition_breakpoint.hit_condition.clone(),
2207                        },
2208                        SourceBreakpoint {
2209                            row: hit_condition_breakpoint.position,
2210                            path: Arc::from(path),
2211                            message: hit_condition_breakpoint.message.clone(),
2212                            state: hit_condition_breakpoint.state,
2213                            condition: hit_condition_breakpoint.condition.clone(),
2214                            hit_condition: hit_condition_breakpoint.hit_condition.clone(),
2215                        },
2216                    ],
2217                );
2218                map
2219            },
2220            session_id: None,
2221            window_id: None,
2222            user_toolchains: Default::default(),
2223        };
2224
2225        db.save_workspace(workspace.clone()).await;
2226
2227        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2228        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
2229
2230        assert_eq!(loaded_breakpoints.len(), 5);
2231
2232        // normal breakpoint
2233        assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
2234        assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
2235        assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
2236        assert_eq!(
2237            loaded_breakpoints[0].hit_condition,
2238            breakpoint.hit_condition
2239        );
2240        assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
2241        assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
2242
2243        // enabled breakpoint
2244        assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
2245        assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
2246        assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
2247        assert_eq!(
2248            loaded_breakpoints[1].hit_condition,
2249            log_breakpoint.hit_condition
2250        );
2251        assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
2252        assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
2253
2254        // disable breakpoint
2255        assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
2256        assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
2257        assert_eq!(
2258            loaded_breakpoints[2].condition,
2259            disable_breakpoint.condition
2260        );
2261        assert_eq!(
2262            loaded_breakpoints[2].hit_condition,
2263            disable_breakpoint.hit_condition
2264        );
2265        assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
2266        assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
2267
2268        // condition breakpoint
2269        assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
2270        assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
2271        assert_eq!(
2272            loaded_breakpoints[3].condition,
2273            condition_breakpoint.condition
2274        );
2275        assert_eq!(
2276            loaded_breakpoints[3].hit_condition,
2277            condition_breakpoint.hit_condition
2278        );
2279        assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
2280        assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
2281
2282        // hit condition breakpoint
2283        assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
2284        assert_eq!(
2285            loaded_breakpoints[4].message,
2286            hit_condition_breakpoint.message
2287        );
2288        assert_eq!(
2289            loaded_breakpoints[4].condition,
2290            hit_condition_breakpoint.condition
2291        );
2292        assert_eq!(
2293            loaded_breakpoints[4].hit_condition,
2294            hit_condition_breakpoint.hit_condition
2295        );
2296        assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
2297        assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
2298    }
2299
2300    #[gpui::test]
2301    async fn test_remove_last_breakpoint() {
2302        zlog::init_test();
2303
2304        let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
2305        let id = db.next_id().await.unwrap();
2306
2307        let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
2308
2309        let breakpoint_to_remove = Breakpoint {
2310            position: 100,
2311            message: None,
2312            state: BreakpointState::Enabled,
2313            condition: None,
2314            hit_condition: None,
2315        };
2316
2317        let workspace = SerializedWorkspace {
2318            id,
2319            paths: PathList::new(&["/tmp"]),
2320            location: SerializedWorkspaceLocation::Local,
2321            center_group: Default::default(),
2322            window_bounds: Default::default(),
2323            display: Default::default(),
2324            docks: Default::default(),
2325            centered_layout: false,
2326            breakpoints: {
2327                let mut map = collections::BTreeMap::default();
2328                map.insert(
2329                    Arc::from(singular_path),
2330                    vec![SourceBreakpoint {
2331                        row: breakpoint_to_remove.position,
2332                        path: Arc::from(singular_path),
2333                        message: None,
2334                        state: BreakpointState::Enabled,
2335                        condition: None,
2336                        hit_condition: None,
2337                    }],
2338                );
2339                map
2340            },
2341            session_id: None,
2342            window_id: None,
2343            user_toolchains: Default::default(),
2344        };
2345
2346        db.save_workspace(workspace.clone()).await;
2347
2348        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2349        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
2350
2351        assert_eq!(loaded_breakpoints.len(), 1);
2352        assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
2353        assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
2354        assert_eq!(
2355            loaded_breakpoints[0].condition,
2356            breakpoint_to_remove.condition
2357        );
2358        assert_eq!(
2359            loaded_breakpoints[0].hit_condition,
2360            breakpoint_to_remove.hit_condition
2361        );
2362        assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
2363        assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
2364
2365        let workspace_without_breakpoint = SerializedWorkspace {
2366            id,
2367            paths: PathList::new(&["/tmp"]),
2368            location: SerializedWorkspaceLocation::Local,
2369            center_group: Default::default(),
2370            window_bounds: Default::default(),
2371            display: Default::default(),
2372            docks: Default::default(),
2373            centered_layout: false,
2374            breakpoints: collections::BTreeMap::default(),
2375            session_id: None,
2376            window_id: None,
2377            user_toolchains: Default::default(),
2378        };
2379
2380        db.save_workspace(workspace_without_breakpoint.clone())
2381            .await;
2382
2383        let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
2384        let empty_breakpoints = loaded_after_remove
2385            .breakpoints
2386            .get(&Arc::from(singular_path));
2387
2388        assert!(empty_breakpoints.is_none());
2389    }
2390
2391    #[gpui::test]
2392    async fn test_next_id_stability() {
2393        zlog::init_test();
2394
2395        let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
2396
2397        db.write(|conn| {
2398            conn.migrate(
2399                "test_table",
2400                &[sql!(
2401                    CREATE TABLE test_table(
2402                        text TEXT,
2403                        workspace_id INTEGER,
2404                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2405                        ON DELETE CASCADE
2406                    ) STRICT;
2407                )],
2408                |_, _, _| false,
2409            )
2410            .unwrap();
2411        })
2412        .await;
2413
2414        let id = db.next_id().await.unwrap();
2415        // Assert the empty row got inserted
2416        assert_eq!(
2417            Some(id),
2418            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
2419                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
2420            ))
2421            .unwrap()(id)
2422            .unwrap()
2423        );
2424
2425        db.write(move |conn| {
2426            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2427                .unwrap()(("test-text-1", id))
2428            .unwrap()
2429        })
2430        .await;
2431
2432        let test_text_1 = db
2433            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2434            .unwrap()(1)
2435        .unwrap()
2436        .unwrap();
2437        assert_eq!(test_text_1, "test-text-1");
2438    }
2439
2440    #[gpui::test]
2441    async fn test_workspace_id_stability() {
2442        zlog::init_test();
2443
2444        let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
2445
2446        db.write(|conn| {
2447            conn.migrate(
2448                "test_table",
2449                &[sql!(
2450                        CREATE TABLE test_table(
2451                            text TEXT,
2452                            workspace_id INTEGER,
2453                            FOREIGN KEY(workspace_id)
2454                                REFERENCES workspaces(workspace_id)
2455                            ON DELETE CASCADE
2456                        ) STRICT;)],
2457                |_, _, _| false,
2458            )
2459        })
2460        .await
2461        .unwrap();
2462
2463        let mut workspace_1 = SerializedWorkspace {
2464            id: WorkspaceId(1),
2465            paths: PathList::new(&["/tmp", "/tmp2"]),
2466            location: SerializedWorkspaceLocation::Local,
2467            center_group: Default::default(),
2468            window_bounds: Default::default(),
2469            display: Default::default(),
2470            docks: Default::default(),
2471            centered_layout: false,
2472            breakpoints: Default::default(),
2473            session_id: None,
2474            window_id: None,
2475            user_toolchains: Default::default(),
2476        };
2477
2478        let workspace_2 = SerializedWorkspace {
2479            id: WorkspaceId(2),
2480            paths: PathList::new(&["/tmp"]),
2481            location: SerializedWorkspaceLocation::Local,
2482            center_group: Default::default(),
2483            window_bounds: Default::default(),
2484            display: Default::default(),
2485            docks: Default::default(),
2486            centered_layout: false,
2487            breakpoints: Default::default(),
2488            session_id: None,
2489            window_id: None,
2490            user_toolchains: Default::default(),
2491        };
2492
2493        db.save_workspace(workspace_1.clone()).await;
2494
2495        db.write(|conn| {
2496            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2497                .unwrap()(("test-text-1", 1))
2498            .unwrap();
2499        })
2500        .await;
2501
2502        db.save_workspace(workspace_2.clone()).await;
2503
2504        db.write(|conn| {
2505            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2506                .unwrap()(("test-text-2", 2))
2507            .unwrap();
2508        })
2509        .await;
2510
2511        workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
2512        db.save_workspace(workspace_1.clone()).await;
2513        db.save_workspace(workspace_1).await;
2514        db.save_workspace(workspace_2).await;
2515
2516        let test_text_2 = db
2517            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2518            .unwrap()(2)
2519        .unwrap()
2520        .unwrap();
2521        assert_eq!(test_text_2, "test-text-2");
2522
2523        let test_text_1 = db
2524            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2525            .unwrap()(1)
2526        .unwrap()
2527        .unwrap();
2528        assert_eq!(test_text_1, "test-text-1");
2529    }
2530
2531    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
2532        SerializedPaneGroup::Group {
2533            axis: SerializedAxis(axis),
2534            flexes: None,
2535            children,
2536        }
2537    }
2538
2539    #[gpui::test]
2540    async fn test_full_workspace_serialization() {
2541        zlog::init_test();
2542
2543        let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
2544
2545        //  -----------------
2546        //  | 1,2   | 5,6   |
2547        //  | - - - |       |
2548        //  | 3,4   |       |
2549        //  -----------------
2550        let center_group = group(
2551            Axis::Horizontal,
2552            vec![
2553                group(
2554                    Axis::Vertical,
2555                    vec![
2556                        SerializedPaneGroup::Pane(SerializedPane::new(
2557                            vec![
2558                                SerializedItem::new("Terminal", 5, false, false),
2559                                SerializedItem::new("Terminal", 6, true, false),
2560                            ],
2561                            false,
2562                            0,
2563                        )),
2564                        SerializedPaneGroup::Pane(SerializedPane::new(
2565                            vec![
2566                                SerializedItem::new("Terminal", 7, true, false),
2567                                SerializedItem::new("Terminal", 8, false, false),
2568                            ],
2569                            false,
2570                            0,
2571                        )),
2572                    ],
2573                ),
2574                SerializedPaneGroup::Pane(SerializedPane::new(
2575                    vec![
2576                        SerializedItem::new("Terminal", 9, false, false),
2577                        SerializedItem::new("Terminal", 10, true, false),
2578                    ],
2579                    false,
2580                    0,
2581                )),
2582            ],
2583        );
2584
2585        let workspace = SerializedWorkspace {
2586            id: WorkspaceId(5),
2587            paths: PathList::new(&["/tmp", "/tmp2"]),
2588            location: SerializedWorkspaceLocation::Local,
2589            center_group,
2590            window_bounds: Default::default(),
2591            breakpoints: Default::default(),
2592            display: Default::default(),
2593            docks: Default::default(),
2594            centered_layout: false,
2595            session_id: None,
2596            window_id: Some(999),
2597            user_toolchains: Default::default(),
2598        };
2599
2600        db.save_workspace(workspace.clone()).await;
2601
2602        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2603        assert_eq!(workspace, round_trip_workspace.unwrap());
2604
2605        // Test guaranteed duplicate IDs
2606        db.save_workspace(workspace.clone()).await;
2607        db.save_workspace(workspace.clone()).await;
2608
2609        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2610        assert_eq!(workspace, round_trip_workspace.unwrap());
2611    }
2612
2613    #[gpui::test]
2614    async fn test_workspace_assignment() {
2615        zlog::init_test();
2616
2617        let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2618
2619        let workspace_1 = SerializedWorkspace {
2620            id: WorkspaceId(1),
2621            paths: PathList::new(&["/tmp", "/tmp2"]),
2622            location: SerializedWorkspaceLocation::Local,
2623            center_group: Default::default(),
2624            window_bounds: Default::default(),
2625            breakpoints: Default::default(),
2626            display: Default::default(),
2627            docks: Default::default(),
2628            centered_layout: false,
2629            session_id: None,
2630            window_id: Some(1),
2631            user_toolchains: Default::default(),
2632        };
2633
2634        let mut workspace_2 = SerializedWorkspace {
2635            id: WorkspaceId(2),
2636            paths: PathList::new(&["/tmp"]),
2637            location: SerializedWorkspaceLocation::Local,
2638            center_group: Default::default(),
2639            window_bounds: Default::default(),
2640            display: Default::default(),
2641            docks: Default::default(),
2642            centered_layout: false,
2643            breakpoints: Default::default(),
2644            session_id: None,
2645            window_id: Some(2),
2646            user_toolchains: Default::default(),
2647        };
2648
2649        db.save_workspace(workspace_1.clone()).await;
2650        db.save_workspace(workspace_2.clone()).await;
2651
2652        // Test that paths are treated as a set
2653        assert_eq!(
2654            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2655            workspace_1
2656        );
2657        assert_eq!(
2658            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
2659            workspace_1
2660        );
2661
2662        // Make sure that other keys work
2663        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
2664        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
2665
2666        // Test 'mutate' case of updating a pre-existing id
2667        workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
2668
2669        db.save_workspace(workspace_2.clone()).await;
2670        assert_eq!(
2671            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2672            workspace_2
2673        );
2674
2675        // Test other mechanism for mutating
2676        let mut workspace_3 = SerializedWorkspace {
2677            id: WorkspaceId(3),
2678            paths: PathList::new(&["/tmp2", "/tmp"]),
2679            location: SerializedWorkspaceLocation::Local,
2680            center_group: Default::default(),
2681            window_bounds: Default::default(),
2682            breakpoints: Default::default(),
2683            display: Default::default(),
2684            docks: Default::default(),
2685            centered_layout: false,
2686            session_id: None,
2687            window_id: Some(3),
2688            user_toolchains: Default::default(),
2689        };
2690
2691        db.save_workspace(workspace_3.clone()).await;
2692        assert_eq!(
2693            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2694            workspace_3
2695        );
2696
2697        // Make sure that updating paths differently also works
2698        workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
2699        db.save_workspace(workspace_3.clone()).await;
2700        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2701        assert_eq!(
2702            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2703                .unwrap(),
2704            workspace_3
2705        );
2706    }
2707
2708    #[gpui::test]
2709    async fn test_session_workspaces() {
2710        zlog::init_test();
2711
2712        let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
2713
2714        let workspace_1 = SerializedWorkspace {
2715            id: WorkspaceId(1),
2716            paths: PathList::new(&["/tmp1"]),
2717            location: SerializedWorkspaceLocation::Local,
2718            center_group: Default::default(),
2719            window_bounds: Default::default(),
2720            display: Default::default(),
2721            docks: Default::default(),
2722            centered_layout: false,
2723            breakpoints: Default::default(),
2724            session_id: Some("session-id-1".to_owned()),
2725            window_id: Some(10),
2726            user_toolchains: Default::default(),
2727        };
2728
2729        let workspace_2 = SerializedWorkspace {
2730            id: WorkspaceId(2),
2731            paths: PathList::new(&["/tmp2"]),
2732            location: SerializedWorkspaceLocation::Local,
2733            center_group: Default::default(),
2734            window_bounds: Default::default(),
2735            display: Default::default(),
2736            docks: Default::default(),
2737            centered_layout: false,
2738            breakpoints: Default::default(),
2739            session_id: Some("session-id-1".to_owned()),
2740            window_id: Some(20),
2741            user_toolchains: Default::default(),
2742        };
2743
2744        let workspace_3 = SerializedWorkspace {
2745            id: WorkspaceId(3),
2746            paths: PathList::new(&["/tmp3"]),
2747            location: SerializedWorkspaceLocation::Local,
2748            center_group: Default::default(),
2749            window_bounds: Default::default(),
2750            display: Default::default(),
2751            docks: Default::default(),
2752            centered_layout: false,
2753            breakpoints: Default::default(),
2754            session_id: Some("session-id-2".to_owned()),
2755            window_id: Some(30),
2756            user_toolchains: Default::default(),
2757        };
2758
2759        let workspace_4 = SerializedWorkspace {
2760            id: WorkspaceId(4),
2761            paths: PathList::new(&["/tmp4"]),
2762            location: SerializedWorkspaceLocation::Local,
2763            center_group: Default::default(),
2764            window_bounds: Default::default(),
2765            display: Default::default(),
2766            docks: Default::default(),
2767            centered_layout: false,
2768            breakpoints: Default::default(),
2769            session_id: None,
2770            window_id: None,
2771            user_toolchains: Default::default(),
2772        };
2773
2774        let connection_id = db
2775            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2776                host: "my-host".into(),
2777                port: Some(1234),
2778                ..Default::default()
2779            }))
2780            .await
2781            .unwrap();
2782
2783        let workspace_5 = SerializedWorkspace {
2784            id: WorkspaceId(5),
2785            paths: PathList::default(),
2786            location: SerializedWorkspaceLocation::Remote(
2787                db.remote_connection(connection_id).unwrap(),
2788            ),
2789            center_group: Default::default(),
2790            window_bounds: Default::default(),
2791            display: Default::default(),
2792            docks: Default::default(),
2793            centered_layout: false,
2794            breakpoints: Default::default(),
2795            session_id: Some("session-id-2".to_owned()),
2796            window_id: Some(50),
2797            user_toolchains: Default::default(),
2798        };
2799
2800        let workspace_6 = SerializedWorkspace {
2801            id: WorkspaceId(6),
2802            paths: PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2803            location: SerializedWorkspaceLocation::Local,
2804            center_group: Default::default(),
2805            window_bounds: Default::default(),
2806            breakpoints: Default::default(),
2807            display: Default::default(),
2808            docks: Default::default(),
2809            centered_layout: false,
2810            session_id: Some("session-id-3".to_owned()),
2811            window_id: Some(60),
2812            user_toolchains: Default::default(),
2813        };
2814
2815        db.save_workspace(workspace_1.clone()).await;
2816        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2817        db.save_workspace(workspace_2.clone()).await;
2818        db.save_workspace(workspace_3.clone()).await;
2819        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2820        db.save_workspace(workspace_4.clone()).await;
2821        db.save_workspace(workspace_5.clone()).await;
2822        db.save_workspace(workspace_6.clone()).await;
2823
2824        let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2825        assert_eq!(locations.len(), 2);
2826        assert_eq!(locations[0].0, PathList::new(&["/tmp2"]));
2827        assert_eq!(locations[0].1, Some(20));
2828        assert_eq!(locations[1].0, PathList::new(&["/tmp1"]));
2829        assert_eq!(locations[1].1, Some(10));
2830
2831        let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
2832        assert_eq!(locations.len(), 2);
2833        assert_eq!(locations[0].0, PathList::default());
2834        assert_eq!(locations[0].1, Some(50));
2835        assert_eq!(locations[0].2, Some(connection_id));
2836        assert_eq!(locations[1].0, PathList::new(&["/tmp3"]));
2837        assert_eq!(locations[1].1, Some(30));
2838
2839        let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
2840        assert_eq!(locations.len(), 1);
2841        assert_eq!(
2842            locations[0].0,
2843            PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2844        );
2845        assert_eq!(locations[0].1, Some(60));
2846    }
2847
2848    fn default_workspace<P: AsRef<Path>>(
2849        paths: &[P],
2850        center_group: &SerializedPaneGroup,
2851    ) -> SerializedWorkspace {
2852        SerializedWorkspace {
2853            id: WorkspaceId(4),
2854            paths: PathList::new(paths),
2855            location: SerializedWorkspaceLocation::Local,
2856            center_group: center_group.clone(),
2857            window_bounds: Default::default(),
2858            display: Default::default(),
2859            docks: Default::default(),
2860            breakpoints: Default::default(),
2861            centered_layout: false,
2862            session_id: None,
2863            window_id: None,
2864            user_toolchains: Default::default(),
2865        }
2866    }
2867
2868    #[gpui::test]
2869    async fn test_last_session_workspace_locations() {
2870        let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
2871        let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
2872        let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
2873        let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
2874
2875        let db =
2876            WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
2877
2878        let workspaces = [
2879            (1, vec![dir1.path()], 9),
2880            (2, vec![dir2.path()], 5),
2881            (3, vec![dir3.path()], 8),
2882            (4, vec![dir4.path()], 2),
2883            (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
2884            (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
2885        ]
2886        .into_iter()
2887        .map(|(id, paths, window_id)| SerializedWorkspace {
2888            id: WorkspaceId(id),
2889            paths: PathList::new(paths.as_slice()),
2890            location: SerializedWorkspaceLocation::Local,
2891            center_group: Default::default(),
2892            window_bounds: Default::default(),
2893            display: Default::default(),
2894            docks: Default::default(),
2895            centered_layout: false,
2896            session_id: Some("one-session".to_owned()),
2897            breakpoints: Default::default(),
2898            window_id: Some(window_id),
2899            user_toolchains: Default::default(),
2900        })
2901        .collect::<Vec<_>>();
2902
2903        for workspace in workspaces.iter() {
2904            db.save_workspace(workspace.clone()).await;
2905        }
2906
2907        let stack = Some(Vec::from([
2908            WindowId::from(2), // Top
2909            WindowId::from(8),
2910            WindowId::from(5),
2911            WindowId::from(9),
2912            WindowId::from(3),
2913            WindowId::from(4), // Bottom
2914        ]));
2915
2916        let locations = db
2917            .last_session_workspace_locations("one-session", stack)
2918            .unwrap();
2919        assert_eq!(
2920            locations,
2921            [
2922                (
2923                    SerializedWorkspaceLocation::Local,
2924                    PathList::new(&[dir4.path()])
2925                ),
2926                (
2927                    SerializedWorkspaceLocation::Local,
2928                    PathList::new(&[dir3.path()])
2929                ),
2930                (
2931                    SerializedWorkspaceLocation::Local,
2932                    PathList::new(&[dir2.path()])
2933                ),
2934                (
2935                    SerializedWorkspaceLocation::Local,
2936                    PathList::new(&[dir1.path()])
2937                ),
2938                (
2939                    SerializedWorkspaceLocation::Local,
2940                    PathList::new(&[dir1.path(), dir2.path(), dir3.path()])
2941                ),
2942                (
2943                    SerializedWorkspaceLocation::Local,
2944                    PathList::new(&[dir4.path(), dir3.path(), dir2.path()])
2945                ),
2946            ]
2947        );
2948    }
2949
2950    #[gpui::test]
2951    async fn test_last_session_workspace_locations_remote() {
2952        let db =
2953            WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
2954                .await;
2955
2956        let remote_connections = [
2957            ("host-1", "my-user-1"),
2958            ("host-2", "my-user-2"),
2959            ("host-3", "my-user-3"),
2960            ("host-4", "my-user-4"),
2961        ]
2962        .into_iter()
2963        .map(|(host, user)| async {
2964            let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
2965                host: host.into(),
2966                username: Some(user.to_string()),
2967                ..Default::default()
2968            });
2969            db.get_or_create_remote_connection(options.clone())
2970                .await
2971                .unwrap();
2972            options
2973        })
2974        .collect::<Vec<_>>();
2975
2976        let remote_connections = futures::future::join_all(remote_connections).await;
2977
2978        let workspaces = [
2979            (1, remote_connections[0].clone(), 9),
2980            (2, remote_connections[1].clone(), 5),
2981            (3, remote_connections[2].clone(), 8),
2982            (4, remote_connections[3].clone(), 2),
2983        ]
2984        .into_iter()
2985        .map(|(id, remote_connection, window_id)| SerializedWorkspace {
2986            id: WorkspaceId(id),
2987            paths: PathList::default(),
2988            location: SerializedWorkspaceLocation::Remote(remote_connection),
2989            center_group: Default::default(),
2990            window_bounds: Default::default(),
2991            display: Default::default(),
2992            docks: Default::default(),
2993            centered_layout: false,
2994            session_id: Some("one-session".to_owned()),
2995            breakpoints: Default::default(),
2996            window_id: Some(window_id),
2997            user_toolchains: Default::default(),
2998        })
2999        .collect::<Vec<_>>();
3000
3001        for workspace in workspaces.iter() {
3002            db.save_workspace(workspace.clone()).await;
3003        }
3004
3005        let stack = Some(Vec::from([
3006            WindowId::from(2), // Top
3007            WindowId::from(8),
3008            WindowId::from(5),
3009            WindowId::from(9), // Bottom
3010        ]));
3011
3012        let have = db
3013            .last_session_workspace_locations("one-session", stack)
3014            .unwrap();
3015        assert_eq!(have.len(), 4);
3016        assert_eq!(
3017            have[0],
3018            (
3019                SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
3020                PathList::default()
3021            )
3022        );
3023        assert_eq!(
3024            have[1],
3025            (
3026                SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
3027                PathList::default()
3028            )
3029        );
3030        assert_eq!(
3031            have[2],
3032            (
3033                SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
3034                PathList::default()
3035            )
3036        );
3037        assert_eq!(
3038            have[3],
3039            (
3040                SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
3041                PathList::default()
3042            )
3043        );
3044    }
3045
3046    #[gpui::test]
3047    async fn test_get_or_create_ssh_project() {
3048        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
3049
3050        let host = "example.com".to_string();
3051        let port = Some(22_u16);
3052        let user = Some("user".to_string());
3053
3054        let connection_id = db
3055            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3056                host: host.clone().into(),
3057                port,
3058                username: user.clone(),
3059                ..Default::default()
3060            }))
3061            .await
3062            .unwrap();
3063
3064        // Test that calling the function again with the same parameters returns the same project
3065        let same_connection = db
3066            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3067                host: host.clone().into(),
3068                port,
3069                username: user.clone(),
3070                ..Default::default()
3071            }))
3072            .await
3073            .unwrap();
3074
3075        assert_eq!(connection_id, same_connection);
3076
3077        // Test with different parameters
3078        let host2 = "otherexample.com".to_string();
3079        let port2 = None;
3080        let user2 = Some("otheruser".to_string());
3081
3082        let different_connection = db
3083            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3084                host: host2.clone().into(),
3085                port: port2,
3086                username: user2.clone(),
3087                ..Default::default()
3088            }))
3089            .await
3090            .unwrap();
3091
3092        assert_ne!(connection_id, different_connection);
3093    }
3094
3095    #[gpui::test]
3096    async fn test_get_or_create_ssh_project_with_null_user() {
3097        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
3098
3099        let (host, port, user) = ("example.com".to_string(), None, None);
3100
3101        let connection_id = db
3102            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3103                host: host.clone().into(),
3104                port,
3105                username: None,
3106                ..Default::default()
3107            }))
3108            .await
3109            .unwrap();
3110
3111        let same_connection_id = db
3112            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3113                host: host.clone().into(),
3114                port,
3115                username: user.clone(),
3116                ..Default::default()
3117            }))
3118            .await
3119            .unwrap();
3120
3121        assert_eq!(connection_id, same_connection_id);
3122    }
3123
3124    #[gpui::test]
3125    async fn test_get_remote_connections() {
3126        let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
3127
3128        let connections = [
3129            ("example.com".to_string(), None, None),
3130            (
3131                "anotherexample.com".to_string(),
3132                Some(123_u16),
3133                Some("user2".to_string()),
3134            ),
3135            ("yetanother.com".to_string(), Some(345_u16), None),
3136        ];
3137
3138        let mut ids = Vec::new();
3139        for (host, port, user) in connections.iter() {
3140            ids.push(
3141                db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
3142                    SshConnectionOptions {
3143                        host: host.clone().into(),
3144                        port: *port,
3145                        username: user.clone(),
3146                        ..Default::default()
3147                    },
3148                ))
3149                .await
3150                .unwrap(),
3151            );
3152        }
3153
3154        let stored_connections = db.remote_connections().unwrap();
3155        assert_eq!(
3156            stored_connections,
3157            [
3158                (
3159                    ids[0],
3160                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3161                        host: "example.com".into(),
3162                        port: None,
3163                        username: None,
3164                        ..Default::default()
3165                    }),
3166                ),
3167                (
3168                    ids[1],
3169                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3170                        host: "anotherexample.com".into(),
3171                        port: Some(123),
3172                        username: Some("user2".into()),
3173                        ..Default::default()
3174                    }),
3175                ),
3176                (
3177                    ids[2],
3178                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3179                        host: "yetanother.com".into(),
3180                        port: Some(345),
3181                        username: None,
3182                        ..Default::default()
3183                    }),
3184                ),
3185            ]
3186            .into_iter()
3187            .collect::<HashMap<_, _>>(),
3188        );
3189    }
3190
3191    #[gpui::test]
3192    async fn test_simple_split() {
3193        zlog::init_test();
3194
3195        let db = WorkspaceDb::open_test_db("simple_split").await;
3196
3197        //  -----------------
3198        //  | 1,2   | 5,6   |
3199        //  | - - - |       |
3200        //  | 3,4   |       |
3201        //  -----------------
3202        let center_pane = group(
3203            Axis::Horizontal,
3204            vec![
3205                group(
3206                    Axis::Vertical,
3207                    vec![
3208                        SerializedPaneGroup::Pane(SerializedPane::new(
3209                            vec![
3210                                SerializedItem::new("Terminal", 1, false, false),
3211                                SerializedItem::new("Terminal", 2, true, false),
3212                            ],
3213                            false,
3214                            0,
3215                        )),
3216                        SerializedPaneGroup::Pane(SerializedPane::new(
3217                            vec![
3218                                SerializedItem::new("Terminal", 4, false, false),
3219                                SerializedItem::new("Terminal", 3, true, false),
3220                            ],
3221                            true,
3222                            0,
3223                        )),
3224                    ],
3225                ),
3226                SerializedPaneGroup::Pane(SerializedPane::new(
3227                    vec![
3228                        SerializedItem::new("Terminal", 5, true, false),
3229                        SerializedItem::new("Terminal", 6, false, false),
3230                    ],
3231                    false,
3232                    0,
3233                )),
3234            ],
3235        );
3236
3237        let workspace = default_workspace(&["/tmp"], &center_pane);
3238
3239        db.save_workspace(workspace.clone()).await;
3240
3241        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
3242
3243        assert_eq!(workspace.center_group, new_workspace.center_group);
3244    }
3245
3246    #[gpui::test]
3247    async fn test_cleanup_panes() {
3248        zlog::init_test();
3249
3250        let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
3251
3252        let center_pane = group(
3253            Axis::Horizontal,
3254            vec![
3255                group(
3256                    Axis::Vertical,
3257                    vec![
3258                        SerializedPaneGroup::Pane(SerializedPane::new(
3259                            vec![
3260                                SerializedItem::new("Terminal", 1, false, false),
3261                                SerializedItem::new("Terminal", 2, true, false),
3262                            ],
3263                            false,
3264                            0,
3265                        )),
3266                        SerializedPaneGroup::Pane(SerializedPane::new(
3267                            vec![
3268                                SerializedItem::new("Terminal", 4, false, false),
3269                                SerializedItem::new("Terminal", 3, true, false),
3270                            ],
3271                            true,
3272                            0,
3273                        )),
3274                    ],
3275                ),
3276                SerializedPaneGroup::Pane(SerializedPane::new(
3277                    vec![
3278                        SerializedItem::new("Terminal", 5, false, false),
3279                        SerializedItem::new("Terminal", 6, true, false),
3280                    ],
3281                    false,
3282                    0,
3283                )),
3284            ],
3285        );
3286
3287        let id = &["/tmp"];
3288
3289        let mut workspace = default_workspace(id, &center_pane);
3290
3291        db.save_workspace(workspace.clone()).await;
3292
3293        workspace.center_group = group(
3294            Axis::Vertical,
3295            vec![
3296                SerializedPaneGroup::Pane(SerializedPane::new(
3297                    vec![
3298                        SerializedItem::new("Terminal", 1, false, false),
3299                        SerializedItem::new("Terminal", 2, true, false),
3300                    ],
3301                    false,
3302                    0,
3303                )),
3304                SerializedPaneGroup::Pane(SerializedPane::new(
3305                    vec![
3306                        SerializedItem::new("Terminal", 4, true, false),
3307                        SerializedItem::new("Terminal", 3, false, false),
3308                    ],
3309                    true,
3310                    0,
3311                )),
3312            ],
3313        );
3314
3315        db.save_workspace(workspace.clone()).await;
3316
3317        let new_workspace = db.workspace_for_roots(id).unwrap();
3318
3319        assert_eq!(workspace.center_group, new_workspace.center_group);
3320    }
3321
3322    #[gpui::test]
3323    async fn test_empty_workspace_window_bounds() {
3324        zlog::init_test();
3325
3326        let db = WorkspaceDb::open_test_db("test_empty_workspace_window_bounds").await;
3327        let id = db.next_id().await.unwrap();
3328
3329        // Create a workspace with empty paths (empty workspace)
3330        let empty_paths: &[&str] = &[];
3331        let display_uuid = Uuid::new_v4();
3332        let window_bounds = SerializedWindowBounds(WindowBounds::Windowed(Bounds {
3333            origin: point(px(100.0), px(200.0)),
3334            size: size(px(800.0), px(600.0)),
3335        }));
3336
3337        let workspace = SerializedWorkspace {
3338            id,
3339            paths: PathList::new(empty_paths),
3340            location: SerializedWorkspaceLocation::Local,
3341            center_group: Default::default(),
3342            window_bounds: None,
3343            display: None,
3344            docks: Default::default(),
3345            breakpoints: Default::default(),
3346            centered_layout: false,
3347            session_id: None,
3348            window_id: None,
3349            user_toolchains: Default::default(),
3350        };
3351
3352        // Save the workspace (this creates the record with empty paths)
3353        db.save_workspace(workspace.clone()).await;
3354
3355        // Save window bounds separately (as the actual code does via set_window_open_status)
3356        db.set_window_open_status(id, window_bounds, display_uuid)
3357            .await
3358            .unwrap();
3359
3360        // Retrieve it using empty paths
3361        let retrieved = db.workspace_for_roots(empty_paths).unwrap();
3362
3363        // Verify window bounds were persisted
3364        assert_eq!(retrieved.id, id);
3365        assert!(retrieved.window_bounds.is_some());
3366        assert_eq!(retrieved.window_bounds.unwrap().0, window_bounds.0);
3367        assert!(retrieved.display.is_some());
3368        assert_eq!(retrieved.display.unwrap(), display_uuid);
3369    }
3370}