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