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