persistence.rs

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