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