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