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 chrono::{DateTime, NaiveDateTime, Utc};
  12use fs::Fs;
  13
  14use anyhow::{Context as _, Result, bail};
  15use collections::{HashMap, HashSet, IndexSet};
  16use db::{
  17    kvp::KEY_VALUE_STORE,
  18    query,
  19    sqlez::{connection::Connection, domain::Domain},
  20    sqlez_macros::sql,
  21};
  22use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size};
  23use project::{
  24    debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
  25    trusted_worktrees::{DbTrustedPaths, RemoteHostLocation},
  26};
  27
  28use language::{LanguageName, Toolchain, ToolchainScope};
  29use remote::{
  30    DockerConnectionOptions, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions,
  31};
  32use serde::{Deserialize, Serialize};
  33use sqlez::{
  34    bindable::{Bind, Column, StaticColumnCount},
  35    statement::Statement,
  36    thread_safe_connection::ThreadSafeConnection,
  37};
  38
  39use ui::{App, SharedString, px};
  40use util::{ResultExt, maybe, rel_path::RelPath};
  41use uuid::Uuid;
  42
  43use crate::{
  44    WorkspaceId,
  45    path_list::{PathList, SerializedPathList},
  46    persistence::model::RemoteConnectionKind,
  47};
  48
  49use model::{
  50    GroupId, ItemId, PaneId, RemoteConnectionId, SerializedItem, SerializedPane,
  51    SerializedPaneGroup, SerializedWorkspace,
  52};
  53
  54use self::model::{DockStructure, SerializedWorkspaceLocation, SessionWorkspace};
  55
  56// https://www.sqlite.org/limits.html
  57// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
  58// > which defaults to <..> 32766 for SQLite versions after 3.32.0.
  59const MAX_QUERY_PLACEHOLDERS: usize = 32000;
  60
  61fn parse_timestamp(text: &str) -> DateTime<Utc> {
  62    NaiveDateTime::parse_from_str(text, "%Y-%m-%d %H:%M:%S")
  63        .map(|naive| naive.and_utc())
  64        .unwrap_or_else(|_| Utc::now())
  65}
  66
  67#[derive(Copy, Clone, Debug, PartialEq)]
  68pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
  69impl sqlez::bindable::StaticColumnCount for SerializedAxis {}
  70impl sqlez::bindable::Bind for SerializedAxis {
  71    fn bind(
  72        &self,
  73        statement: &sqlez::statement::Statement,
  74        start_index: i32,
  75    ) -> anyhow::Result<i32> {
  76        match self.0 {
  77            gpui::Axis::Horizontal => "Horizontal",
  78            gpui::Axis::Vertical => "Vertical",
  79        }
  80        .bind(statement, start_index)
  81    }
  82}
  83
  84impl sqlez::bindable::Column for SerializedAxis {
  85    fn column(
  86        statement: &mut sqlez::statement::Statement,
  87        start_index: i32,
  88    ) -> anyhow::Result<(Self, i32)> {
  89        String::column(statement, start_index).and_then(|(axis_text, next_index)| {
  90            Ok((
  91                match axis_text.as_str() {
  92                    "Horizontal" => Self(Axis::Horizontal),
  93                    "Vertical" => Self(Axis::Vertical),
  94                    _ => anyhow::bail!("Stored serialized item kind is incorrect"),
  95                },
  96                next_index,
  97            ))
  98        })
  99    }
 100}
 101
 102#[derive(Copy, Clone, Debug, PartialEq, Default)]
 103pub(crate) struct SerializedWindowBounds(pub(crate) WindowBounds);
 104
 105impl StaticColumnCount for SerializedWindowBounds {
 106    fn column_count() -> usize {
 107        5
 108    }
 109}
 110
 111impl Bind for SerializedWindowBounds {
 112    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
 113        match self.0 {
 114            WindowBounds::Windowed(bounds) => {
 115                let next_index = statement.bind(&"Windowed", start_index)?;
 116                statement.bind(
 117                    &(
 118                        SerializedPixels(bounds.origin.x),
 119                        SerializedPixels(bounds.origin.y),
 120                        SerializedPixels(bounds.size.width),
 121                        SerializedPixels(bounds.size.height),
 122                    ),
 123                    next_index,
 124                )
 125            }
 126            WindowBounds::Maximized(bounds) => {
 127                let next_index = statement.bind(&"Maximized", start_index)?;
 128                statement.bind(
 129                    &(
 130                        SerializedPixels(bounds.origin.x),
 131                        SerializedPixels(bounds.origin.y),
 132                        SerializedPixels(bounds.size.width),
 133                        SerializedPixels(bounds.size.height),
 134                    ),
 135                    next_index,
 136                )
 137            }
 138            WindowBounds::Fullscreen(bounds) => {
 139                let next_index = statement.bind(&"FullScreen", start_index)?;
 140                statement.bind(
 141                    &(
 142                        SerializedPixels(bounds.origin.x),
 143                        SerializedPixels(bounds.origin.y),
 144                        SerializedPixels(bounds.size.width),
 145                        SerializedPixels(bounds.size.height),
 146                    ),
 147                    next_index,
 148                )
 149            }
 150        }
 151    }
 152}
 153
 154impl Column for SerializedWindowBounds {
 155    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 156        let (window_state, next_index) = String::column(statement, start_index)?;
 157        let ((x, y, width, height), _): ((i32, i32, i32, i32), _) =
 158            Column::column(statement, next_index)?;
 159        let bounds = Bounds {
 160            origin: point(px(x as f32), px(y as f32)),
 161            size: size(px(width as f32), px(height as f32)),
 162        };
 163
 164        let status = match window_state.as_str() {
 165            "Windowed" | "Fixed" => SerializedWindowBounds(WindowBounds::Windowed(bounds)),
 166            "Maximized" => SerializedWindowBounds(WindowBounds::Maximized(bounds)),
 167            "FullScreen" => SerializedWindowBounds(WindowBounds::Fullscreen(bounds)),
 168            _ => bail!("Window State did not have a valid string"),
 169        };
 170
 171        Ok((status, next_index + 4))
 172    }
 173}
 174
 175const DEFAULT_WINDOW_BOUNDS_KEY: &str = "default_window_bounds";
 176
 177pub fn read_default_window_bounds() -> Option<(Uuid, WindowBounds)> {
 178    let json_str = KEY_VALUE_STORE
 179        .read_kvp(DEFAULT_WINDOW_BOUNDS_KEY)
 180        .log_err()
 181        .flatten()?;
 182
 183    let (display_uuid, persisted) =
 184        serde_json::from_str::<(Uuid, WindowBoundsJson)>(&json_str).ok()?;
 185    Some((display_uuid, persisted.into()))
 186}
 187
 188pub async fn write_default_window_bounds(
 189    bounds: WindowBounds,
 190    display_uuid: Uuid,
 191) -> anyhow::Result<()> {
 192    let persisted = WindowBoundsJson::from(bounds);
 193    let json_str = serde_json::to_string(&(display_uuid, persisted))?;
 194    KEY_VALUE_STORE
 195        .write_kvp(DEFAULT_WINDOW_BOUNDS_KEY.to_string(), json_str)
 196        .await?;
 197    Ok(())
 198}
 199
 200#[derive(Serialize, Deserialize)]
 201pub enum WindowBoundsJson {
 202    Windowed {
 203        x: i32,
 204        y: i32,
 205        width: i32,
 206        height: i32,
 207    },
 208    Maximized {
 209        x: i32,
 210        y: i32,
 211        width: i32,
 212        height: i32,
 213    },
 214    Fullscreen {
 215        x: i32,
 216        y: i32,
 217        width: i32,
 218        height: i32,
 219    },
 220}
 221
 222impl From<WindowBounds> for WindowBoundsJson {
 223    fn from(b: WindowBounds) -> Self {
 224        match b {
 225            WindowBounds::Windowed(bounds) => {
 226                let origin = bounds.origin;
 227                let size = bounds.size;
 228                WindowBoundsJson::Windowed {
 229                    x: f32::from(origin.x).round() as i32,
 230                    y: f32::from(origin.y).round() as i32,
 231                    width: f32::from(size.width).round() as i32,
 232                    height: f32::from(size.height).round() as i32,
 233                }
 234            }
 235            WindowBounds::Maximized(bounds) => {
 236                let origin = bounds.origin;
 237                let size = bounds.size;
 238                WindowBoundsJson::Maximized {
 239                    x: f32::from(origin.x).round() as i32,
 240                    y: f32::from(origin.y).round() as i32,
 241                    width: f32::from(size.width).round() as i32,
 242                    height: f32::from(size.height).round() as i32,
 243                }
 244            }
 245            WindowBounds::Fullscreen(bounds) => {
 246                let origin = bounds.origin;
 247                let size = bounds.size;
 248                WindowBoundsJson::Fullscreen {
 249                    x: f32::from(origin.x).round() as i32,
 250                    y: f32::from(origin.y).round() as i32,
 251                    width: f32::from(size.width).round() as i32,
 252                    height: f32::from(size.height).round() as i32,
 253                }
 254            }
 255        }
 256    }
 257}
 258
 259impl From<WindowBoundsJson> for WindowBounds {
 260    fn from(n: WindowBoundsJson) -> Self {
 261        match n {
 262            WindowBoundsJson::Windowed {
 263                x,
 264                y,
 265                width,
 266                height,
 267            } => WindowBounds::Windowed(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::Maximized {
 272                x,
 273                y,
 274                width,
 275                height,
 276            } => WindowBounds::Maximized(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            WindowBoundsJson::Fullscreen {
 281                x,
 282                y,
 283                width,
 284                height,
 285            } => WindowBounds::Fullscreen(Bounds {
 286                origin: point(px(x as f32), px(y as f32)),
 287                size: size(px(width as f32), px(height as f32)),
 288            }),
 289        }
 290    }
 291}
 292
 293fn multi_workspace_states() -> db::kvp::ScopedKeyValueStore<'static> {
 294    KEY_VALUE_STORE.scoped("multi_workspace_state")
 295}
 296
 297fn read_multi_workspace_state(window_id: WindowId) -> model::MultiWorkspaceState {
 298    multi_workspace_states()
 299        .read(&window_id.as_u64().to_string())
 300        .log_err()
 301        .flatten()
 302        .and_then(|json| serde_json::from_str(&json).ok())
 303        .unwrap_or_default()
 304}
 305
 306pub async fn write_multi_workspace_state(window_id: WindowId, state: model::MultiWorkspaceState) {
 307    if let Ok(json_str) = serde_json::to_string(&state) {
 308        multi_workspace_states()
 309            .write(window_id.as_u64().to_string(), json_str)
 310            .await
 311            .log_err();
 312    }
 313}
 314
 315pub fn read_serialized_multi_workspaces(
 316    session_workspaces: Vec<model::SessionWorkspace>,
 317) -> Vec<model::SerializedMultiWorkspace> {
 318    let mut window_groups: Vec<Vec<model::SessionWorkspace>> = Vec::new();
 319    let mut window_id_to_group: HashMap<WindowId, usize> = HashMap::default();
 320
 321    for session_workspace in session_workspaces {
 322        match session_workspace.window_id {
 323            Some(window_id) => {
 324                let group_index = *window_id_to_group.entry(window_id).or_insert_with(|| {
 325                    window_groups.push(Vec::new());
 326                    window_groups.len() - 1
 327                });
 328                window_groups[group_index].push(session_workspace);
 329            }
 330            None => {
 331                window_groups.push(vec![session_workspace]);
 332            }
 333        }
 334    }
 335
 336    window_groups
 337        .into_iter()
 338        .map(|group| {
 339            let window_id = group.first().and_then(|sw| sw.window_id);
 340            let state = window_id
 341                .map(read_multi_workspace_state)
 342                .unwrap_or_default();
 343            model::SerializedMultiWorkspace {
 344                id: window_id.map(|id| model::MultiWorkspaceId(id.as_u64())),
 345                workspaces: group,
 346                state,
 347            }
 348        })
 349        .collect()
 350}
 351
 352const DEFAULT_DOCK_STATE_KEY: &str = "default_dock_state";
 353
 354pub fn read_default_dock_state() -> Option<DockStructure> {
 355    let json_str = KEY_VALUE_STORE
 356        .read_kvp(DEFAULT_DOCK_STATE_KEY)
 357        .log_err()
 358        .flatten()?;
 359
 360    serde_json::from_str::<DockStructure>(&json_str).ok()
 361}
 362
 363pub async fn write_default_dock_state(docks: DockStructure) -> anyhow::Result<()> {
 364    let json_str = serde_json::to_string(&docks)?;
 365    KEY_VALUE_STORE
 366        .write_kvp(DEFAULT_DOCK_STATE_KEY.to_string(), json_str)
 367        .await?;
 368    Ok(())
 369}
 370
 371#[derive(Debug)]
 372pub struct Breakpoint {
 373    pub position: u32,
 374    pub message: Option<Arc<str>>,
 375    pub condition: Option<Arc<str>>,
 376    pub hit_condition: Option<Arc<str>>,
 377    pub state: BreakpointState,
 378}
 379
 380/// Wrapper for DB type of a breakpoint
 381struct BreakpointStateWrapper<'a>(Cow<'a, BreakpointState>);
 382
 383impl From<BreakpointState> for BreakpointStateWrapper<'static> {
 384    fn from(kind: BreakpointState) -> Self {
 385        BreakpointStateWrapper(Cow::Owned(kind))
 386    }
 387}
 388
 389impl StaticColumnCount for BreakpointStateWrapper<'_> {
 390    fn column_count() -> usize {
 391        1
 392    }
 393}
 394
 395impl Bind for BreakpointStateWrapper<'_> {
 396    fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
 397        statement.bind(&self.0.to_int(), start_index)
 398    }
 399}
 400
 401impl Column for BreakpointStateWrapper<'_> {
 402    fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
 403        let state = statement.column_int(start_index)?;
 404
 405        match state {
 406            0 => Ok((BreakpointState::Enabled.into(), start_index + 1)),
 407            1 => Ok((BreakpointState::Disabled.into(), start_index + 1)),
 408            _ => anyhow::bail!("Invalid BreakpointState discriminant {state}"),
 409        }
 410    }
 411}
 412
 413impl sqlez::bindable::StaticColumnCount for Breakpoint {
 414    fn column_count() -> usize {
 415        // Position, log message, condition message, and hit condition message
 416        4 + BreakpointStateWrapper::column_count()
 417    }
 418}
 419
 420impl sqlez::bindable::Bind for Breakpoint {
 421    fn bind(
 422        &self,
 423        statement: &sqlez::statement::Statement,
 424        start_index: i32,
 425    ) -> anyhow::Result<i32> {
 426        let next_index = statement.bind(&self.position, start_index)?;
 427        let next_index = statement.bind(&self.message, next_index)?;
 428        let next_index = statement.bind(&self.condition, next_index)?;
 429        let next_index = statement.bind(&self.hit_condition, next_index)?;
 430        statement.bind(
 431            &BreakpointStateWrapper(Cow::Borrowed(&self.state)),
 432            next_index,
 433        )
 434    }
 435}
 436
 437impl Column for Breakpoint {
 438    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 439        let position = statement
 440            .column_int(start_index)
 441            .with_context(|| format!("Failed to read BreakPoint at index {start_index}"))?
 442            as u32;
 443        let (message, next_index) = Option::<String>::column(statement, start_index + 1)?;
 444        let (condition, next_index) = Option::<String>::column(statement, next_index)?;
 445        let (hit_condition, next_index) = Option::<String>::column(statement, next_index)?;
 446        let (state, next_index) = BreakpointStateWrapper::column(statement, next_index)?;
 447
 448        Ok((
 449            Breakpoint {
 450                position,
 451                message: message.map(Arc::from),
 452                condition: condition.map(Arc::from),
 453                hit_condition: hit_condition.map(Arc::from),
 454                state: state.0.into_owned(),
 455            },
 456            next_index,
 457        ))
 458    }
 459}
 460
 461#[derive(Clone, Debug, PartialEq)]
 462struct SerializedPixels(gpui::Pixels);
 463impl sqlez::bindable::StaticColumnCount for SerializedPixels {}
 464
 465impl sqlez::bindable::Bind for SerializedPixels {
 466    fn bind(
 467        &self,
 468        statement: &sqlez::statement::Statement,
 469        start_index: i32,
 470    ) -> anyhow::Result<i32> {
 471        let this: i32 = u32::from(self.0) as _;
 472        this.bind(statement, start_index)
 473    }
 474}
 475
 476pub struct WorkspaceDb(ThreadSafeConnection);
 477
 478impl Domain for WorkspaceDb {
 479    const NAME: &str = stringify!(WorkspaceDb);
 480
 481    const MIGRATIONS: &[&str] = &[
 482        sql!(
 483            CREATE TABLE workspaces(
 484                workspace_id INTEGER PRIMARY KEY,
 485                workspace_location BLOB UNIQUE,
 486                dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
 487                dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
 488                dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
 489                left_sidebar_open INTEGER, // Boolean
 490                timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 491                FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
 492            ) STRICT;
 493
 494            CREATE TABLE pane_groups(
 495                group_id INTEGER PRIMARY KEY,
 496                workspace_id INTEGER NOT NULL,
 497                parent_group_id INTEGER, // NULL indicates that this is a root node
 498                position INTEGER, // NULL indicates that this is a root node
 499                axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
 500                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 501                ON DELETE CASCADE
 502                ON UPDATE CASCADE,
 503                FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 504            ) STRICT;
 505
 506            CREATE TABLE panes(
 507                pane_id INTEGER PRIMARY KEY,
 508                workspace_id INTEGER NOT NULL,
 509                active INTEGER NOT NULL, // Boolean
 510                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 511                ON DELETE CASCADE
 512                ON UPDATE CASCADE
 513            ) STRICT;
 514
 515            CREATE TABLE center_panes(
 516                pane_id INTEGER PRIMARY KEY,
 517                parent_group_id INTEGER, // NULL means that this is a root pane
 518                position INTEGER, // NULL means that this is a root pane
 519                FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 520                ON DELETE CASCADE,
 521                FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 522            ) STRICT;
 523
 524            CREATE TABLE items(
 525                item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
 526                workspace_id INTEGER NOT NULL,
 527                pane_id INTEGER NOT NULL,
 528                kind TEXT NOT NULL,
 529                position INTEGER NOT NULL,
 530                active INTEGER NOT NULL,
 531                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 532                ON DELETE CASCADE
 533                ON UPDATE CASCADE,
 534                FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 535                ON DELETE CASCADE,
 536                PRIMARY KEY(item_id, workspace_id)
 537            ) STRICT;
 538        ),
 539        sql!(
 540            ALTER TABLE workspaces ADD COLUMN window_state TEXT;
 541            ALTER TABLE workspaces ADD COLUMN window_x REAL;
 542            ALTER TABLE workspaces ADD COLUMN window_y REAL;
 543            ALTER TABLE workspaces ADD COLUMN window_width REAL;
 544            ALTER TABLE workspaces ADD COLUMN window_height REAL;
 545            ALTER TABLE workspaces ADD COLUMN display BLOB;
 546        ),
 547        // Drop foreign key constraint from workspaces.dock_pane to panes table.
 548        sql!(
 549            CREATE TABLE workspaces_2(
 550                workspace_id INTEGER PRIMARY KEY,
 551                workspace_location BLOB UNIQUE,
 552                dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
 553                dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
 554                dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
 555                left_sidebar_open INTEGER, // Boolean
 556                timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 557                window_state TEXT,
 558                window_x REAL,
 559                window_y REAL,
 560                window_width REAL,
 561                window_height REAL,
 562                display BLOB
 563            ) STRICT;
 564            INSERT INTO workspaces_2 SELECT * FROM workspaces;
 565            DROP TABLE workspaces;
 566            ALTER TABLE workspaces_2 RENAME TO workspaces;
 567        ),
 568        // Add panels related information
 569        sql!(
 570            ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
 571            ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
 572            ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
 573            ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
 574            ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
 575            ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
 576        ),
 577        // Add panel zoom persistence
 578        sql!(
 579            ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
 580            ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
 581            ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
 582        ),
 583        // Add pane group flex data
 584        sql!(
 585            ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
 586        ),
 587        // Add fullscreen field to workspace
 588        // Deprecated, `WindowBounds` holds the fullscreen state now.
 589        // Preserving so users can downgrade Zed.
 590        sql!(
 591            ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
 592        ),
 593        // Add preview field to items
 594        sql!(
 595            ALTER TABLE items ADD COLUMN preview INTEGER; //bool
 596        ),
 597        // Add centered_layout field to workspace
 598        sql!(
 599            ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
 600        ),
 601        sql!(
 602            CREATE TABLE remote_projects (
 603                remote_project_id INTEGER NOT NULL UNIQUE,
 604                path TEXT,
 605                dev_server_name TEXT
 606            );
 607            ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
 608            ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
 609        ),
 610        sql!(
 611            DROP TABLE remote_projects;
 612            CREATE TABLE dev_server_projects (
 613                id INTEGER NOT NULL UNIQUE,
 614                path TEXT,
 615                dev_server_name TEXT
 616            );
 617            ALTER TABLE workspaces DROP COLUMN remote_project_id;
 618            ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
 619        ),
 620        sql!(
 621            ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
 622        ),
 623        sql!(
 624            ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
 625        ),
 626        sql!(
 627            ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
 628        ),
 629        sql!(
 630            ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
 631        ),
 632        sql!(
 633            CREATE TABLE ssh_projects (
 634                id INTEGER PRIMARY KEY,
 635                host TEXT NOT NULL,
 636                port INTEGER,
 637                path TEXT NOT NULL,
 638                user TEXT
 639            );
 640            ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
 641        ),
 642        sql!(
 643            ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
 644        ),
 645        sql!(
 646            CREATE TABLE toolchains (
 647                workspace_id INTEGER,
 648                worktree_id INTEGER,
 649                language_name TEXT NOT NULL,
 650                name TEXT NOT NULL,
 651                path TEXT NOT NULL,
 652                PRIMARY KEY (workspace_id, worktree_id, language_name)
 653            );
 654        ),
 655        sql!(
 656            ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}";
 657        ),
 658        sql!(
 659            CREATE TABLE breakpoints (
 660                workspace_id INTEGER NOT NULL,
 661                path TEXT NOT NULL,
 662                breakpoint_location INTEGER NOT NULL,
 663                kind INTEGER NOT NULL,
 664                log_message TEXT,
 665                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 666                ON DELETE CASCADE
 667                ON UPDATE CASCADE
 668            );
 669        ),
 670        sql!(
 671            ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT;
 672            CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array);
 673            ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT;
 674        ),
 675        sql!(
 676            ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL
 677        ),
 678        sql!(
 679            ALTER TABLE breakpoints DROP COLUMN kind
 680        ),
 681        sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL),
 682        sql!(
 683            ALTER TABLE breakpoints ADD COLUMN condition TEXT;
 684            ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT;
 685        ),
 686        sql!(CREATE TABLE toolchains2 (
 687            workspace_id INTEGER,
 688            worktree_id INTEGER,
 689            language_name TEXT NOT NULL,
 690            name TEXT NOT NULL,
 691            path TEXT NOT NULL,
 692            raw_json TEXT NOT NULL,
 693            relative_worktree_path TEXT NOT NULL,
 694            PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT;
 695            INSERT INTO toolchains2
 696                SELECT * FROM toolchains;
 697            DROP TABLE toolchains;
 698            ALTER TABLE toolchains2 RENAME TO toolchains;
 699        ),
 700        sql!(
 701            CREATE TABLE ssh_connections (
 702                id INTEGER PRIMARY KEY,
 703                host TEXT NOT NULL,
 704                port INTEGER,
 705                user TEXT
 706            );
 707
 708            INSERT INTO ssh_connections (host, port, user)
 709            SELECT DISTINCT host, port, user
 710            FROM ssh_projects;
 711
 712            CREATE TABLE workspaces_2(
 713                workspace_id INTEGER PRIMARY KEY,
 714                paths TEXT,
 715                paths_order TEXT,
 716                ssh_connection_id INTEGER REFERENCES ssh_connections(id),
 717                timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 718                window_state TEXT,
 719                window_x REAL,
 720                window_y REAL,
 721                window_width REAL,
 722                window_height REAL,
 723                display BLOB,
 724                left_dock_visible INTEGER,
 725                left_dock_active_panel TEXT,
 726                right_dock_visible INTEGER,
 727                right_dock_active_panel TEXT,
 728                bottom_dock_visible INTEGER,
 729                bottom_dock_active_panel TEXT,
 730                left_dock_zoom INTEGER,
 731                right_dock_zoom INTEGER,
 732                bottom_dock_zoom INTEGER,
 733                fullscreen INTEGER,
 734                centered_layout INTEGER,
 735                session_id TEXT,
 736                window_id INTEGER
 737            ) STRICT;
 738
 739            INSERT
 740            INTO workspaces_2
 741            SELECT
 742                workspaces.workspace_id,
 743                CASE
 744                    WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths
 745                    ELSE
 746                        CASE
 747                            WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN
 748                                NULL
 749                            ELSE
 750                                replace(workspaces.local_paths_array, ',', CHAR(10))
 751                        END
 752                END as paths,
 753
 754                CASE
 755                    WHEN ssh_projects.id IS NOT NULL THEN ""
 756                    ELSE workspaces.local_paths_order_array
 757                END as paths_order,
 758
 759                CASE
 760                    WHEN ssh_projects.id IS NOT NULL THEN (
 761                        SELECT ssh_connections.id
 762                        FROM ssh_connections
 763                        WHERE
 764                            ssh_connections.host IS ssh_projects.host AND
 765                            ssh_connections.port IS ssh_projects.port AND
 766                            ssh_connections.user IS ssh_projects.user
 767                    )
 768                    ELSE NULL
 769                END as ssh_connection_id,
 770
 771                workspaces.timestamp,
 772                workspaces.window_state,
 773                workspaces.window_x,
 774                workspaces.window_y,
 775                workspaces.window_width,
 776                workspaces.window_height,
 777                workspaces.display,
 778                workspaces.left_dock_visible,
 779                workspaces.left_dock_active_panel,
 780                workspaces.right_dock_visible,
 781                workspaces.right_dock_active_panel,
 782                workspaces.bottom_dock_visible,
 783                workspaces.bottom_dock_active_panel,
 784                workspaces.left_dock_zoom,
 785                workspaces.right_dock_zoom,
 786                workspaces.bottom_dock_zoom,
 787                workspaces.fullscreen,
 788                workspaces.centered_layout,
 789                workspaces.session_id,
 790                workspaces.window_id
 791            FROM
 792                workspaces LEFT JOIN
 793                ssh_projects ON
 794                workspaces.ssh_project_id = ssh_projects.id;
 795
 796            DELETE FROM workspaces_2
 797            WHERE workspace_id NOT IN (
 798                SELECT MAX(workspace_id)
 799                FROM workspaces_2
 800                GROUP BY ssh_connection_id, paths
 801            );
 802
 803            DROP TABLE ssh_projects;
 804            DROP TABLE workspaces;
 805            ALTER TABLE workspaces_2 RENAME TO workspaces;
 806
 807            CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths);
 808        ),
 809        // Fix any data from when workspaces.paths were briefly encoded as JSON arrays
 810        sql!(
 811            UPDATE workspaces
 812            SET paths = CASE
 813                WHEN substr(paths, 1, 2) = '[' || '"' AND substr(paths, -2, 2) = '"' || ']' THEN
 814                    replace(
 815                        substr(paths, 3, length(paths) - 4),
 816                        '"' || ',' || '"',
 817                        CHAR(10)
 818                    )
 819                ELSE
 820                    replace(paths, ',', CHAR(10))
 821            END
 822            WHERE paths IS NOT NULL
 823        ),
 824        sql!(
 825            CREATE TABLE remote_connections(
 826                id INTEGER PRIMARY KEY,
 827                kind TEXT NOT NULL,
 828                host TEXT,
 829                port INTEGER,
 830                user TEXT,
 831                distro TEXT
 832            );
 833
 834            CREATE TABLE workspaces_2(
 835                workspace_id INTEGER PRIMARY KEY,
 836                paths TEXT,
 837                paths_order TEXT,
 838                remote_connection_id INTEGER REFERENCES remote_connections(id),
 839                timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 840                window_state TEXT,
 841                window_x REAL,
 842                window_y REAL,
 843                window_width REAL,
 844                window_height REAL,
 845                display BLOB,
 846                left_dock_visible INTEGER,
 847                left_dock_active_panel TEXT,
 848                right_dock_visible INTEGER,
 849                right_dock_active_panel TEXT,
 850                bottom_dock_visible INTEGER,
 851                bottom_dock_active_panel TEXT,
 852                left_dock_zoom INTEGER,
 853                right_dock_zoom INTEGER,
 854                bottom_dock_zoom INTEGER,
 855                fullscreen INTEGER,
 856                centered_layout INTEGER,
 857                session_id TEXT,
 858                window_id INTEGER
 859            ) STRICT;
 860
 861            INSERT INTO remote_connections
 862            SELECT
 863                id,
 864                "ssh" as kind,
 865                host,
 866                port,
 867                user,
 868                NULL as distro
 869            FROM ssh_connections;
 870
 871            INSERT
 872            INTO workspaces_2
 873            SELECT
 874                workspace_id,
 875                paths,
 876                paths_order,
 877                ssh_connection_id as remote_connection_id,
 878                timestamp,
 879                window_state,
 880                window_x,
 881                window_y,
 882                window_width,
 883                window_height,
 884                display,
 885                left_dock_visible,
 886                left_dock_active_panel,
 887                right_dock_visible,
 888                right_dock_active_panel,
 889                bottom_dock_visible,
 890                bottom_dock_active_panel,
 891                left_dock_zoom,
 892                right_dock_zoom,
 893                bottom_dock_zoom,
 894                fullscreen,
 895                centered_layout,
 896                session_id,
 897                window_id
 898            FROM
 899                workspaces;
 900
 901            DROP TABLE workspaces;
 902            ALTER TABLE workspaces_2 RENAME TO workspaces;
 903
 904            CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(remote_connection_id, paths);
 905        ),
 906        sql!(CREATE TABLE user_toolchains (
 907            remote_connection_id INTEGER,
 908            workspace_id INTEGER NOT NULL,
 909            worktree_id INTEGER NOT NULL,
 910            relative_worktree_path TEXT NOT NULL,
 911            language_name TEXT NOT NULL,
 912            name TEXT NOT NULL,
 913            path TEXT NOT NULL,
 914            raw_json TEXT NOT NULL,
 915
 916            PRIMARY KEY (workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json)
 917        ) STRICT;),
 918        sql!(
 919            DROP TABLE ssh_connections;
 920        ),
 921        sql!(
 922            ALTER TABLE remote_connections ADD COLUMN name TEXT;
 923            ALTER TABLE remote_connections ADD COLUMN container_id TEXT;
 924        ),
 925        sql!(
 926            CREATE TABLE IF NOT EXISTS trusted_worktrees (
 927                trust_id INTEGER PRIMARY KEY AUTOINCREMENT,
 928                absolute_path TEXT,
 929                user_name TEXT,
 930                host_name TEXT
 931            ) STRICT;
 932        ),
 933        sql!(CREATE TABLE toolchains2 (
 934            workspace_id INTEGER,
 935            worktree_root_path TEXT NOT NULL,
 936            language_name TEXT NOT NULL,
 937            name TEXT NOT NULL,
 938            path TEXT NOT NULL,
 939            raw_json TEXT NOT NULL,
 940            relative_worktree_path TEXT NOT NULL,
 941            PRIMARY KEY (workspace_id, worktree_root_path, language_name, relative_worktree_path)) STRICT;
 942            INSERT OR REPLACE INTO toolchains2
 943                // The `instr(paths, '\n') = 0` part allows us to find all
 944                // workspaces that have a single worktree, as `\n` is used as a
 945                // separator when serializing the workspace paths, so if no `\n` is
 946                // found, we know we have a single worktree.
 947                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;
 948            DROP TABLE toolchains;
 949            ALTER TABLE toolchains2 RENAME TO toolchains;
 950        ),
 951        sql!(CREATE TABLE user_toolchains2 (
 952            remote_connection_id INTEGER,
 953            workspace_id INTEGER NOT NULL,
 954            worktree_root_path TEXT NOT NULL,
 955            relative_worktree_path TEXT NOT NULL,
 956            language_name TEXT NOT NULL,
 957            name TEXT NOT NULL,
 958            path TEXT NOT NULL,
 959            raw_json TEXT NOT NULL,
 960
 961            PRIMARY KEY (workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json)) STRICT;
 962            INSERT OR REPLACE INTO user_toolchains2
 963                // The `instr(paths, '\n') = 0` part allows us to find all
 964                // workspaces that have a single worktree, as `\n` is used as a
 965                // separator when serializing the workspace paths, so if no `\n` is
 966                // found, we know we have a single worktree.
 967                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;
 968            DROP TABLE user_toolchains;
 969            ALTER TABLE user_toolchains2 RENAME TO user_toolchains;
 970        ),
 971        sql!(
 972            ALTER TABLE remote_connections ADD COLUMN use_podman BOOLEAN;
 973        ),
 974    ];
 975
 976    // Allow recovering from bad migration that was initially shipped to nightly
 977    // when introducing the ssh_connections table.
 978    fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool {
 979        old.starts_with("CREATE TABLE ssh_connections")
 980            && new.starts_with("CREATE TABLE ssh_connections")
 981    }
 982}
 983
 984db::static_connection!(DB, WorkspaceDb, []);
 985
 986impl WorkspaceDb {
 987    /// Returns a serialized workspace for the given worktree_roots. If the passed array
 988    /// is empty, the most recent workspace is returned instead. If no workspace for the
 989    /// passed roots is stored, returns none.
 990    pub(crate) fn workspace_for_roots<P: AsRef<Path>>(
 991        &self,
 992        worktree_roots: &[P],
 993    ) -> Option<SerializedWorkspace> {
 994        self.workspace_for_roots_internal(worktree_roots, None)
 995    }
 996
 997    pub(crate) fn remote_workspace_for_roots<P: AsRef<Path>>(
 998        &self,
 999        worktree_roots: &[P],
1000        remote_project_id: RemoteConnectionId,
1001    ) -> Option<SerializedWorkspace> {
1002        self.workspace_for_roots_internal(worktree_roots, Some(remote_project_id))
1003    }
1004
1005    pub(crate) fn workspace_for_roots_internal<P: AsRef<Path>>(
1006        &self,
1007        worktree_roots: &[P],
1008        remote_connection_id: Option<RemoteConnectionId>,
1009    ) -> Option<SerializedWorkspace> {
1010        // paths are sorted before db interactions to ensure that the order of the paths
1011        // doesn't affect the workspace selection for existing workspaces
1012        let root_paths = PathList::new(worktree_roots);
1013
1014        // Empty workspaces cannot be matched by paths (all empty workspaces have paths = "").
1015        // They should only be restored via workspace_for_id during session restoration.
1016        if root_paths.is_empty() && remote_connection_id.is_none() {
1017            return None;
1018        }
1019
1020        // Note that we re-assign the workspace_id here in case it's empty
1021        // and we've grabbed the most recent workspace
1022        let (
1023            workspace_id,
1024            paths,
1025            paths_order,
1026            window_bounds,
1027            display,
1028            centered_layout,
1029            docks,
1030            window_id,
1031        ): (
1032            WorkspaceId,
1033            String,
1034            String,
1035            Option<SerializedWindowBounds>,
1036            Option<Uuid>,
1037            Option<bool>,
1038            DockStructure,
1039            Option<u64>,
1040        ) = self
1041            .select_row_bound(sql! {
1042                SELECT
1043                    workspace_id,
1044                    paths,
1045                    paths_order,
1046                    window_state,
1047                    window_x,
1048                    window_y,
1049                    window_width,
1050                    window_height,
1051                    display,
1052                    centered_layout,
1053                    left_dock_visible,
1054                    left_dock_active_panel,
1055                    left_dock_zoom,
1056                    right_dock_visible,
1057                    right_dock_active_panel,
1058                    right_dock_zoom,
1059                    bottom_dock_visible,
1060                    bottom_dock_active_panel,
1061                    bottom_dock_zoom,
1062                    window_id
1063                FROM workspaces
1064                WHERE
1065                    paths IS ? AND
1066                    remote_connection_id IS ?
1067                LIMIT 1
1068            })
1069            .and_then(|mut prepared_statement| {
1070                (prepared_statement)((
1071                    root_paths.serialize().paths,
1072                    remote_connection_id.map(|id| id.0 as i32),
1073                ))
1074            })
1075            .context("No workspaces found")
1076            .warn_on_err()
1077            .flatten()?;
1078
1079        let paths = PathList::deserialize(&SerializedPathList {
1080            paths,
1081            order: paths_order,
1082        });
1083
1084        let remote_connection_options = if let Some(remote_connection_id) = remote_connection_id {
1085            self.remote_connection(remote_connection_id)
1086                .context("Get remote connection")
1087                .log_err()
1088        } else {
1089            None
1090        };
1091
1092        Some(SerializedWorkspace {
1093            id: workspace_id,
1094            location: match remote_connection_options {
1095                Some(options) => SerializedWorkspaceLocation::Remote(options),
1096                None => SerializedWorkspaceLocation::Local,
1097            },
1098            paths,
1099            center_group: self
1100                .get_center_pane_group(workspace_id)
1101                .context("Getting center group")
1102                .log_err()?,
1103            window_bounds,
1104            centered_layout: centered_layout.unwrap_or(false),
1105            display,
1106            docks,
1107            session_id: None,
1108            breakpoints: self.breakpoints(workspace_id),
1109            window_id,
1110            user_toolchains: self.user_toolchains(workspace_id, remote_connection_id),
1111        })
1112    }
1113
1114    /// Returns the workspace with the given ID, loading all associated data.
1115    pub(crate) fn workspace_for_id(
1116        &self,
1117        workspace_id: WorkspaceId,
1118    ) -> Option<SerializedWorkspace> {
1119        let (
1120            paths,
1121            paths_order,
1122            window_bounds,
1123            display,
1124            centered_layout,
1125            docks,
1126            window_id,
1127            remote_connection_id,
1128        ): (
1129            String,
1130            String,
1131            Option<SerializedWindowBounds>,
1132            Option<Uuid>,
1133            Option<bool>,
1134            DockStructure,
1135            Option<u64>,
1136            Option<i32>,
1137        ) = self
1138            .select_row_bound(sql! {
1139                SELECT
1140                    paths,
1141                    paths_order,
1142                    window_state,
1143                    window_x,
1144                    window_y,
1145                    window_width,
1146                    window_height,
1147                    display,
1148                    centered_layout,
1149                    left_dock_visible,
1150                    left_dock_active_panel,
1151                    left_dock_zoom,
1152                    right_dock_visible,
1153                    right_dock_active_panel,
1154                    right_dock_zoom,
1155                    bottom_dock_visible,
1156                    bottom_dock_active_panel,
1157                    bottom_dock_zoom,
1158                    window_id,
1159                    remote_connection_id
1160                FROM workspaces
1161                WHERE workspace_id = ?
1162            })
1163            .and_then(|mut prepared_statement| (prepared_statement)(workspace_id))
1164            .context("No workspace found for id")
1165            .warn_on_err()
1166            .flatten()?;
1167
1168        let paths = PathList::deserialize(&SerializedPathList {
1169            paths,
1170            order: paths_order,
1171        });
1172
1173        let remote_connection_id = remote_connection_id.map(|id| RemoteConnectionId(id as u64));
1174        let remote_connection_options = if let Some(remote_connection_id) = remote_connection_id {
1175            self.remote_connection(remote_connection_id)
1176                .context("Get remote connection")
1177                .log_err()
1178        } else {
1179            None
1180        };
1181
1182        Some(SerializedWorkspace {
1183            id: workspace_id,
1184            location: match remote_connection_options {
1185                Some(options) => SerializedWorkspaceLocation::Remote(options),
1186                None => SerializedWorkspaceLocation::Local,
1187            },
1188            paths,
1189            center_group: self
1190                .get_center_pane_group(workspace_id)
1191                .context("Getting center group")
1192                .log_err()?,
1193            window_bounds,
1194            centered_layout: centered_layout.unwrap_or(false),
1195            display,
1196            docks,
1197            session_id: None,
1198            breakpoints: self.breakpoints(workspace_id),
1199            window_id,
1200            user_toolchains: self.user_toolchains(workspace_id, remote_connection_id),
1201        })
1202    }
1203
1204    fn breakpoints(&self, workspace_id: WorkspaceId) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
1205        let breakpoints: Result<Vec<(PathBuf, Breakpoint)>> = self
1206            .select_bound(sql! {
1207                SELECT path, breakpoint_location, log_message, condition, hit_condition, state
1208                FROM breakpoints
1209                WHERE workspace_id = ?
1210            })
1211            .and_then(|mut prepared_statement| (prepared_statement)(workspace_id));
1212
1213        match breakpoints {
1214            Ok(bp) => {
1215                if bp.is_empty() {
1216                    log::debug!("Breakpoints are empty after querying database for them");
1217                }
1218
1219                let mut map: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> = Default::default();
1220
1221                for (path, breakpoint) in bp {
1222                    let path: Arc<Path> = path.into();
1223                    map.entry(path.clone()).or_default().push(SourceBreakpoint {
1224                        row: breakpoint.position,
1225                        path,
1226                        message: breakpoint.message,
1227                        condition: breakpoint.condition,
1228                        hit_condition: breakpoint.hit_condition,
1229                        state: breakpoint.state,
1230                    });
1231                }
1232
1233                for (path, bps) in map.iter() {
1234                    log::info!(
1235                        "Got {} breakpoints from database at path: {}",
1236                        bps.len(),
1237                        path.to_string_lossy()
1238                    );
1239                }
1240
1241                map
1242            }
1243            Err(msg) => {
1244                log::error!("Breakpoints query failed with msg: {msg}");
1245                Default::default()
1246            }
1247        }
1248    }
1249
1250    fn user_toolchains(
1251        &self,
1252        workspace_id: WorkspaceId,
1253        remote_connection_id: Option<RemoteConnectionId>,
1254    ) -> BTreeMap<ToolchainScope, IndexSet<Toolchain>> {
1255        type RowKind = (WorkspaceId, String, String, String, String, String, String);
1256
1257        let toolchains: Vec<RowKind> = self
1258            .select_bound(sql! {
1259                SELECT workspace_id, worktree_root_path, relative_worktree_path,
1260                language_name, name, path, raw_json
1261                FROM user_toolchains WHERE remote_connection_id IS ?1 AND (
1262                      workspace_id IN (0, ?2)
1263                )
1264            })
1265            .and_then(|mut statement| {
1266                (statement)((remote_connection_id.map(|id| id.0), workspace_id))
1267            })
1268            .unwrap_or_default();
1269        let mut ret = BTreeMap::<_, IndexSet<_>>::default();
1270
1271        for (
1272            _workspace_id,
1273            worktree_root_path,
1274            relative_worktree_path,
1275            language_name,
1276            name,
1277            path,
1278            raw_json,
1279        ) in toolchains
1280        {
1281            // INTEGER's that are primary keys (like workspace ids, remote connection ids and such) start at 1, so we're safe to
1282            let scope = if _workspace_id == WorkspaceId(0) {
1283                debug_assert_eq!(worktree_root_path, String::default());
1284                debug_assert_eq!(relative_worktree_path, String::default());
1285                ToolchainScope::Global
1286            } else {
1287                debug_assert_eq!(workspace_id, _workspace_id);
1288                debug_assert_eq!(
1289                    worktree_root_path == String::default(),
1290                    relative_worktree_path == String::default()
1291                );
1292
1293                let Some(relative_path) = RelPath::unix(&relative_worktree_path).log_err() else {
1294                    continue;
1295                };
1296                if worktree_root_path != String::default()
1297                    && relative_worktree_path != String::default()
1298                {
1299                    ToolchainScope::Subproject(
1300                        Arc::from(worktree_root_path.as_ref()),
1301                        relative_path.into(),
1302                    )
1303                } else {
1304                    ToolchainScope::Project
1305                }
1306            };
1307            let Ok(as_json) = serde_json::from_str(&raw_json) else {
1308                continue;
1309            };
1310            let toolchain = Toolchain {
1311                name: SharedString::from(name),
1312                path: SharedString::from(path),
1313                language_name: LanguageName::from_proto(language_name),
1314                as_json,
1315            };
1316            ret.entry(scope).or_default().insert(toolchain);
1317        }
1318
1319        ret
1320    }
1321
1322    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
1323    /// that used this workspace previously
1324    pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
1325        let paths = workspace.paths.serialize();
1326        log::debug!("Saving workspace at location: {:?}", workspace.location);
1327        self.write(move |conn| {
1328            conn.with_savepoint("update_worktrees", || {
1329                let remote_connection_id = match workspace.location.clone() {
1330                    SerializedWorkspaceLocation::Local => None,
1331                    SerializedWorkspaceLocation::Remote(connection_options) => {
1332                        Some(Self::get_or_create_remote_connection_internal(
1333                            conn,
1334                            connection_options
1335                        )?.0)
1336                    }
1337                };
1338
1339                // Clear out panes and pane_groups
1340                conn.exec_bound(sql!(
1341                    DELETE FROM pane_groups WHERE workspace_id = ?1;
1342                    DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
1343                    .context("Clearing old panes")?;
1344
1345                conn.exec_bound(
1346                    sql!(
1347                        DELETE FROM breakpoints WHERE workspace_id = ?1;
1348                    )
1349                )?(workspace.id).context("Clearing old breakpoints")?;
1350
1351                for (path, breakpoints) in workspace.breakpoints {
1352                    for bp in breakpoints {
1353                        let state = BreakpointStateWrapper::from(bp.state);
1354                        match conn.exec_bound(sql!(
1355                            INSERT INTO breakpoints (workspace_id, path, breakpoint_location,  log_message, condition, hit_condition, state)
1356                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);))?
1357
1358                        ((
1359                            workspace.id,
1360                            path.as_ref(),
1361                            bp.row,
1362                            bp.message,
1363                            bp.condition,
1364                            bp.hit_condition,
1365                            state,
1366                        )) {
1367                            Ok(_) => {
1368                                log::debug!("Stored breakpoint at row: {} in path: {}", bp.row, path.to_string_lossy())
1369                            }
1370                            Err(err) => {
1371                                log::error!("{err}");
1372                                continue;
1373                            }
1374                        }
1375                    }
1376                }
1377
1378                conn.exec_bound(
1379                    sql!(
1380                        DELETE FROM user_toolchains WHERE workspace_id = ?1;
1381                    )
1382                )?(workspace.id).context("Clearing old user toolchains")?;
1383
1384                for (scope, toolchains) in workspace.user_toolchains {
1385                    for toolchain in toolchains {
1386                        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));
1387                        let (workspace_id, worktree_root_path, relative_worktree_path) = match scope {
1388                            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())),
1389                            ToolchainScope::Project => (Some(workspace.id), None, None),
1390                            ToolchainScope::Global => (None, None, None),
1391                        };
1392                        let args = (remote_connection_id, workspace_id.unwrap_or(WorkspaceId(0)), worktree_root_path.unwrap_or_default(), relative_worktree_path.unwrap_or_default(),
1393                        toolchain.language_name.as_ref().to_owned(), toolchain.name.to_string(), toolchain.path.to_string(), toolchain.as_json.to_string());
1394                        if let Err(err) = conn.exec_bound(query)?(args) {
1395                            log::error!("{err}");
1396                            continue;
1397                        }
1398                    }
1399                }
1400
1401                // Clear out old workspaces with the same paths.
1402                // Skip this for empty workspaces - they are identified by workspace_id, not paths.
1403                // Multiple empty workspaces with different content should coexist.
1404                if !paths.paths.is_empty() {
1405                    conn.exec_bound(sql!(
1406                        DELETE
1407                        FROM workspaces
1408                        WHERE
1409                            workspace_id != ?1 AND
1410                            paths IS ?2 AND
1411                            remote_connection_id IS ?3
1412                    ))?((
1413                        workspace.id,
1414                        paths.paths.clone(),
1415                        remote_connection_id,
1416                    ))
1417                    .context("clearing out old locations")?;
1418                }
1419
1420                // Upsert
1421                let query = sql!(
1422                    INSERT INTO workspaces(
1423                        workspace_id,
1424                        paths,
1425                        paths_order,
1426                        remote_connection_id,
1427                        left_dock_visible,
1428                        left_dock_active_panel,
1429                        left_dock_zoom,
1430                        right_dock_visible,
1431                        right_dock_active_panel,
1432                        right_dock_zoom,
1433                        bottom_dock_visible,
1434                        bottom_dock_active_panel,
1435                        bottom_dock_zoom,
1436                        session_id,
1437                        window_id,
1438                        timestamp
1439                    )
1440                    VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, CURRENT_TIMESTAMP)
1441                    ON CONFLICT DO
1442                    UPDATE SET
1443                        paths = ?2,
1444                        paths_order = ?3,
1445                        remote_connection_id = ?4,
1446                        left_dock_visible = ?5,
1447                        left_dock_active_panel = ?6,
1448                        left_dock_zoom = ?7,
1449                        right_dock_visible = ?8,
1450                        right_dock_active_panel = ?9,
1451                        right_dock_zoom = ?10,
1452                        bottom_dock_visible = ?11,
1453                        bottom_dock_active_panel = ?12,
1454                        bottom_dock_zoom = ?13,
1455                        session_id = ?14,
1456                        window_id = ?15,
1457                        timestamp = CURRENT_TIMESTAMP
1458                );
1459                let mut prepared_query = conn.exec_bound(query)?;
1460                let args = (
1461                    workspace.id,
1462                    paths.paths.clone(),
1463                    paths.order.clone(),
1464                    remote_connection_id,
1465                    workspace.docks,
1466                    workspace.session_id,
1467                    workspace.window_id,
1468                );
1469
1470                prepared_query(args).context("Updating workspace")?;
1471
1472                // Save center pane group
1473                Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
1474                    .context("save pane group in save workspace")?;
1475
1476                Ok(())
1477            })
1478            .log_err();
1479        })
1480        .await;
1481    }
1482
1483    pub(crate) async fn get_or_create_remote_connection(
1484        &self,
1485        options: RemoteConnectionOptions,
1486    ) -> Result<RemoteConnectionId> {
1487        self.write(move |conn| Self::get_or_create_remote_connection_internal(conn, options))
1488            .await
1489    }
1490
1491    fn get_or_create_remote_connection_internal(
1492        this: &Connection,
1493        options: RemoteConnectionOptions,
1494    ) -> Result<RemoteConnectionId> {
1495        let kind;
1496        let user: Option<String>;
1497        let mut host = None;
1498        let mut port = None;
1499        let mut distro = None;
1500        let mut name = None;
1501        let mut container_id = None;
1502        let mut use_podman = None;
1503        match options {
1504            RemoteConnectionOptions::Ssh(options) => {
1505                kind = RemoteConnectionKind::Ssh;
1506                host = Some(options.host.to_string());
1507                port = options.port;
1508                user = options.username;
1509            }
1510            RemoteConnectionOptions::Wsl(options) => {
1511                kind = RemoteConnectionKind::Wsl;
1512                distro = Some(options.distro_name);
1513                user = options.user;
1514            }
1515            RemoteConnectionOptions::Docker(options) => {
1516                kind = RemoteConnectionKind::Docker;
1517                container_id = Some(options.container_id);
1518                name = Some(options.name);
1519                use_podman = Some(options.use_podman);
1520                user = Some(options.remote_user);
1521            }
1522            #[cfg(any(test, feature = "test-support"))]
1523            RemoteConnectionOptions::Mock(options) => {
1524                kind = RemoteConnectionKind::Ssh;
1525                host = Some(format!("mock-{}", options.id));
1526                user = Some(format!("mock-user-{}", options.id));
1527            }
1528        }
1529        Self::get_or_create_remote_connection_query(
1530            this,
1531            kind,
1532            host,
1533            port,
1534            user,
1535            distro,
1536            name,
1537            container_id,
1538            use_podman,
1539        )
1540    }
1541
1542    fn get_or_create_remote_connection_query(
1543        this: &Connection,
1544        kind: RemoteConnectionKind,
1545        host: Option<String>,
1546        port: Option<u16>,
1547        user: Option<String>,
1548        distro: Option<String>,
1549        name: Option<String>,
1550        container_id: Option<String>,
1551        use_podman: Option<bool>,
1552    ) -> Result<RemoteConnectionId> {
1553        if let Some(id) = this.select_row_bound(sql!(
1554            SELECT id
1555            FROM remote_connections
1556            WHERE
1557                kind IS ? AND
1558                host IS ? AND
1559                port IS ? AND
1560                user IS ? AND
1561                distro IS ? AND
1562                name IS ? AND
1563                container_id IS ?
1564            LIMIT 1
1565        ))?((
1566            kind.serialize(),
1567            host.clone(),
1568            port,
1569            user.clone(),
1570            distro.clone(),
1571            name.clone(),
1572            container_id.clone(),
1573        ))? {
1574            Ok(RemoteConnectionId(id))
1575        } else {
1576            let id = this.select_row_bound(sql!(
1577                INSERT INTO remote_connections (
1578                    kind,
1579                    host,
1580                    port,
1581                    user,
1582                    distro,
1583                    name,
1584                    container_id,
1585                    use_podman
1586                ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
1587                RETURNING id
1588            ))?((
1589                kind.serialize(),
1590                host,
1591                port,
1592                user,
1593                distro,
1594                name,
1595                container_id,
1596                use_podman,
1597            ))?
1598            .context("failed to insert remote project")?;
1599            Ok(RemoteConnectionId(id))
1600        }
1601    }
1602
1603    query! {
1604        pub async fn next_id() -> Result<WorkspaceId> {
1605            INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
1606        }
1607    }
1608
1609    fn recent_workspaces(
1610        &self,
1611    ) -> Result<
1612        Vec<(
1613            WorkspaceId,
1614            PathList,
1615            Option<RemoteConnectionId>,
1616            DateTime<Utc>,
1617        )>,
1618    > {
1619        Ok(self
1620            .recent_workspaces_query()?
1621            .into_iter()
1622            .map(|(id, paths, order, remote_connection_id, timestamp)| {
1623                (
1624                    id,
1625                    PathList::deserialize(&SerializedPathList { paths, order }),
1626                    remote_connection_id.map(RemoteConnectionId),
1627                    parse_timestamp(&timestamp),
1628                )
1629            })
1630            .collect())
1631    }
1632
1633    query! {
1634        fn recent_workspaces_query() -> Result<Vec<(WorkspaceId, String, String, Option<u64>, String)>> {
1635            SELECT workspace_id, paths, paths_order, remote_connection_id, timestamp
1636            FROM workspaces
1637            WHERE
1638                paths IS NOT NULL OR
1639                remote_connection_id IS NOT NULL
1640            ORDER BY timestamp DESC
1641        }
1642    }
1643
1644    fn session_workspaces(
1645        &self,
1646        session_id: String,
1647    ) -> Result<
1648        Vec<(
1649            WorkspaceId,
1650            PathList,
1651            Option<u64>,
1652            Option<RemoteConnectionId>,
1653        )>,
1654    > {
1655        Ok(self
1656            .session_workspaces_query(session_id)?
1657            .into_iter()
1658            .map(
1659                |(workspace_id, paths, order, window_id, remote_connection_id)| {
1660                    (
1661                        WorkspaceId(workspace_id),
1662                        PathList::deserialize(&SerializedPathList { paths, order }),
1663                        window_id,
1664                        remote_connection_id.map(RemoteConnectionId),
1665                    )
1666                },
1667            )
1668            .collect())
1669    }
1670
1671    query! {
1672        fn session_workspaces_query(session_id: String) -> Result<Vec<(i64, String, String, Option<u64>, Option<u64>)>> {
1673            SELECT workspace_id, paths, paths_order, window_id, remote_connection_id
1674            FROM workspaces
1675            WHERE session_id = ?1
1676            ORDER BY timestamp DESC
1677        }
1678    }
1679
1680    query! {
1681        pub fn breakpoints_for_file(workspace_id: WorkspaceId, file_path: &Path) -> Result<Vec<Breakpoint>> {
1682            SELECT breakpoint_location
1683            FROM breakpoints
1684            WHERE  workspace_id= ?1 AND path = ?2
1685        }
1686    }
1687
1688    query! {
1689        pub fn clear_breakpoints(file_path: &Path) -> Result<()> {
1690            DELETE FROM breakpoints
1691            WHERE file_path = ?2
1692        }
1693    }
1694
1695    fn remote_connections(&self) -> Result<HashMap<RemoteConnectionId, RemoteConnectionOptions>> {
1696        Ok(self.select(sql!(
1697            SELECT
1698                id, kind, host, port, user, distro, container_id, name, use_podman
1699            FROM
1700                remote_connections
1701        ))?()?
1702        .into_iter()
1703        .filter_map(
1704            |(id, kind, host, port, user, distro, container_id, name, use_podman)| {
1705                Some((
1706                    RemoteConnectionId(id),
1707                    Self::remote_connection_from_row(
1708                        kind,
1709                        host,
1710                        port,
1711                        user,
1712                        distro,
1713                        container_id,
1714                        name,
1715                        use_podman,
1716                    )?,
1717                ))
1718            },
1719        )
1720        .collect())
1721    }
1722
1723    pub(crate) fn remote_connection(
1724        &self,
1725        id: RemoteConnectionId,
1726    ) -> Result<RemoteConnectionOptions> {
1727        let (kind, host, port, user, distro, container_id, name, use_podman) =
1728            self.select_row_bound(sql!(
1729                SELECT kind, host, port, user, distro, container_id, name, use_podman
1730                FROM remote_connections
1731                WHERE id = ?
1732            ))?(id.0)?
1733            .context("no such remote connection")?;
1734        Self::remote_connection_from_row(
1735            kind,
1736            host,
1737            port,
1738            user,
1739            distro,
1740            container_id,
1741            name,
1742            use_podman,
1743        )
1744        .context("invalid remote_connection row")
1745    }
1746
1747    fn remote_connection_from_row(
1748        kind: String,
1749        host: Option<String>,
1750        port: Option<u16>,
1751        user: Option<String>,
1752        distro: Option<String>,
1753        container_id: Option<String>,
1754        name: Option<String>,
1755        use_podman: Option<bool>,
1756    ) -> Option<RemoteConnectionOptions> {
1757        match RemoteConnectionKind::deserialize(&kind)? {
1758            RemoteConnectionKind::Wsl => Some(RemoteConnectionOptions::Wsl(WslConnectionOptions {
1759                distro_name: distro?,
1760                user: user,
1761            })),
1762            RemoteConnectionKind::Ssh => Some(RemoteConnectionOptions::Ssh(SshConnectionOptions {
1763                host: host?.into(),
1764                port,
1765                username: user,
1766                ..Default::default()
1767            })),
1768            RemoteConnectionKind::Docker => {
1769                Some(RemoteConnectionOptions::Docker(DockerConnectionOptions {
1770                    container_id: container_id?,
1771                    name: name?,
1772                    remote_user: user?,
1773                    upload_binary_over_docker_exec: false,
1774                    use_podman: use_podman?,
1775                }))
1776            }
1777        }
1778    }
1779
1780    query! {
1781        pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
1782            DELETE FROM workspaces
1783            WHERE workspace_id IS ?
1784        }
1785    }
1786
1787    async fn all_paths_exist_with_a_directory(paths: &[PathBuf], fs: &dyn Fs) -> bool {
1788        let mut any_dir = false;
1789        for path in paths {
1790            match fs.metadata(path).await.ok().flatten() {
1791                None => return false,
1792                Some(meta) => {
1793                    if meta.is_dir {
1794                        any_dir = true;
1795                    }
1796                }
1797            }
1798        }
1799        any_dir
1800    }
1801
1802    // Returns the recent locations which are still valid on disk and deletes ones which no longer
1803    // exist.
1804    pub async fn recent_workspaces_on_disk(
1805        &self,
1806        fs: &dyn Fs,
1807    ) -> Result<
1808        Vec<(
1809            WorkspaceId,
1810            SerializedWorkspaceLocation,
1811            PathList,
1812            DateTime<Utc>,
1813        )>,
1814    > {
1815        let mut result = Vec::new();
1816        let mut delete_tasks = Vec::new();
1817        let remote_connections = self.remote_connections()?;
1818
1819        for (id, paths, remote_connection_id, timestamp) in self.recent_workspaces()? {
1820            if let Some(remote_connection_id) = remote_connection_id {
1821                if let Some(connection_options) = remote_connections.get(&remote_connection_id) {
1822                    result.push((
1823                        id,
1824                        SerializedWorkspaceLocation::Remote(connection_options.clone()),
1825                        paths,
1826                        timestamp,
1827                    ));
1828                } else {
1829                    delete_tasks.push(self.delete_workspace_by_id(id));
1830                }
1831                continue;
1832            }
1833
1834            let has_wsl_path = if cfg!(windows) {
1835                paths
1836                    .paths()
1837                    .iter()
1838                    .any(|path| util::paths::WslPath::from_path(path).is_some())
1839            } else {
1840                false
1841            };
1842
1843            // Delete the workspace if any of the paths are WSL paths.
1844            // If a local workspace points to WSL, this check will cause us to wait for the
1845            // WSL VM and file server to boot up. This can block for many seconds.
1846            // Supported scenarios use remote workspaces.
1847            if !has_wsl_path && Self::all_paths_exist_with_a_directory(paths.paths(), fs).await {
1848                result.push((id, SerializedWorkspaceLocation::Local, paths, timestamp));
1849            } else {
1850                delete_tasks.push(self.delete_workspace_by_id(id));
1851            }
1852        }
1853
1854        futures::future::join_all(delete_tasks).await;
1855        Ok(result)
1856    }
1857
1858    pub async fn last_workspace(
1859        &self,
1860        fs: &dyn Fs,
1861    ) -> Result<
1862        Option<(
1863            WorkspaceId,
1864            SerializedWorkspaceLocation,
1865            PathList,
1866            DateTime<Utc>,
1867        )>,
1868    > {
1869        Ok(self.recent_workspaces_on_disk(fs).await?.into_iter().next())
1870    }
1871
1872    // Returns the locations of the workspaces that were still opened when the last
1873    // session was closed (i.e. when Zed was quit).
1874    // If `last_session_window_order` is provided, the returned locations are ordered
1875    // according to that.
1876    pub async fn last_session_workspace_locations(
1877        &self,
1878        last_session_id: &str,
1879        last_session_window_stack: Option<Vec<WindowId>>,
1880        fs: &dyn Fs,
1881    ) -> Result<Vec<SessionWorkspace>> {
1882        let mut workspaces = Vec::new();
1883
1884        for (workspace_id, paths, window_id, remote_connection_id) in
1885            self.session_workspaces(last_session_id.to_owned())?
1886        {
1887            let window_id = window_id.map(WindowId::from);
1888
1889            if let Some(remote_connection_id) = remote_connection_id {
1890                workspaces.push(SessionWorkspace {
1891                    workspace_id,
1892                    location: SerializedWorkspaceLocation::Remote(
1893                        self.remote_connection(remote_connection_id)?,
1894                    ),
1895                    paths,
1896                    window_id,
1897                });
1898            } else if paths.is_empty() {
1899                // Empty workspace with items (drafts, files) - include for restoration
1900                workspaces.push(SessionWorkspace {
1901                    workspace_id,
1902                    location: SerializedWorkspaceLocation::Local,
1903                    paths,
1904                    window_id,
1905                });
1906            } else {
1907                if Self::all_paths_exist_with_a_directory(paths.paths(), fs).await {
1908                    workspaces.push(SessionWorkspace {
1909                        workspace_id,
1910                        location: SerializedWorkspaceLocation::Local,
1911                        paths,
1912                        window_id,
1913                    });
1914                }
1915            }
1916        }
1917
1918        if let Some(stack) = last_session_window_stack {
1919            workspaces.sort_by_key(|workspace| {
1920                workspace
1921                    .window_id
1922                    .and_then(|id| stack.iter().position(|&order_id| order_id == id))
1923                    .unwrap_or(usize::MAX)
1924            });
1925        }
1926
1927        Ok(workspaces)
1928    }
1929
1930    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1931        Ok(self
1932            .get_pane_group(workspace_id, None)?
1933            .into_iter()
1934            .next()
1935            .unwrap_or_else(|| {
1936                SerializedPaneGroup::Pane(SerializedPane {
1937                    active: true,
1938                    children: vec![],
1939                    pinned_count: 0,
1940                })
1941            }))
1942    }
1943
1944    fn get_pane_group(
1945        &self,
1946        workspace_id: WorkspaceId,
1947        group_id: Option<GroupId>,
1948    ) -> Result<Vec<SerializedPaneGroup>> {
1949        type GroupKey = (Option<GroupId>, WorkspaceId);
1950        type GroupOrPane = (
1951            Option<GroupId>,
1952            Option<SerializedAxis>,
1953            Option<PaneId>,
1954            Option<bool>,
1955            Option<usize>,
1956            Option<String>,
1957        );
1958        self.select_bound::<GroupKey, GroupOrPane>(sql!(
1959            SELECT group_id, axis, pane_id, active, pinned_count, flexes
1960                FROM (SELECT
1961                        group_id,
1962                        axis,
1963                        NULL as pane_id,
1964                        NULL as active,
1965                        NULL as pinned_count,
1966                        position,
1967                        parent_group_id,
1968                        workspace_id,
1969                        flexes
1970                      FROM pane_groups
1971                    UNION
1972                      SELECT
1973                        NULL,
1974                        NULL,
1975                        center_panes.pane_id,
1976                        panes.active as active,
1977                        pinned_count,
1978                        position,
1979                        parent_group_id,
1980                        panes.workspace_id as workspace_id,
1981                        NULL
1982                      FROM center_panes
1983                      JOIN panes ON center_panes.pane_id = panes.pane_id)
1984                WHERE parent_group_id IS ? AND workspace_id = ?
1985                ORDER BY position
1986        ))?((group_id, workspace_id))?
1987        .into_iter()
1988        .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1989            let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1990            if let Some((group_id, axis)) = group_id.zip(axis) {
1991                let flexes = flexes
1992                    .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1993                    .transpose()?;
1994
1995                Ok(SerializedPaneGroup::Group {
1996                    axis,
1997                    children: self.get_pane_group(workspace_id, Some(group_id))?,
1998                    flexes,
1999                })
2000            } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
2001                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
2002                    self.get_items(pane_id)?,
2003                    active,
2004                    pinned_count,
2005                )))
2006            } else {
2007                bail!("Pane Group Child was neither a pane group or a pane");
2008            }
2009        })
2010        // Filter out panes and pane groups which don't have any children or items
2011        .filter(|pane_group| match pane_group {
2012            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
2013            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
2014            _ => true,
2015        })
2016        .collect::<Result<_>>()
2017    }
2018
2019    fn save_pane_group(
2020        conn: &Connection,
2021        workspace_id: WorkspaceId,
2022        pane_group: &SerializedPaneGroup,
2023        parent: Option<(GroupId, usize)>,
2024    ) -> Result<()> {
2025        if parent.is_none() {
2026            log::debug!("Saving a pane group for workspace {workspace_id:?}");
2027        }
2028        match pane_group {
2029            SerializedPaneGroup::Group {
2030                axis,
2031                children,
2032                flexes,
2033            } => {
2034                let (parent_id, position) = parent.unzip();
2035
2036                let flex_string = flexes
2037                    .as_ref()
2038                    .map(|flexes| serde_json::json!(flexes).to_string());
2039
2040                let group_id = conn.select_row_bound::<_, i64>(sql!(
2041                    INSERT INTO pane_groups(
2042                        workspace_id,
2043                        parent_group_id,
2044                        position,
2045                        axis,
2046                        flexes
2047                    )
2048                    VALUES (?, ?, ?, ?, ?)
2049                    RETURNING group_id
2050                ))?((
2051                    workspace_id,
2052                    parent_id,
2053                    position,
2054                    *axis,
2055                    flex_string,
2056                ))?
2057                .context("Couldn't retrieve group_id from inserted pane_group")?;
2058
2059                for (position, group) in children.iter().enumerate() {
2060                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
2061                }
2062
2063                Ok(())
2064            }
2065            SerializedPaneGroup::Pane(pane) => {
2066                Self::save_pane(conn, workspace_id, pane, parent)?;
2067                Ok(())
2068            }
2069        }
2070    }
2071
2072    fn save_pane(
2073        conn: &Connection,
2074        workspace_id: WorkspaceId,
2075        pane: &SerializedPane,
2076        parent: Option<(GroupId, usize)>,
2077    ) -> Result<PaneId> {
2078        let pane_id = conn.select_row_bound::<_, i64>(sql!(
2079            INSERT INTO panes(workspace_id, active, pinned_count)
2080            VALUES (?, ?, ?)
2081            RETURNING pane_id
2082        ))?((workspace_id, pane.active, pane.pinned_count))?
2083        .context("Could not retrieve inserted pane_id")?;
2084
2085        let (parent_id, order) = parent.unzip();
2086        conn.exec_bound(sql!(
2087            INSERT INTO center_panes(pane_id, parent_group_id, position)
2088            VALUES (?, ?, ?)
2089        ))?((pane_id, parent_id, order))?;
2090
2091        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
2092
2093        Ok(pane_id)
2094    }
2095
2096    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
2097        self.select_bound(sql!(
2098            SELECT kind, item_id, active, preview FROM items
2099            WHERE pane_id = ?
2100                ORDER BY position
2101        ))?(pane_id)
2102    }
2103
2104    fn save_items(
2105        conn: &Connection,
2106        workspace_id: WorkspaceId,
2107        pane_id: PaneId,
2108        items: &[SerializedItem],
2109    ) -> Result<()> {
2110        let mut insert = conn.exec_bound(sql!(
2111            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
2112        )).context("Preparing insertion")?;
2113        for (position, item) in items.iter().enumerate() {
2114            insert((workspace_id, pane_id, position, item))?;
2115        }
2116
2117        Ok(())
2118    }
2119
2120    query! {
2121        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
2122            UPDATE workspaces
2123            SET timestamp = CURRENT_TIMESTAMP
2124            WHERE workspace_id = ?
2125        }
2126    }
2127
2128    query! {
2129        pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
2130            UPDATE workspaces
2131            SET window_state = ?2,
2132                window_x = ?3,
2133                window_y = ?4,
2134                window_width = ?5,
2135                window_height = ?6,
2136                display = ?7
2137            WHERE workspace_id = ?1
2138        }
2139    }
2140
2141    query! {
2142        pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
2143            UPDATE workspaces
2144            SET centered_layout = ?2
2145            WHERE workspace_id = ?1
2146        }
2147    }
2148
2149    query! {
2150        pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option<String>) -> Result<()> {
2151            UPDATE workspaces
2152            SET session_id = ?2
2153            WHERE workspace_id = ?1
2154        }
2155    }
2156
2157    query! {
2158        pub(crate) async fn set_session_binding(workspace_id: WorkspaceId, session_id: Option<String>, window_id: Option<u64>) -> Result<()> {
2159            UPDATE workspaces
2160            SET session_id = ?2, window_id = ?3
2161            WHERE workspace_id = ?1
2162        }
2163    }
2164
2165    pub(crate) async fn toolchains(
2166        &self,
2167        workspace_id: WorkspaceId,
2168    ) -> Result<Vec<(Toolchain, Arc<Path>, Arc<RelPath>)>> {
2169        self.write(move |this| {
2170            let mut select = this
2171                .select_bound(sql!(
2172                    SELECT
2173                        name, path, worktree_root_path, relative_worktree_path, language_name, raw_json
2174                    FROM toolchains
2175                    WHERE workspace_id = ?
2176                ))
2177                .context("select toolchains")?;
2178
2179            let toolchain: Vec<(String, String, String, String, String, String)> =
2180                select(workspace_id)?;
2181
2182            Ok(toolchain
2183                .into_iter()
2184                .filter_map(
2185                    |(name, path, worktree_root_path, relative_worktree_path, language, json)| {
2186                        Some((
2187                            Toolchain {
2188                                name: name.into(),
2189                                path: path.into(),
2190                                language_name: LanguageName::new(&language),
2191                                as_json: serde_json::Value::from_str(&json).ok()?,
2192                            },
2193                           Arc::from(worktree_root_path.as_ref()),
2194                            RelPath::from_proto(&relative_worktree_path).log_err()?,
2195                        ))
2196                    },
2197                )
2198                .collect())
2199        })
2200        .await
2201    }
2202
2203    pub async fn set_toolchain(
2204        &self,
2205        workspace_id: WorkspaceId,
2206        worktree_root_path: Arc<Path>,
2207        relative_worktree_path: Arc<RelPath>,
2208        toolchain: Toolchain,
2209    ) -> Result<()> {
2210        log::debug!(
2211            "Setting toolchain for workspace, worktree: {worktree_root_path:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
2212            toolchain.name
2213        );
2214        self.write(move |conn| {
2215            let mut insert = conn
2216                .exec_bound(sql!(
2217                    INSERT INTO toolchains(workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?,  ?, ?)
2218                    ON CONFLICT DO
2219                    UPDATE SET
2220                        name = ?5,
2221                        path = ?6,
2222                        raw_json = ?7
2223                ))
2224                .context("Preparing insertion")?;
2225
2226            insert((
2227                workspace_id,
2228                worktree_root_path.to_string_lossy().into_owned(),
2229                relative_worktree_path.as_unix_str(),
2230                toolchain.language_name.as_ref(),
2231                toolchain.name.as_ref(),
2232                toolchain.path.as_ref(),
2233                toolchain.as_json.to_string(),
2234            ))?;
2235
2236            Ok(())
2237        }).await
2238    }
2239
2240    pub(crate) async fn save_trusted_worktrees(
2241        &self,
2242        trusted_worktrees: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
2243    ) -> anyhow::Result<()> {
2244        use anyhow::Context as _;
2245        use db::sqlez::statement::Statement;
2246        use itertools::Itertools as _;
2247
2248        DB.clear_trusted_worktrees()
2249            .await
2250            .context("clearing previous trust state")?;
2251
2252        let trusted_worktrees = trusted_worktrees
2253            .into_iter()
2254            .flat_map(|(host, abs_paths)| {
2255                abs_paths
2256                    .into_iter()
2257                    .map(move |abs_path| (Some(abs_path), host.clone()))
2258            })
2259            .collect::<Vec<_>>();
2260        let mut first_worktree;
2261        let mut last_worktree = 0_usize;
2262        for (count, placeholders) in std::iter::once("(?, ?, ?)")
2263            .cycle()
2264            .take(trusted_worktrees.len())
2265            .chunks(MAX_QUERY_PLACEHOLDERS / 3)
2266            .into_iter()
2267            .map(|chunk| {
2268                let mut count = 0;
2269                let placeholders = chunk
2270                    .inspect(|_| {
2271                        count += 1;
2272                    })
2273                    .join(", ");
2274                (count, placeholders)
2275            })
2276            .collect::<Vec<_>>()
2277        {
2278            first_worktree = last_worktree;
2279            last_worktree = last_worktree + count;
2280            let query = format!(
2281                r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name)
2282VALUES {placeholders};"#
2283            );
2284
2285            let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec();
2286            self.write(move |conn| {
2287                let mut statement = Statement::prepare(conn, query)?;
2288                let mut next_index = 1;
2289                for (abs_path, host) in trusted_worktrees {
2290                    let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy());
2291                    next_index = statement.bind(
2292                        &abs_path.as_ref().map(|abs_path| abs_path.as_ref()),
2293                        next_index,
2294                    )?;
2295                    next_index = statement.bind(
2296                        &host
2297                            .as_ref()
2298                            .and_then(|host| Some(host.user_name.as_ref()?.as_str())),
2299                        next_index,
2300                    )?;
2301                    next_index = statement.bind(
2302                        &host.as_ref().map(|host| host.host_identifier.as_str()),
2303                        next_index,
2304                    )?;
2305                }
2306                statement.exec()
2307            })
2308            .await
2309            .context("inserting new trusted state")?;
2310        }
2311        Ok(())
2312    }
2313
2314    pub fn fetch_trusted_worktrees(&self) -> Result<DbTrustedPaths> {
2315        let trusted_worktrees = DB.trusted_worktrees()?;
2316        Ok(trusted_worktrees
2317            .into_iter()
2318            .filter_map(|(abs_path, user_name, host_name)| {
2319                let db_host = match (user_name, host_name) {
2320                    (None, Some(host_name)) => Some(RemoteHostLocation {
2321                        user_name: None,
2322                        host_identifier: SharedString::new(host_name),
2323                    }),
2324                    (Some(user_name), Some(host_name)) => Some(RemoteHostLocation {
2325                        user_name: Some(SharedString::new(user_name)),
2326                        host_identifier: SharedString::new(host_name),
2327                    }),
2328                    _ => None,
2329                };
2330                Some((db_host, abs_path?))
2331            })
2332            .fold(HashMap::default(), |mut acc, (remote_host, abs_path)| {
2333                acc.entry(remote_host)
2334                    .or_insert_with(HashSet::default)
2335                    .insert(abs_path);
2336                acc
2337            }))
2338    }
2339
2340    query! {
2341        fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
2342            SELECT absolute_path, user_name, host_name
2343            FROM trusted_worktrees
2344        }
2345    }
2346
2347    query! {
2348        pub async fn clear_trusted_worktrees() -> Result<()> {
2349            DELETE FROM trusted_worktrees
2350        }
2351    }
2352}
2353
2354pub fn delete_unloaded_items(
2355    alive_items: Vec<ItemId>,
2356    workspace_id: WorkspaceId,
2357    table: &'static str,
2358    db: &ThreadSafeConnection,
2359    cx: &mut App,
2360) -> Task<Result<()>> {
2361    let db = db.clone();
2362    cx.spawn(async move |_| {
2363        let placeholders = alive_items
2364            .iter()
2365            .map(|_| "?")
2366            .collect::<Vec<&str>>()
2367            .join(", ");
2368
2369        let query = format!(
2370            "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
2371        );
2372
2373        db.write(move |conn| {
2374            let mut statement = Statement::prepare(conn, query)?;
2375            let mut next_index = statement.bind(&workspace_id, 1)?;
2376            for id in alive_items {
2377                next_index = statement.bind(&id, next_index)?;
2378            }
2379            statement.exec()
2380        })
2381        .await
2382    })
2383}
2384
2385#[cfg(test)]
2386mod tests {
2387    use super::*;
2388    use crate::persistence::model::{
2389        SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, SessionWorkspace,
2390    };
2391    use gpui;
2392    use pretty_assertions::assert_eq;
2393    use remote::SshConnectionOptions;
2394    use serde_json::json;
2395    use std::{thread, time::Duration};
2396
2397    #[gpui::test]
2398    async fn test_multi_workspace_serializes_on_add_and_remove(cx: &mut gpui::TestAppContext) {
2399        use crate::multi_workspace::MultiWorkspace;
2400        use crate::persistence::read_multi_workspace_state;
2401        use feature_flags::FeatureFlagAppExt;
2402        use gpui::AppContext as _;
2403        use project::Project;
2404
2405        crate::tests::init_test(cx);
2406
2407        cx.update(|cx| {
2408            cx.set_staff(true);
2409            cx.update_flags(true, vec!["agent-v2".to_string()]);
2410        });
2411
2412        let fs = fs::FakeFs::new(cx.executor());
2413        let project1 = Project::test(fs.clone(), [], cx).await;
2414        let project2 = Project::test(fs.clone(), [], cx).await;
2415
2416        let (multi_workspace, cx) =
2417            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
2418
2419        multi_workspace.update_in(cx, |mw, _, cx| {
2420            mw.set_random_database_id(cx);
2421        });
2422
2423        let window_id =
2424            multi_workspace.update_in(cx, |_, window, _cx| window.window_handle().window_id());
2425
2426        // --- Add a second workspace ---
2427        let workspace2 = multi_workspace.update_in(cx, |mw, window, cx| {
2428            let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx));
2429            workspace.update(cx, |ws, _cx| ws.set_random_database_id());
2430            mw.activate(workspace.clone(), cx);
2431            workspace
2432        });
2433
2434        // Run background tasks so serialize has a chance to flush.
2435        cx.run_until_parked();
2436
2437        // Read back the persisted state and check that the active workspace ID was written.
2438        let state_after_add = read_multi_workspace_state(window_id);
2439        let active_workspace2_db_id = workspace2.read_with(cx, |ws, _| ws.database_id());
2440        assert_eq!(
2441            state_after_add.active_workspace_id, active_workspace2_db_id,
2442            "After adding a second workspace, the serialized active_workspace_id should match \
2443             the newly activated workspace's database id"
2444        );
2445
2446        // --- Remove the second workspace (index 1) ---
2447        multi_workspace.update_in(cx, |mw, window, cx| {
2448            mw.remove_workspace(1, window, cx);
2449        });
2450
2451        cx.run_until_parked();
2452
2453        let state_after_remove = read_multi_workspace_state(window_id);
2454        let remaining_db_id =
2455            multi_workspace.read_with(cx, |mw, cx| mw.workspace().read(cx).database_id());
2456        assert_eq!(
2457            state_after_remove.active_workspace_id, remaining_db_id,
2458            "After removing a workspace, the serialized active_workspace_id should match \
2459             the remaining active workspace's database id"
2460        );
2461    }
2462
2463    #[gpui::test]
2464    async fn test_breakpoints() {
2465        zlog::init_test();
2466
2467        let db = WorkspaceDb::open_test_db("test_breakpoints").await;
2468        let id = db.next_id().await.unwrap();
2469
2470        let path = Path::new("/tmp/test.rs");
2471
2472        let breakpoint = Breakpoint {
2473            position: 123,
2474            message: None,
2475            state: BreakpointState::Enabled,
2476            condition: None,
2477            hit_condition: None,
2478        };
2479
2480        let log_breakpoint = Breakpoint {
2481            position: 456,
2482            message: Some("Test log message".into()),
2483            state: BreakpointState::Enabled,
2484            condition: None,
2485            hit_condition: None,
2486        };
2487
2488        let disable_breakpoint = Breakpoint {
2489            position: 578,
2490            message: None,
2491            state: BreakpointState::Disabled,
2492            condition: None,
2493            hit_condition: None,
2494        };
2495
2496        let condition_breakpoint = Breakpoint {
2497            position: 789,
2498            message: None,
2499            state: BreakpointState::Enabled,
2500            condition: Some("x > 5".into()),
2501            hit_condition: None,
2502        };
2503
2504        let hit_condition_breakpoint = Breakpoint {
2505            position: 999,
2506            message: None,
2507            state: BreakpointState::Enabled,
2508            condition: None,
2509            hit_condition: Some(">= 3".into()),
2510        };
2511
2512        let workspace = SerializedWorkspace {
2513            id,
2514            paths: PathList::new(&["/tmp"]),
2515            location: SerializedWorkspaceLocation::Local,
2516            center_group: Default::default(),
2517            window_bounds: Default::default(),
2518            display: Default::default(),
2519            docks: Default::default(),
2520            centered_layout: false,
2521            breakpoints: {
2522                let mut map = collections::BTreeMap::default();
2523                map.insert(
2524                    Arc::from(path),
2525                    vec![
2526                        SourceBreakpoint {
2527                            row: breakpoint.position,
2528                            path: Arc::from(path),
2529                            message: breakpoint.message.clone(),
2530                            state: breakpoint.state,
2531                            condition: breakpoint.condition.clone(),
2532                            hit_condition: breakpoint.hit_condition.clone(),
2533                        },
2534                        SourceBreakpoint {
2535                            row: log_breakpoint.position,
2536                            path: Arc::from(path),
2537                            message: log_breakpoint.message.clone(),
2538                            state: log_breakpoint.state,
2539                            condition: log_breakpoint.condition.clone(),
2540                            hit_condition: log_breakpoint.hit_condition.clone(),
2541                        },
2542                        SourceBreakpoint {
2543                            row: disable_breakpoint.position,
2544                            path: Arc::from(path),
2545                            message: disable_breakpoint.message.clone(),
2546                            state: disable_breakpoint.state,
2547                            condition: disable_breakpoint.condition.clone(),
2548                            hit_condition: disable_breakpoint.hit_condition.clone(),
2549                        },
2550                        SourceBreakpoint {
2551                            row: condition_breakpoint.position,
2552                            path: Arc::from(path),
2553                            message: condition_breakpoint.message.clone(),
2554                            state: condition_breakpoint.state,
2555                            condition: condition_breakpoint.condition.clone(),
2556                            hit_condition: condition_breakpoint.hit_condition.clone(),
2557                        },
2558                        SourceBreakpoint {
2559                            row: hit_condition_breakpoint.position,
2560                            path: Arc::from(path),
2561                            message: hit_condition_breakpoint.message.clone(),
2562                            state: hit_condition_breakpoint.state,
2563                            condition: hit_condition_breakpoint.condition.clone(),
2564                            hit_condition: hit_condition_breakpoint.hit_condition.clone(),
2565                        },
2566                    ],
2567                );
2568                map
2569            },
2570            session_id: None,
2571            window_id: None,
2572            user_toolchains: Default::default(),
2573        };
2574
2575        db.save_workspace(workspace.clone()).await;
2576
2577        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2578        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
2579
2580        assert_eq!(loaded_breakpoints.len(), 5);
2581
2582        // normal breakpoint
2583        assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
2584        assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
2585        assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
2586        assert_eq!(
2587            loaded_breakpoints[0].hit_condition,
2588            breakpoint.hit_condition
2589        );
2590        assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
2591        assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
2592
2593        // enabled breakpoint
2594        assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
2595        assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
2596        assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
2597        assert_eq!(
2598            loaded_breakpoints[1].hit_condition,
2599            log_breakpoint.hit_condition
2600        );
2601        assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
2602        assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
2603
2604        // disable breakpoint
2605        assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
2606        assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
2607        assert_eq!(
2608            loaded_breakpoints[2].condition,
2609            disable_breakpoint.condition
2610        );
2611        assert_eq!(
2612            loaded_breakpoints[2].hit_condition,
2613            disable_breakpoint.hit_condition
2614        );
2615        assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
2616        assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
2617
2618        // condition breakpoint
2619        assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
2620        assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
2621        assert_eq!(
2622            loaded_breakpoints[3].condition,
2623            condition_breakpoint.condition
2624        );
2625        assert_eq!(
2626            loaded_breakpoints[3].hit_condition,
2627            condition_breakpoint.hit_condition
2628        );
2629        assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
2630        assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
2631
2632        // hit condition breakpoint
2633        assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
2634        assert_eq!(
2635            loaded_breakpoints[4].message,
2636            hit_condition_breakpoint.message
2637        );
2638        assert_eq!(
2639            loaded_breakpoints[4].condition,
2640            hit_condition_breakpoint.condition
2641        );
2642        assert_eq!(
2643            loaded_breakpoints[4].hit_condition,
2644            hit_condition_breakpoint.hit_condition
2645        );
2646        assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
2647        assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
2648    }
2649
2650    #[gpui::test]
2651    async fn test_remove_last_breakpoint() {
2652        zlog::init_test();
2653
2654        let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
2655        let id = db.next_id().await.unwrap();
2656
2657        let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
2658
2659        let breakpoint_to_remove = Breakpoint {
2660            position: 100,
2661            message: None,
2662            state: BreakpointState::Enabled,
2663            condition: None,
2664            hit_condition: None,
2665        };
2666
2667        let workspace = SerializedWorkspace {
2668            id,
2669            paths: PathList::new(&["/tmp"]),
2670            location: SerializedWorkspaceLocation::Local,
2671            center_group: Default::default(),
2672            window_bounds: Default::default(),
2673            display: Default::default(),
2674            docks: Default::default(),
2675            centered_layout: false,
2676            breakpoints: {
2677                let mut map = collections::BTreeMap::default();
2678                map.insert(
2679                    Arc::from(singular_path),
2680                    vec![SourceBreakpoint {
2681                        row: breakpoint_to_remove.position,
2682                        path: Arc::from(singular_path),
2683                        message: None,
2684                        state: BreakpointState::Enabled,
2685                        condition: None,
2686                        hit_condition: None,
2687                    }],
2688                );
2689                map
2690            },
2691            session_id: None,
2692            window_id: None,
2693            user_toolchains: Default::default(),
2694        };
2695
2696        db.save_workspace(workspace.clone()).await;
2697
2698        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2699        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
2700
2701        assert_eq!(loaded_breakpoints.len(), 1);
2702        assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
2703        assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
2704        assert_eq!(
2705            loaded_breakpoints[0].condition,
2706            breakpoint_to_remove.condition
2707        );
2708        assert_eq!(
2709            loaded_breakpoints[0].hit_condition,
2710            breakpoint_to_remove.hit_condition
2711        );
2712        assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
2713        assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
2714
2715        let workspace_without_breakpoint = SerializedWorkspace {
2716            id,
2717            paths: PathList::new(&["/tmp"]),
2718            location: SerializedWorkspaceLocation::Local,
2719            center_group: Default::default(),
2720            window_bounds: Default::default(),
2721            display: Default::default(),
2722            docks: Default::default(),
2723            centered_layout: false,
2724            breakpoints: collections::BTreeMap::default(),
2725            session_id: None,
2726            window_id: None,
2727            user_toolchains: Default::default(),
2728        };
2729
2730        db.save_workspace(workspace_without_breakpoint.clone())
2731            .await;
2732
2733        let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
2734        let empty_breakpoints = loaded_after_remove
2735            .breakpoints
2736            .get(&Arc::from(singular_path));
2737
2738        assert!(empty_breakpoints.is_none());
2739    }
2740
2741    #[gpui::test]
2742    async fn test_next_id_stability() {
2743        zlog::init_test();
2744
2745        let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
2746
2747        db.write(|conn| {
2748            conn.migrate(
2749                "test_table",
2750                &[sql!(
2751                    CREATE TABLE test_table(
2752                        text TEXT,
2753                        workspace_id INTEGER,
2754                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2755                        ON DELETE CASCADE
2756                    ) STRICT;
2757                )],
2758                &mut |_, _, _| false,
2759            )
2760            .unwrap();
2761        })
2762        .await;
2763
2764        let id = db.next_id().await.unwrap();
2765        // Assert the empty row got inserted
2766        assert_eq!(
2767            Some(id),
2768            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
2769                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
2770            ))
2771            .unwrap()(id)
2772            .unwrap()
2773        );
2774
2775        db.write(move |conn| {
2776            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2777                .unwrap()(("test-text-1", id))
2778            .unwrap()
2779        })
2780        .await;
2781
2782        let test_text_1 = db
2783            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2784            .unwrap()(1)
2785        .unwrap()
2786        .unwrap();
2787        assert_eq!(test_text_1, "test-text-1");
2788    }
2789
2790    #[gpui::test]
2791    async fn test_workspace_id_stability() {
2792        zlog::init_test();
2793
2794        let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
2795
2796        db.write(|conn| {
2797            conn.migrate(
2798                "test_table",
2799                &[sql!(
2800                        CREATE TABLE test_table(
2801                            text TEXT,
2802                            workspace_id INTEGER,
2803                            FOREIGN KEY(workspace_id)
2804                                REFERENCES workspaces(workspace_id)
2805                            ON DELETE CASCADE
2806                        ) STRICT;)],
2807                &mut |_, _, _| false,
2808            )
2809        })
2810        .await
2811        .unwrap();
2812
2813        let mut workspace_1 = SerializedWorkspace {
2814            id: WorkspaceId(1),
2815            paths: PathList::new(&["/tmp", "/tmp2"]),
2816            location: SerializedWorkspaceLocation::Local,
2817            center_group: Default::default(),
2818            window_bounds: Default::default(),
2819            display: Default::default(),
2820            docks: Default::default(),
2821            centered_layout: false,
2822            breakpoints: Default::default(),
2823            session_id: None,
2824            window_id: None,
2825            user_toolchains: Default::default(),
2826        };
2827
2828        let workspace_2 = SerializedWorkspace {
2829            id: WorkspaceId(2),
2830            paths: PathList::new(&["/tmp"]),
2831            location: SerializedWorkspaceLocation::Local,
2832            center_group: Default::default(),
2833            window_bounds: Default::default(),
2834            display: Default::default(),
2835            docks: Default::default(),
2836            centered_layout: false,
2837            breakpoints: Default::default(),
2838            session_id: None,
2839            window_id: None,
2840            user_toolchains: Default::default(),
2841        };
2842
2843        db.save_workspace(workspace_1.clone()).await;
2844
2845        db.write(|conn| {
2846            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2847                .unwrap()(("test-text-1", 1))
2848            .unwrap();
2849        })
2850        .await;
2851
2852        db.save_workspace(workspace_2.clone()).await;
2853
2854        db.write(|conn| {
2855            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2856                .unwrap()(("test-text-2", 2))
2857            .unwrap();
2858        })
2859        .await;
2860
2861        workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
2862        db.save_workspace(workspace_1.clone()).await;
2863        db.save_workspace(workspace_1).await;
2864        db.save_workspace(workspace_2).await;
2865
2866        let test_text_2 = db
2867            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2868            .unwrap()(2)
2869        .unwrap()
2870        .unwrap();
2871        assert_eq!(test_text_2, "test-text-2");
2872
2873        let test_text_1 = db
2874            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2875            .unwrap()(1)
2876        .unwrap()
2877        .unwrap();
2878        assert_eq!(test_text_1, "test-text-1");
2879    }
2880
2881    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
2882        SerializedPaneGroup::Group {
2883            axis: SerializedAxis(axis),
2884            flexes: None,
2885            children,
2886        }
2887    }
2888
2889    #[gpui::test]
2890    async fn test_full_workspace_serialization() {
2891        zlog::init_test();
2892
2893        let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
2894
2895        //  -----------------
2896        //  | 1,2   | 5,6   |
2897        //  | - - - |       |
2898        //  | 3,4   |       |
2899        //  -----------------
2900        let center_group = group(
2901            Axis::Horizontal,
2902            vec![
2903                group(
2904                    Axis::Vertical,
2905                    vec![
2906                        SerializedPaneGroup::Pane(SerializedPane::new(
2907                            vec![
2908                                SerializedItem::new("Terminal", 5, false, false),
2909                                SerializedItem::new("Terminal", 6, true, false),
2910                            ],
2911                            false,
2912                            0,
2913                        )),
2914                        SerializedPaneGroup::Pane(SerializedPane::new(
2915                            vec![
2916                                SerializedItem::new("Terminal", 7, true, false),
2917                                SerializedItem::new("Terminal", 8, false, false),
2918                            ],
2919                            false,
2920                            0,
2921                        )),
2922                    ],
2923                ),
2924                SerializedPaneGroup::Pane(SerializedPane::new(
2925                    vec![
2926                        SerializedItem::new("Terminal", 9, false, false),
2927                        SerializedItem::new("Terminal", 10, true, false),
2928                    ],
2929                    false,
2930                    0,
2931                )),
2932            ],
2933        );
2934
2935        let workspace = SerializedWorkspace {
2936            id: WorkspaceId(5),
2937            paths: PathList::new(&["/tmp", "/tmp2"]),
2938            location: SerializedWorkspaceLocation::Local,
2939            center_group,
2940            window_bounds: Default::default(),
2941            breakpoints: Default::default(),
2942            display: Default::default(),
2943            docks: Default::default(),
2944            centered_layout: false,
2945            session_id: None,
2946            window_id: Some(999),
2947            user_toolchains: Default::default(),
2948        };
2949
2950        db.save_workspace(workspace.clone()).await;
2951
2952        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2953        assert_eq!(workspace, round_trip_workspace.unwrap());
2954
2955        // Test guaranteed duplicate IDs
2956        db.save_workspace(workspace.clone()).await;
2957        db.save_workspace(workspace.clone()).await;
2958
2959        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2960        assert_eq!(workspace, round_trip_workspace.unwrap());
2961    }
2962
2963    #[gpui::test]
2964    async fn test_workspace_assignment() {
2965        zlog::init_test();
2966
2967        let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2968
2969        let workspace_1 = SerializedWorkspace {
2970            id: WorkspaceId(1),
2971            paths: PathList::new(&["/tmp", "/tmp2"]),
2972            location: SerializedWorkspaceLocation::Local,
2973            center_group: Default::default(),
2974            window_bounds: Default::default(),
2975            breakpoints: Default::default(),
2976            display: Default::default(),
2977            docks: Default::default(),
2978            centered_layout: false,
2979            session_id: None,
2980            window_id: Some(1),
2981            user_toolchains: Default::default(),
2982        };
2983
2984        let mut workspace_2 = SerializedWorkspace {
2985            id: WorkspaceId(2),
2986            paths: PathList::new(&["/tmp"]),
2987            location: SerializedWorkspaceLocation::Local,
2988            center_group: Default::default(),
2989            window_bounds: Default::default(),
2990            display: Default::default(),
2991            docks: Default::default(),
2992            centered_layout: false,
2993            breakpoints: Default::default(),
2994            session_id: None,
2995            window_id: Some(2),
2996            user_toolchains: Default::default(),
2997        };
2998
2999        db.save_workspace(workspace_1.clone()).await;
3000        db.save_workspace(workspace_2.clone()).await;
3001
3002        // Test that paths are treated as a set
3003        assert_eq!(
3004            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
3005            workspace_1
3006        );
3007        assert_eq!(
3008            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
3009            workspace_1
3010        );
3011
3012        // Make sure that other keys work
3013        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
3014        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
3015
3016        // Test 'mutate' case of updating a pre-existing id
3017        workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
3018
3019        db.save_workspace(workspace_2.clone()).await;
3020        assert_eq!(
3021            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
3022            workspace_2
3023        );
3024
3025        // Test other mechanism for mutating
3026        let mut workspace_3 = SerializedWorkspace {
3027            id: WorkspaceId(3),
3028            paths: PathList::new(&["/tmp2", "/tmp"]),
3029            location: SerializedWorkspaceLocation::Local,
3030            center_group: Default::default(),
3031            window_bounds: Default::default(),
3032            breakpoints: Default::default(),
3033            display: Default::default(),
3034            docks: Default::default(),
3035            centered_layout: false,
3036            session_id: None,
3037            window_id: Some(3),
3038            user_toolchains: Default::default(),
3039        };
3040
3041        db.save_workspace(workspace_3.clone()).await;
3042        assert_eq!(
3043            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
3044            workspace_3
3045        );
3046
3047        // Make sure that updating paths differently also works
3048        workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
3049        db.save_workspace(workspace_3.clone()).await;
3050        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
3051        assert_eq!(
3052            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
3053                .unwrap(),
3054            workspace_3
3055        );
3056    }
3057
3058    #[gpui::test]
3059    async fn test_session_workspaces() {
3060        zlog::init_test();
3061
3062        let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
3063
3064        let workspace_1 = SerializedWorkspace {
3065            id: WorkspaceId(1),
3066            paths: PathList::new(&["/tmp1"]),
3067            location: SerializedWorkspaceLocation::Local,
3068            center_group: Default::default(),
3069            window_bounds: Default::default(),
3070            display: Default::default(),
3071            docks: Default::default(),
3072            centered_layout: false,
3073            breakpoints: Default::default(),
3074            session_id: Some("session-id-1".to_owned()),
3075            window_id: Some(10),
3076            user_toolchains: Default::default(),
3077        };
3078
3079        let workspace_2 = SerializedWorkspace {
3080            id: WorkspaceId(2),
3081            paths: PathList::new(&["/tmp2"]),
3082            location: SerializedWorkspaceLocation::Local,
3083            center_group: Default::default(),
3084            window_bounds: Default::default(),
3085            display: Default::default(),
3086            docks: Default::default(),
3087            centered_layout: false,
3088            breakpoints: Default::default(),
3089            session_id: Some("session-id-1".to_owned()),
3090            window_id: Some(20),
3091            user_toolchains: Default::default(),
3092        };
3093
3094        let workspace_3 = SerializedWorkspace {
3095            id: WorkspaceId(3),
3096            paths: PathList::new(&["/tmp3"]),
3097            location: SerializedWorkspaceLocation::Local,
3098            center_group: Default::default(),
3099            window_bounds: Default::default(),
3100            display: Default::default(),
3101            docks: Default::default(),
3102            centered_layout: false,
3103            breakpoints: Default::default(),
3104            session_id: Some("session-id-2".to_owned()),
3105            window_id: Some(30),
3106            user_toolchains: Default::default(),
3107        };
3108
3109        let workspace_4 = SerializedWorkspace {
3110            id: WorkspaceId(4),
3111            paths: PathList::new(&["/tmp4"]),
3112            location: SerializedWorkspaceLocation::Local,
3113            center_group: Default::default(),
3114            window_bounds: Default::default(),
3115            display: Default::default(),
3116            docks: Default::default(),
3117            centered_layout: false,
3118            breakpoints: Default::default(),
3119            session_id: None,
3120            window_id: None,
3121            user_toolchains: Default::default(),
3122        };
3123
3124        let connection_id = db
3125            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3126                host: "my-host".into(),
3127                port: Some(1234),
3128                ..Default::default()
3129            }))
3130            .await
3131            .unwrap();
3132
3133        let workspace_5 = SerializedWorkspace {
3134            id: WorkspaceId(5),
3135            paths: PathList::default(),
3136            location: SerializedWorkspaceLocation::Remote(
3137                db.remote_connection(connection_id).unwrap(),
3138            ),
3139            center_group: Default::default(),
3140            window_bounds: Default::default(),
3141            display: Default::default(),
3142            docks: Default::default(),
3143            centered_layout: false,
3144            breakpoints: Default::default(),
3145            session_id: Some("session-id-2".to_owned()),
3146            window_id: Some(50),
3147            user_toolchains: Default::default(),
3148        };
3149
3150        let workspace_6 = SerializedWorkspace {
3151            id: WorkspaceId(6),
3152            paths: PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
3153            location: SerializedWorkspaceLocation::Local,
3154            center_group: Default::default(),
3155            window_bounds: Default::default(),
3156            breakpoints: Default::default(),
3157            display: Default::default(),
3158            docks: Default::default(),
3159            centered_layout: false,
3160            session_id: Some("session-id-3".to_owned()),
3161            window_id: Some(60),
3162            user_toolchains: Default::default(),
3163        };
3164
3165        db.save_workspace(workspace_1.clone()).await;
3166        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
3167        db.save_workspace(workspace_2.clone()).await;
3168        db.save_workspace(workspace_3.clone()).await;
3169        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
3170        db.save_workspace(workspace_4.clone()).await;
3171        db.save_workspace(workspace_5.clone()).await;
3172        db.save_workspace(workspace_6.clone()).await;
3173
3174        let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
3175        assert_eq!(locations.len(), 2);
3176        assert_eq!(locations[0].0, WorkspaceId(2));
3177        assert_eq!(locations[0].1, PathList::new(&["/tmp2"]));
3178        assert_eq!(locations[0].2, Some(20));
3179        assert_eq!(locations[1].0, WorkspaceId(1));
3180        assert_eq!(locations[1].1, PathList::new(&["/tmp1"]));
3181        assert_eq!(locations[1].2, Some(10));
3182
3183        let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
3184        assert_eq!(locations.len(), 2);
3185        assert_eq!(locations[0].0, WorkspaceId(5));
3186        assert_eq!(locations[0].1, PathList::default());
3187        assert_eq!(locations[0].2, Some(50));
3188        assert_eq!(locations[0].3, Some(connection_id));
3189        assert_eq!(locations[1].0, WorkspaceId(3));
3190        assert_eq!(locations[1].1, PathList::new(&["/tmp3"]));
3191        assert_eq!(locations[1].2, Some(30));
3192
3193        let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
3194        assert_eq!(locations.len(), 1);
3195        assert_eq!(locations[0].0, WorkspaceId(6));
3196        assert_eq!(
3197            locations[0].1,
3198            PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
3199        );
3200        assert_eq!(locations[0].2, Some(60));
3201    }
3202
3203    fn default_workspace<P: AsRef<Path>>(
3204        paths: &[P],
3205        center_group: &SerializedPaneGroup,
3206    ) -> SerializedWorkspace {
3207        SerializedWorkspace {
3208            id: WorkspaceId(4),
3209            paths: PathList::new(paths),
3210            location: SerializedWorkspaceLocation::Local,
3211            center_group: center_group.clone(),
3212            window_bounds: Default::default(),
3213            display: Default::default(),
3214            docks: Default::default(),
3215            breakpoints: Default::default(),
3216            centered_layout: false,
3217            session_id: None,
3218            window_id: None,
3219            user_toolchains: Default::default(),
3220        }
3221    }
3222
3223    #[gpui::test]
3224    async fn test_last_session_workspace_locations(cx: &mut gpui::TestAppContext) {
3225        let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
3226        let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
3227        let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
3228        let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
3229
3230        let fs = fs::FakeFs::new(cx.executor());
3231        fs.insert_tree(dir1.path(), json!({})).await;
3232        fs.insert_tree(dir2.path(), json!({})).await;
3233        fs.insert_tree(dir3.path(), json!({})).await;
3234        fs.insert_tree(dir4.path(), json!({})).await;
3235
3236        let db =
3237            WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
3238
3239        let workspaces = [
3240            (1, vec![dir1.path()], 9),
3241            (2, vec![dir2.path()], 5),
3242            (3, vec![dir3.path()], 8),
3243            (4, vec![dir4.path()], 2),
3244            (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
3245            (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
3246        ]
3247        .into_iter()
3248        .map(|(id, paths, window_id)| SerializedWorkspace {
3249            id: WorkspaceId(id),
3250            paths: PathList::new(paths.as_slice()),
3251            location: SerializedWorkspaceLocation::Local,
3252            center_group: Default::default(),
3253            window_bounds: Default::default(),
3254            display: Default::default(),
3255            docks: Default::default(),
3256            centered_layout: false,
3257            session_id: Some("one-session".to_owned()),
3258            breakpoints: Default::default(),
3259            window_id: Some(window_id),
3260            user_toolchains: Default::default(),
3261        })
3262        .collect::<Vec<_>>();
3263
3264        for workspace in workspaces.iter() {
3265            db.save_workspace(workspace.clone()).await;
3266        }
3267
3268        let stack = Some(Vec::from([
3269            WindowId::from(2), // Top
3270            WindowId::from(8),
3271            WindowId::from(5),
3272            WindowId::from(9),
3273            WindowId::from(3),
3274            WindowId::from(4), // Bottom
3275        ]));
3276
3277        let locations = db
3278            .last_session_workspace_locations("one-session", stack, fs.as_ref())
3279            .await
3280            .unwrap();
3281        assert_eq!(
3282            locations,
3283            [
3284                SessionWorkspace {
3285                    workspace_id: WorkspaceId(4),
3286                    location: SerializedWorkspaceLocation::Local,
3287                    paths: PathList::new(&[dir4.path()]),
3288                    window_id: Some(WindowId::from(2u64)),
3289                },
3290                SessionWorkspace {
3291                    workspace_id: WorkspaceId(3),
3292                    location: SerializedWorkspaceLocation::Local,
3293                    paths: PathList::new(&[dir3.path()]),
3294                    window_id: Some(WindowId::from(8u64)),
3295                },
3296                SessionWorkspace {
3297                    workspace_id: WorkspaceId(2),
3298                    location: SerializedWorkspaceLocation::Local,
3299                    paths: PathList::new(&[dir2.path()]),
3300                    window_id: Some(WindowId::from(5u64)),
3301                },
3302                SessionWorkspace {
3303                    workspace_id: WorkspaceId(1),
3304                    location: SerializedWorkspaceLocation::Local,
3305                    paths: PathList::new(&[dir1.path()]),
3306                    window_id: Some(WindowId::from(9u64)),
3307                },
3308                SessionWorkspace {
3309                    workspace_id: WorkspaceId(5),
3310                    location: SerializedWorkspaceLocation::Local,
3311                    paths: PathList::new(&[dir1.path(), dir2.path(), dir3.path()]),
3312                    window_id: Some(WindowId::from(3u64)),
3313                },
3314                SessionWorkspace {
3315                    workspace_id: WorkspaceId(6),
3316                    location: SerializedWorkspaceLocation::Local,
3317                    paths: PathList::new(&[dir4.path(), dir3.path(), dir2.path()]),
3318                    window_id: Some(WindowId::from(4u64)),
3319                },
3320            ]
3321        );
3322    }
3323
3324    #[gpui::test]
3325    async fn test_last_session_workspace_locations_remote(cx: &mut gpui::TestAppContext) {
3326        let fs = fs::FakeFs::new(cx.executor());
3327        let db =
3328            WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
3329                .await;
3330
3331        let remote_connections = [
3332            ("host-1", "my-user-1"),
3333            ("host-2", "my-user-2"),
3334            ("host-3", "my-user-3"),
3335            ("host-4", "my-user-4"),
3336        ]
3337        .into_iter()
3338        .map(|(host, user)| async {
3339            let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
3340                host: host.into(),
3341                username: Some(user.to_string()),
3342                ..Default::default()
3343            });
3344            db.get_or_create_remote_connection(options.clone())
3345                .await
3346                .unwrap();
3347            options
3348        })
3349        .collect::<Vec<_>>();
3350
3351        let remote_connections = futures::future::join_all(remote_connections).await;
3352
3353        let workspaces = [
3354            (1, remote_connections[0].clone(), 9),
3355            (2, remote_connections[1].clone(), 5),
3356            (3, remote_connections[2].clone(), 8),
3357            (4, remote_connections[3].clone(), 2),
3358        ]
3359        .into_iter()
3360        .map(|(id, remote_connection, window_id)| SerializedWorkspace {
3361            id: WorkspaceId(id),
3362            paths: PathList::default(),
3363            location: SerializedWorkspaceLocation::Remote(remote_connection),
3364            center_group: Default::default(),
3365            window_bounds: Default::default(),
3366            display: Default::default(),
3367            docks: Default::default(),
3368            centered_layout: false,
3369            session_id: Some("one-session".to_owned()),
3370            breakpoints: Default::default(),
3371            window_id: Some(window_id),
3372            user_toolchains: Default::default(),
3373        })
3374        .collect::<Vec<_>>();
3375
3376        for workspace in workspaces.iter() {
3377            db.save_workspace(workspace.clone()).await;
3378        }
3379
3380        let stack = Some(Vec::from([
3381            WindowId::from(2), // Top
3382            WindowId::from(8),
3383            WindowId::from(5),
3384            WindowId::from(9), // Bottom
3385        ]));
3386
3387        let have = db
3388            .last_session_workspace_locations("one-session", stack, fs.as_ref())
3389            .await
3390            .unwrap();
3391        assert_eq!(have.len(), 4);
3392        assert_eq!(
3393            have[0],
3394            SessionWorkspace {
3395                workspace_id: WorkspaceId(4),
3396                location: SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
3397                paths: PathList::default(),
3398                window_id: Some(WindowId::from(2u64)),
3399            }
3400        );
3401        assert_eq!(
3402            have[1],
3403            SessionWorkspace {
3404                workspace_id: WorkspaceId(3),
3405                location: SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
3406                paths: PathList::default(),
3407                window_id: Some(WindowId::from(8u64)),
3408            }
3409        );
3410        assert_eq!(
3411            have[2],
3412            SessionWorkspace {
3413                workspace_id: WorkspaceId(2),
3414                location: SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
3415                paths: PathList::default(),
3416                window_id: Some(WindowId::from(5u64)),
3417            }
3418        );
3419        assert_eq!(
3420            have[3],
3421            SessionWorkspace {
3422                workspace_id: WorkspaceId(1),
3423                location: SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
3424                paths: PathList::default(),
3425                window_id: Some(WindowId::from(9u64)),
3426            }
3427        );
3428    }
3429
3430    #[gpui::test]
3431    async fn test_get_or_create_ssh_project() {
3432        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
3433
3434        let host = "example.com".to_string();
3435        let port = Some(22_u16);
3436        let user = Some("user".to_string());
3437
3438        let connection_id = db
3439            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3440                host: host.clone().into(),
3441                port,
3442                username: user.clone(),
3443                ..Default::default()
3444            }))
3445            .await
3446            .unwrap();
3447
3448        // Test that calling the function again with the same parameters returns the same project
3449        let same_connection = db
3450            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3451                host: host.clone().into(),
3452                port,
3453                username: user.clone(),
3454                ..Default::default()
3455            }))
3456            .await
3457            .unwrap();
3458
3459        assert_eq!(connection_id, same_connection);
3460
3461        // Test with different parameters
3462        let host2 = "otherexample.com".to_string();
3463        let port2 = None;
3464        let user2 = Some("otheruser".to_string());
3465
3466        let different_connection = db
3467            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3468                host: host2.clone().into(),
3469                port: port2,
3470                username: user2.clone(),
3471                ..Default::default()
3472            }))
3473            .await
3474            .unwrap();
3475
3476        assert_ne!(connection_id, different_connection);
3477    }
3478
3479    #[gpui::test]
3480    async fn test_get_or_create_ssh_project_with_null_user() {
3481        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
3482
3483        let (host, port, user) = ("example.com".to_string(), None, None);
3484
3485        let connection_id = db
3486            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3487                host: host.clone().into(),
3488                port,
3489                username: None,
3490                ..Default::default()
3491            }))
3492            .await
3493            .unwrap();
3494
3495        let same_connection_id = db
3496            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3497                host: host.clone().into(),
3498                port,
3499                username: user.clone(),
3500                ..Default::default()
3501            }))
3502            .await
3503            .unwrap();
3504
3505        assert_eq!(connection_id, same_connection_id);
3506    }
3507
3508    #[gpui::test]
3509    async fn test_get_remote_connections() {
3510        let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
3511
3512        let connections = [
3513            ("example.com".to_string(), None, None),
3514            (
3515                "anotherexample.com".to_string(),
3516                Some(123_u16),
3517                Some("user2".to_string()),
3518            ),
3519            ("yetanother.com".to_string(), Some(345_u16), None),
3520        ];
3521
3522        let mut ids = Vec::new();
3523        for (host, port, user) in connections.iter() {
3524            ids.push(
3525                db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
3526                    SshConnectionOptions {
3527                        host: host.clone().into(),
3528                        port: *port,
3529                        username: user.clone(),
3530                        ..Default::default()
3531                    },
3532                ))
3533                .await
3534                .unwrap(),
3535            );
3536        }
3537
3538        let stored_connections = db.remote_connections().unwrap();
3539        assert_eq!(
3540            stored_connections,
3541            [
3542                (
3543                    ids[0],
3544                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3545                        host: "example.com".into(),
3546                        port: None,
3547                        username: None,
3548                        ..Default::default()
3549                    }),
3550                ),
3551                (
3552                    ids[1],
3553                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3554                        host: "anotherexample.com".into(),
3555                        port: Some(123),
3556                        username: Some("user2".into()),
3557                        ..Default::default()
3558                    }),
3559                ),
3560                (
3561                    ids[2],
3562                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3563                        host: "yetanother.com".into(),
3564                        port: Some(345),
3565                        username: None,
3566                        ..Default::default()
3567                    }),
3568                ),
3569            ]
3570            .into_iter()
3571            .collect::<HashMap<_, _>>(),
3572        );
3573    }
3574
3575    #[gpui::test]
3576    async fn test_simple_split() {
3577        zlog::init_test();
3578
3579        let db = WorkspaceDb::open_test_db("simple_split").await;
3580
3581        //  -----------------
3582        //  | 1,2   | 5,6   |
3583        //  | - - - |       |
3584        //  | 3,4   |       |
3585        //  -----------------
3586        let center_pane = group(
3587            Axis::Horizontal,
3588            vec![
3589                group(
3590                    Axis::Vertical,
3591                    vec![
3592                        SerializedPaneGroup::Pane(SerializedPane::new(
3593                            vec![
3594                                SerializedItem::new("Terminal", 1, false, false),
3595                                SerializedItem::new("Terminal", 2, true, false),
3596                            ],
3597                            false,
3598                            0,
3599                        )),
3600                        SerializedPaneGroup::Pane(SerializedPane::new(
3601                            vec![
3602                                SerializedItem::new("Terminal", 4, false, false),
3603                                SerializedItem::new("Terminal", 3, true, false),
3604                            ],
3605                            true,
3606                            0,
3607                        )),
3608                    ],
3609                ),
3610                SerializedPaneGroup::Pane(SerializedPane::new(
3611                    vec![
3612                        SerializedItem::new("Terminal", 5, true, false),
3613                        SerializedItem::new("Terminal", 6, false, false),
3614                    ],
3615                    false,
3616                    0,
3617                )),
3618            ],
3619        );
3620
3621        let workspace = default_workspace(&["/tmp"], &center_pane);
3622
3623        db.save_workspace(workspace.clone()).await;
3624
3625        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
3626
3627        assert_eq!(workspace.center_group, new_workspace.center_group);
3628    }
3629
3630    #[gpui::test]
3631    async fn test_cleanup_panes() {
3632        zlog::init_test();
3633
3634        let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
3635
3636        let center_pane = group(
3637            Axis::Horizontal,
3638            vec![
3639                group(
3640                    Axis::Vertical,
3641                    vec![
3642                        SerializedPaneGroup::Pane(SerializedPane::new(
3643                            vec![
3644                                SerializedItem::new("Terminal", 1, false, false),
3645                                SerializedItem::new("Terminal", 2, true, false),
3646                            ],
3647                            false,
3648                            0,
3649                        )),
3650                        SerializedPaneGroup::Pane(SerializedPane::new(
3651                            vec![
3652                                SerializedItem::new("Terminal", 4, false, false),
3653                                SerializedItem::new("Terminal", 3, true, false),
3654                            ],
3655                            true,
3656                            0,
3657                        )),
3658                    ],
3659                ),
3660                SerializedPaneGroup::Pane(SerializedPane::new(
3661                    vec![
3662                        SerializedItem::new("Terminal", 5, false, false),
3663                        SerializedItem::new("Terminal", 6, true, false),
3664                    ],
3665                    false,
3666                    0,
3667                )),
3668            ],
3669        );
3670
3671        let id = &["/tmp"];
3672
3673        let mut workspace = default_workspace(id, &center_pane);
3674
3675        db.save_workspace(workspace.clone()).await;
3676
3677        workspace.center_group = group(
3678            Axis::Vertical,
3679            vec![
3680                SerializedPaneGroup::Pane(SerializedPane::new(
3681                    vec![
3682                        SerializedItem::new("Terminal", 1, false, false),
3683                        SerializedItem::new("Terminal", 2, true, false),
3684                    ],
3685                    false,
3686                    0,
3687                )),
3688                SerializedPaneGroup::Pane(SerializedPane::new(
3689                    vec![
3690                        SerializedItem::new("Terminal", 4, true, false),
3691                        SerializedItem::new("Terminal", 3, false, false),
3692                    ],
3693                    true,
3694                    0,
3695                )),
3696            ],
3697        );
3698
3699        db.save_workspace(workspace.clone()).await;
3700
3701        let new_workspace = db.workspace_for_roots(id).unwrap();
3702
3703        assert_eq!(workspace.center_group, new_workspace.center_group);
3704    }
3705
3706    #[gpui::test]
3707    async fn test_empty_workspace_window_bounds() {
3708        zlog::init_test();
3709
3710        let db = WorkspaceDb::open_test_db("test_empty_workspace_window_bounds").await;
3711        let id = db.next_id().await.unwrap();
3712
3713        // Create a workspace with empty paths (empty workspace)
3714        let empty_paths: &[&str] = &[];
3715        let display_uuid = Uuid::new_v4();
3716        let window_bounds = SerializedWindowBounds(WindowBounds::Windowed(Bounds {
3717            origin: point(px(100.0), px(200.0)),
3718            size: size(px(800.0), px(600.0)),
3719        }));
3720
3721        let workspace = SerializedWorkspace {
3722            id,
3723            paths: PathList::new(empty_paths),
3724            location: SerializedWorkspaceLocation::Local,
3725            center_group: Default::default(),
3726            window_bounds: None,
3727            display: None,
3728            docks: Default::default(),
3729            breakpoints: Default::default(),
3730            centered_layout: false,
3731            session_id: None,
3732            window_id: None,
3733            user_toolchains: Default::default(),
3734        };
3735
3736        // Save the workspace (this creates the record with empty paths)
3737        db.save_workspace(workspace.clone()).await;
3738
3739        // Save window bounds separately (as the actual code does via set_window_open_status)
3740        db.set_window_open_status(id, window_bounds, display_uuid)
3741            .await
3742            .unwrap();
3743
3744        // Empty workspaces cannot be retrieved by paths (they'd all match).
3745        // They must be retrieved by workspace_id.
3746        assert!(db.workspace_for_roots(empty_paths).is_none());
3747
3748        // Retrieve using workspace_for_id instead
3749        let retrieved = db.workspace_for_id(id).unwrap();
3750
3751        // Verify window bounds were persisted
3752        assert_eq!(retrieved.id, id);
3753        assert!(retrieved.window_bounds.is_some());
3754        assert_eq!(retrieved.window_bounds.unwrap().0, window_bounds.0);
3755        assert!(retrieved.display.is_some());
3756        assert_eq!(retrieved.display.unwrap(), display_uuid);
3757    }
3758
3759    #[gpui::test]
3760    async fn test_last_session_workspace_locations_groups_by_window_id(
3761        cx: &mut gpui::TestAppContext,
3762    ) {
3763        let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
3764        let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
3765        let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
3766        let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
3767        let dir5 = tempfile::TempDir::with_prefix("dir5").unwrap();
3768
3769        let fs = fs::FakeFs::new(cx.executor());
3770        fs.insert_tree(dir1.path(), json!({})).await;
3771        fs.insert_tree(dir2.path(), json!({})).await;
3772        fs.insert_tree(dir3.path(), json!({})).await;
3773        fs.insert_tree(dir4.path(), json!({})).await;
3774        fs.insert_tree(dir5.path(), json!({})).await;
3775
3776        let db =
3777            WorkspaceDb::open_test_db("test_last_session_workspace_locations_groups_by_window_id")
3778                .await;
3779
3780        // Simulate two MultiWorkspace windows each containing two workspaces,
3781        // plus one single-workspace window:
3782        //   Window 10: workspace 1, workspace 2
3783        //   Window 20: workspace 3, workspace 4
3784        //   Window 30: workspace 5 (only one)
3785        //
3786        // On session restore, the caller should be able to group these by
3787        // window_id to reconstruct the MultiWorkspace windows.
3788        let workspaces_data: Vec<(i64, &Path, u64)> = vec![
3789            (1, dir1.path(), 10),
3790            (2, dir2.path(), 10),
3791            (3, dir3.path(), 20),
3792            (4, dir4.path(), 20),
3793            (5, dir5.path(), 30),
3794        ];
3795
3796        for (id, dir, window_id) in &workspaces_data {
3797            db.save_workspace(SerializedWorkspace {
3798                id: WorkspaceId(*id),
3799                paths: PathList::new(&[*dir]),
3800                location: SerializedWorkspaceLocation::Local,
3801                center_group: Default::default(),
3802                window_bounds: Default::default(),
3803                display: Default::default(),
3804                docks: Default::default(),
3805                centered_layout: false,
3806                session_id: Some("test-session".to_owned()),
3807                breakpoints: Default::default(),
3808                window_id: Some(*window_id),
3809                user_toolchains: Default::default(),
3810            })
3811            .await;
3812        }
3813
3814        let locations = db
3815            .last_session_workspace_locations("test-session", None, fs.as_ref())
3816            .await
3817            .unwrap();
3818
3819        // All 5 workspaces should be returned with their window_ids.
3820        assert_eq!(locations.len(), 5);
3821
3822        // Every entry should have a window_id so the caller can group them.
3823        for session_workspace in &locations {
3824            assert!(
3825                session_workspace.window_id.is_some(),
3826                "workspace {:?} missing window_id",
3827                session_workspace.workspace_id
3828            );
3829        }
3830
3831        // Group by window_id, simulating what the restoration code should do.
3832        let mut by_window: HashMap<WindowId, Vec<WorkspaceId>> = HashMap::default();
3833        for session_workspace in &locations {
3834            if let Some(window_id) = session_workspace.window_id {
3835                by_window
3836                    .entry(window_id)
3837                    .or_default()
3838                    .push(session_workspace.workspace_id);
3839            }
3840        }
3841
3842        // Should produce 3 windows, not 5.
3843        assert_eq!(
3844            by_window.len(),
3845            3,
3846            "Expected 3 window groups, got {}: {:?}",
3847            by_window.len(),
3848            by_window
3849        );
3850
3851        // Window 10 should contain workspaces 1 and 2.
3852        let window_10 = by_window.get(&WindowId::from(10u64)).unwrap();
3853        assert_eq!(window_10.len(), 2);
3854        assert!(window_10.contains(&WorkspaceId(1)));
3855        assert!(window_10.contains(&WorkspaceId(2)));
3856
3857        // Window 20 should contain workspaces 3 and 4.
3858        let window_20 = by_window.get(&WindowId::from(20u64)).unwrap();
3859        assert_eq!(window_20.len(), 2);
3860        assert!(window_20.contains(&WorkspaceId(3)));
3861        assert!(window_20.contains(&WorkspaceId(4)));
3862
3863        // Window 30 should contain only workspace 5.
3864        let window_30 = by_window.get(&WindowId::from(30u64)).unwrap();
3865        assert_eq!(window_30.len(), 1);
3866        assert!(window_30.contains(&WorkspaceId(5)));
3867    }
3868
3869    #[gpui::test]
3870    async fn test_read_serialized_multi_workspaces_with_state() {
3871        use crate::persistence::model::MultiWorkspaceState;
3872
3873        // Write multi-workspace state for two windows via the scoped KVP.
3874        let window_10 = WindowId::from(10u64);
3875        let window_20 = WindowId::from(20u64);
3876
3877        write_multi_workspace_state(
3878            window_10,
3879            MultiWorkspaceState {
3880                active_workspace_id: Some(WorkspaceId(2)),
3881            },
3882        )
3883        .await;
3884
3885        write_multi_workspace_state(
3886            window_20,
3887            MultiWorkspaceState {
3888                active_workspace_id: Some(WorkspaceId(3)),
3889            },
3890        )
3891        .await;
3892
3893        // Build session workspaces: two in window 10, one in window 20, one with no window.
3894        let session_workspaces = vec![
3895            SessionWorkspace {
3896                workspace_id: WorkspaceId(1),
3897                location: SerializedWorkspaceLocation::Local,
3898                paths: PathList::new(&["/a"]),
3899                window_id: Some(window_10),
3900            },
3901            SessionWorkspace {
3902                workspace_id: WorkspaceId(2),
3903                location: SerializedWorkspaceLocation::Local,
3904                paths: PathList::new(&["/b"]),
3905                window_id: Some(window_10),
3906            },
3907            SessionWorkspace {
3908                workspace_id: WorkspaceId(3),
3909                location: SerializedWorkspaceLocation::Local,
3910                paths: PathList::new(&["/c"]),
3911                window_id: Some(window_20),
3912            },
3913            SessionWorkspace {
3914                workspace_id: WorkspaceId(4),
3915                location: SerializedWorkspaceLocation::Local,
3916                paths: PathList::new(&["/d"]),
3917                window_id: None,
3918            },
3919        ];
3920
3921        let results = read_serialized_multi_workspaces(session_workspaces);
3922
3923        // Should produce 3 groups: window 10, window 20, and the orphan.
3924        assert_eq!(results.len(), 3);
3925
3926        // Window 10 group: 2 workspaces, active_workspace_id = 2.
3927        let group_10 = &results[0];
3928        assert_eq!(group_10.workspaces.len(), 2);
3929        assert_eq!(group_10.state.active_workspace_id, Some(WorkspaceId(2)));
3930
3931        // Window 20 group: 1 workspace, active_workspace_id = 3.
3932        let group_20 = &results[1];
3933        assert_eq!(group_20.workspaces.len(), 1);
3934        assert_eq!(group_20.state.active_workspace_id, Some(WorkspaceId(3)));
3935
3936        // Orphan group: no window_id, so state is default.
3937        let group_none = &results[2];
3938        assert_eq!(group_none.workspaces.len(), 1);
3939        assert_eq!(group_none.state.active_workspace_id, None);
3940    }
3941
3942    #[gpui::test]
3943    async fn test_flush_serialization_completes_before_quit(cx: &mut gpui::TestAppContext) {
3944        use crate::multi_workspace::MultiWorkspace;
3945        use feature_flags::FeatureFlagAppExt;
3946
3947        use project::Project;
3948
3949        crate::tests::init_test(cx);
3950
3951        cx.update(|cx| {
3952            cx.set_staff(true);
3953            cx.update_flags(true, vec!["agent-v2".to_string()]);
3954        });
3955
3956        let fs = fs::FakeFs::new(cx.executor());
3957        let project = Project::test(fs.clone(), [], cx).await;
3958
3959        let (multi_workspace, cx) =
3960            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3961
3962        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3963
3964        // Assign a database_id so serialization will actually persist.
3965        let workspace_id = DB.next_id().await.unwrap();
3966        workspace.update(cx, |ws, _cx| {
3967            ws.set_database_id(workspace_id);
3968        });
3969
3970        // Mutate some workspace state.
3971        DB.set_centered_layout(workspace_id, true).await.unwrap();
3972
3973        // Call flush_serialization and await the returned task directly
3974        // (without run_until_parked — the point is that awaiting the task
3975        // alone is sufficient).
3976        let task = multi_workspace.update_in(cx, |mw, window, cx| {
3977            mw.workspace()
3978                .update(cx, |ws, cx| ws.flush_serialization(window, cx))
3979        });
3980        task.await;
3981
3982        // Read the workspace back from the DB and verify serialization happened.
3983        let serialized = DB.workspace_for_id(workspace_id);
3984        assert!(
3985            serialized.is_some(),
3986            "flush_serialization should have persisted the workspace to DB"
3987        );
3988    }
3989
3990    #[gpui::test]
3991    async fn test_create_workspace_serializes_active_workspace_id_after_db_id_assigned(
3992        cx: &mut gpui::TestAppContext,
3993    ) {
3994        use crate::multi_workspace::MultiWorkspace;
3995        use crate::persistence::read_multi_workspace_state;
3996        use feature_flags::FeatureFlagAppExt;
3997
3998        use project::Project;
3999
4000        crate::tests::init_test(cx);
4001
4002        cx.update(|cx| {
4003            cx.set_staff(true);
4004            cx.update_flags(true, vec!["agent-v2".to_string()]);
4005        });
4006
4007        let fs = fs::FakeFs::new(cx.executor());
4008        let project = Project::test(fs.clone(), [], cx).await;
4009
4010        let (multi_workspace, cx) =
4011            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4012
4013        // Give the first workspace a database_id.
4014        multi_workspace.update_in(cx, |mw, _, cx| {
4015            mw.set_random_database_id(cx);
4016        });
4017
4018        let window_id =
4019            multi_workspace.update_in(cx, |_, window, _cx| window.window_handle().window_id());
4020
4021        // Create a new workspace via the MultiWorkspace API (triggers next_id()).
4022        multi_workspace.update_in(cx, |mw, window, cx| {
4023            mw.create_workspace(window, cx);
4024        });
4025
4026        // Let the async next_id() and re-serialization tasks complete.
4027        cx.run_until_parked();
4028
4029        // Read back the multi-workspace state.
4030        let state = read_multi_workspace_state(window_id);
4031
4032        // The new workspace should now have a database_id, and the multi-workspace
4033        // state should record it as the active workspace.
4034        let new_workspace_db_id =
4035            multi_workspace.read_with(cx, |mw, cx| mw.workspace().read(cx).database_id());
4036        assert!(
4037            new_workspace_db_id.is_some(),
4038            "New workspace should have a database_id after run_until_parked"
4039        );
4040        assert_eq!(
4041            state.active_workspace_id, new_workspace_db_id,
4042            "Serialized active_workspace_id should match the new workspace's database_id"
4043        );
4044    }
4045
4046    #[gpui::test]
4047    async fn test_create_workspace_individual_serialization(cx: &mut gpui::TestAppContext) {
4048        use crate::multi_workspace::MultiWorkspace;
4049        use feature_flags::FeatureFlagAppExt;
4050
4051        use project::Project;
4052
4053        crate::tests::init_test(cx);
4054
4055        cx.update(|cx| {
4056            cx.set_staff(true);
4057            cx.update_flags(true, vec!["agent-v2".to_string()]);
4058        });
4059
4060        let fs = fs::FakeFs::new(cx.executor());
4061        let project = Project::test(fs.clone(), [], cx).await;
4062
4063        let (multi_workspace, cx) =
4064            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4065
4066        multi_workspace.update_in(cx, |mw, _, cx| {
4067            mw.set_random_database_id(cx);
4068        });
4069
4070        // Create a new workspace.
4071        multi_workspace.update_in(cx, |mw, window, cx| {
4072            mw.create_workspace(window, cx);
4073        });
4074
4075        cx.run_until_parked();
4076
4077        // Get the new workspace's database_id.
4078        let new_db_id =
4079            multi_workspace.read_with(cx, |mw, cx| mw.workspace().read(cx).database_id());
4080        assert!(
4081            new_db_id.is_some(),
4082            "New workspace should have a database_id"
4083        );
4084
4085        let workspace_id = new_db_id.unwrap();
4086
4087        // The workspace should have been serialized to the DB with real data
4088        // (not just the bare DEFAULT VALUES row from next_id).
4089        let serialized = DB.workspace_for_id(workspace_id);
4090        assert!(
4091            serialized.is_some(),
4092            "Newly created workspace should be fully serialized in the DB after database_id assignment"
4093        );
4094    }
4095
4096    #[gpui::test]
4097    async fn test_remove_workspace_deletes_db_row(cx: &mut gpui::TestAppContext) {
4098        use crate::multi_workspace::MultiWorkspace;
4099        use feature_flags::FeatureFlagAppExt;
4100        use gpui::AppContext as _;
4101        use project::Project;
4102
4103        crate::tests::init_test(cx);
4104
4105        cx.update(|cx| {
4106            cx.set_staff(true);
4107            cx.update_flags(true, vec!["agent-v2".to_string()]);
4108        });
4109
4110        let fs = fs::FakeFs::new(cx.executor());
4111        let project1 = Project::test(fs.clone(), [], cx).await;
4112        let project2 = Project::test(fs.clone(), [], cx).await;
4113
4114        let (multi_workspace, cx) =
4115            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
4116
4117        multi_workspace.update_in(cx, |mw, _, cx| {
4118            mw.set_random_database_id(cx);
4119        });
4120
4121        // Get a real DB id for workspace2 so the row actually exists.
4122        let workspace2_db_id = DB.next_id().await.unwrap();
4123
4124        multi_workspace.update_in(cx, |mw, window, cx| {
4125            let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx));
4126            workspace.update(cx, |ws: &mut crate::Workspace, _cx| {
4127                ws.set_database_id(workspace2_db_id)
4128            });
4129            mw.activate(workspace.clone(), cx);
4130        });
4131
4132        // Save a full workspace row to the DB directly.
4133        DB.save_workspace(SerializedWorkspace {
4134            id: workspace2_db_id,
4135            paths: PathList::new(&["/tmp/remove_test"]),
4136            location: SerializedWorkspaceLocation::Local,
4137            center_group: Default::default(),
4138            window_bounds: Default::default(),
4139            display: Default::default(),
4140            docks: Default::default(),
4141            centered_layout: false,
4142            session_id: Some("remove-test-session".to_owned()),
4143            breakpoints: Default::default(),
4144            window_id: Some(99),
4145            user_toolchains: Default::default(),
4146        })
4147        .await;
4148
4149        assert!(
4150            DB.workspace_for_id(workspace2_db_id).is_some(),
4151            "Workspace2 should exist in DB before removal"
4152        );
4153
4154        // Remove workspace at index 1 (the second workspace).
4155        multi_workspace.update_in(cx, |mw, window, cx| {
4156            mw.remove_workspace(1, window, cx);
4157        });
4158
4159        cx.run_until_parked();
4160
4161        // The row should be deleted, not just have session_id cleared.
4162        assert!(
4163            DB.workspace_for_id(workspace2_db_id).is_none(),
4164            "Removed workspace's DB row should be deleted entirely"
4165        );
4166    }
4167
4168    #[gpui::test]
4169    async fn test_remove_workspace_not_restored_as_zombie(cx: &mut gpui::TestAppContext) {
4170        use crate::multi_workspace::MultiWorkspace;
4171        use feature_flags::FeatureFlagAppExt;
4172        use gpui::AppContext as _;
4173        use project::Project;
4174
4175        crate::tests::init_test(cx);
4176
4177        cx.update(|cx| {
4178            cx.set_staff(true);
4179            cx.update_flags(true, vec!["agent-v2".to_string()]);
4180        });
4181
4182        let fs = fs::FakeFs::new(cx.executor());
4183        let dir1 = tempfile::TempDir::with_prefix("zombie_test1").unwrap();
4184        let dir2 = tempfile::TempDir::with_prefix("zombie_test2").unwrap();
4185        fs.insert_tree(dir1.path(), json!({})).await;
4186        fs.insert_tree(dir2.path(), json!({})).await;
4187
4188        let project1 = Project::test(fs.clone(), [], cx).await;
4189        let project2 = Project::test(fs.clone(), [], cx).await;
4190
4191        // Get real DB ids so the rows actually exist.
4192        let ws1_id = DB.next_id().await.unwrap();
4193        let ws2_id = DB.next_id().await.unwrap();
4194
4195        let (multi_workspace, cx) =
4196            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
4197
4198        multi_workspace.update_in(cx, |mw, _, cx| {
4199            mw.workspace().update(cx, |ws, _cx| {
4200                ws.set_database_id(ws1_id);
4201            });
4202        });
4203
4204        multi_workspace.update_in(cx, |mw, window, cx| {
4205            let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx));
4206            workspace.update(cx, |ws: &mut crate::Workspace, _cx| {
4207                ws.set_database_id(ws2_id)
4208            });
4209            mw.activate(workspace.clone(), cx);
4210        });
4211
4212        let session_id = "test-zombie-session";
4213        let window_id_val: u64 = 42;
4214
4215        DB.save_workspace(SerializedWorkspace {
4216            id: ws1_id,
4217            paths: PathList::new(&[dir1.path()]),
4218            location: SerializedWorkspaceLocation::Local,
4219            center_group: Default::default(),
4220            window_bounds: Default::default(),
4221            display: Default::default(),
4222            docks: Default::default(),
4223            centered_layout: false,
4224            session_id: Some(session_id.to_owned()),
4225            breakpoints: Default::default(),
4226            window_id: Some(window_id_val),
4227            user_toolchains: Default::default(),
4228        })
4229        .await;
4230
4231        DB.save_workspace(SerializedWorkspace {
4232            id: ws2_id,
4233            paths: PathList::new(&[dir2.path()]),
4234            location: SerializedWorkspaceLocation::Local,
4235            center_group: Default::default(),
4236            window_bounds: Default::default(),
4237            display: Default::default(),
4238            docks: Default::default(),
4239            centered_layout: false,
4240            session_id: Some(session_id.to_owned()),
4241            breakpoints: Default::default(),
4242            window_id: Some(window_id_val),
4243            user_toolchains: Default::default(),
4244        })
4245        .await;
4246
4247        // Remove workspace2 (index 1).
4248        multi_workspace.update_in(cx, |mw, window, cx| {
4249            mw.remove_workspace(1, window, cx);
4250        });
4251
4252        cx.run_until_parked();
4253
4254        // The removed workspace should NOT appear in session restoration.
4255        let locations = DB
4256            .last_session_workspace_locations(session_id, None, fs.as_ref())
4257            .await
4258            .unwrap();
4259
4260        let restored_ids: Vec<WorkspaceId> = locations.iter().map(|sw| sw.workspace_id).collect();
4261        assert!(
4262            !restored_ids.contains(&ws2_id),
4263            "Removed workspace should not appear in session restoration list. Found: {:?}",
4264            restored_ids
4265        );
4266        assert!(
4267            restored_ids.contains(&ws1_id),
4268            "Remaining workspace should still appear in session restoration list"
4269        );
4270    }
4271
4272    #[gpui::test]
4273    async fn test_pending_removal_tasks_drained_on_flush(cx: &mut gpui::TestAppContext) {
4274        use crate::multi_workspace::MultiWorkspace;
4275        use feature_flags::FeatureFlagAppExt;
4276        use gpui::AppContext as _;
4277        use project::Project;
4278
4279        crate::tests::init_test(cx);
4280
4281        cx.update(|cx| {
4282            cx.set_staff(true);
4283            cx.update_flags(true, vec!["agent-v2".to_string()]);
4284        });
4285
4286        let fs = fs::FakeFs::new(cx.executor());
4287        let project1 = Project::test(fs.clone(), [], cx).await;
4288        let project2 = Project::test(fs.clone(), [], cx).await;
4289
4290        // Get a real DB id for workspace2 so the row actually exists.
4291        let workspace2_db_id = DB.next_id().await.unwrap();
4292
4293        let (multi_workspace, cx) =
4294            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
4295
4296        multi_workspace.update_in(cx, |mw, _, cx| {
4297            mw.set_random_database_id(cx);
4298        });
4299
4300        multi_workspace.update_in(cx, |mw, window, cx| {
4301            let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx));
4302            workspace.update(cx, |ws: &mut crate::Workspace, _cx| {
4303                ws.set_database_id(workspace2_db_id)
4304            });
4305            mw.activate(workspace.clone(), cx);
4306        });
4307
4308        // Save a full workspace row to the DB directly and let it settle.
4309        DB.save_workspace(SerializedWorkspace {
4310            id: workspace2_db_id,
4311            paths: PathList::new(&["/tmp/pending_removal_test"]),
4312            location: SerializedWorkspaceLocation::Local,
4313            center_group: Default::default(),
4314            window_bounds: Default::default(),
4315            display: Default::default(),
4316            docks: Default::default(),
4317            centered_layout: false,
4318            session_id: Some("pending-removal-session".to_owned()),
4319            breakpoints: Default::default(),
4320            window_id: Some(88),
4321            user_toolchains: Default::default(),
4322        })
4323        .await;
4324        cx.run_until_parked();
4325
4326        // Remove workspace2 — this pushes a task to pending_removal_tasks.
4327        multi_workspace.update_in(cx, |mw, window, cx| {
4328            mw.remove_workspace(1, window, cx);
4329        });
4330
4331        // Simulate the quit handler pattern: collect flush tasks + pending
4332        // removal tasks and await them all.
4333        let all_tasks = multi_workspace.update_in(cx, |mw, window, cx| {
4334            let mut tasks: Vec<Task<()>> = mw
4335                .workspaces()
4336                .iter()
4337                .map(|workspace| {
4338                    workspace.update(cx, |workspace, cx| {
4339                        workspace.flush_serialization(window, cx)
4340                    })
4341                })
4342                .collect();
4343            let mut removal_tasks = mw.take_pending_removal_tasks();
4344            // Note: removal_tasks may be empty if the background task already
4345            // completed (take_pending_removal_tasks filters out ready tasks).
4346            tasks.append(&mut removal_tasks);
4347            tasks.push(mw.flush_serialization());
4348            tasks
4349        });
4350        futures::future::join_all(all_tasks).await;
4351
4352        // After awaiting, the DB row should be deleted.
4353        assert!(
4354            DB.workspace_for_id(workspace2_db_id).is_none(),
4355            "Pending removal task should have deleted the workspace row when awaited"
4356        );
4357    }
4358
4359    #[gpui::test]
4360    async fn test_create_workspace_bounds_observer_uses_fresh_id(cx: &mut gpui::TestAppContext) {
4361        use crate::multi_workspace::MultiWorkspace;
4362        use feature_flags::FeatureFlagAppExt;
4363        use project::Project;
4364
4365        crate::tests::init_test(cx);
4366
4367        cx.update(|cx| {
4368            cx.set_staff(true);
4369            cx.update_flags(true, vec!["agent-v2".to_string()]);
4370        });
4371
4372        let fs = fs::FakeFs::new(cx.executor());
4373        let project = Project::test(fs.clone(), [], cx).await;
4374
4375        let (multi_workspace, cx) =
4376            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4377
4378        multi_workspace.update_in(cx, |mw, _, cx| {
4379            mw.set_random_database_id(cx);
4380        });
4381
4382        multi_workspace.update_in(cx, |mw, window, cx| {
4383            mw.create_workspace(window, cx);
4384        });
4385
4386        cx.run_until_parked();
4387
4388        let new_workspace_db_id =
4389            multi_workspace.read_with(cx, |mw, cx| mw.workspace().read(cx).database_id());
4390        assert!(
4391            new_workspace_db_id.is_some(),
4392            "After run_until_parked, the workspace should have a database_id"
4393        );
4394
4395        let workspace_id = new_workspace_db_id.unwrap();
4396
4397        assert!(
4398            DB.workspace_for_id(workspace_id).is_some(),
4399            "The workspace row should exist in the DB"
4400        );
4401
4402        cx.simulate_resize(gpui::size(px(1024.0), px(768.0)));
4403
4404        // Advance the clock past the 100ms debounce timer so the bounds
4405        // observer task fires
4406        cx.executor().advance_clock(Duration::from_millis(200));
4407        cx.run_until_parked();
4408
4409        let serialized = DB
4410            .workspace_for_id(workspace_id)
4411            .expect("workspace row should still exist");
4412        assert!(
4413            serialized.window_bounds.is_some(),
4414            "The bounds observer should write bounds for the workspace's real DB ID, \
4415             even when the workspace was created via create_workspace (where the ID \
4416             is assigned asynchronously after construction)."
4417        );
4418    }
4419
4420    #[gpui::test]
4421    async fn test_flush_serialization_writes_bounds(cx: &mut gpui::TestAppContext) {
4422        use crate::multi_workspace::MultiWorkspace;
4423        use feature_flags::FeatureFlagAppExt;
4424        use project::Project;
4425
4426        crate::tests::init_test(cx);
4427
4428        cx.update(|cx| {
4429            cx.set_staff(true);
4430            cx.update_flags(true, vec!["agent-v2".to_string()]);
4431        });
4432
4433        let fs = fs::FakeFs::new(cx.executor());
4434        let dir = tempfile::TempDir::with_prefix("flush_bounds_test").unwrap();
4435        fs.insert_tree(dir.path(), json!({})).await;
4436
4437        let project = Project::test(fs.clone(), [dir.path()], cx).await;
4438
4439        let (multi_workspace, cx) =
4440            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4441
4442        let workspace_id = DB.next_id().await.unwrap();
4443        multi_workspace.update_in(cx, |mw, _, cx| {
4444            mw.workspace().update(cx, |ws, _cx| {
4445                ws.set_database_id(workspace_id);
4446            });
4447        });
4448
4449        let task = multi_workspace.update_in(cx, |mw, window, cx| {
4450            mw.workspace()
4451                .update(cx, |ws, cx| ws.flush_serialization(window, cx))
4452        });
4453        task.await;
4454
4455        let after = DB
4456            .workspace_for_id(workspace_id)
4457            .expect("workspace row should exist after flush_serialization");
4458        assert!(
4459            !after.paths.is_empty(),
4460            "flush_serialization should have written paths via save_workspace"
4461        );
4462        assert!(
4463            after.window_bounds.is_some(),
4464            "flush_serialization should ensure window bounds are persisted to the DB \
4465             before the process exits."
4466        );
4467    }
4468}