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