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(
1788        paths: &[PathBuf],
1789        fs: &dyn Fs,
1790        timestamp: Option<DateTime<Utc>>,
1791    ) -> bool {
1792        let mut any_dir = false;
1793        for path in paths {
1794            match fs.metadata(path).await.ok().flatten() {
1795                None => {
1796                    return timestamp.is_some_and(|t| Utc::now() - t < chrono::Duration::days(7));
1797                }
1798                Some(meta) => {
1799                    if meta.is_dir {
1800                        any_dir = true;
1801                    }
1802                }
1803            }
1804        }
1805        any_dir
1806    }
1807
1808    // Returns the recent locations which are still valid on disk and deletes ones which no longer
1809    // exist.
1810    pub async fn recent_workspaces_on_disk(
1811        &self,
1812        fs: &dyn Fs,
1813    ) -> Result<
1814        Vec<(
1815            WorkspaceId,
1816            SerializedWorkspaceLocation,
1817            PathList,
1818            DateTime<Utc>,
1819        )>,
1820    > {
1821        let mut result = Vec::new();
1822        let mut delete_tasks = Vec::new();
1823        let remote_connections = self.remote_connections()?;
1824
1825        for (id, paths, remote_connection_id, timestamp) in self.recent_workspaces()? {
1826            if let Some(remote_connection_id) = remote_connection_id {
1827                if let Some(connection_options) = remote_connections.get(&remote_connection_id) {
1828                    result.push((
1829                        id,
1830                        SerializedWorkspaceLocation::Remote(connection_options.clone()),
1831                        paths,
1832                        timestamp,
1833                    ));
1834                } else {
1835                    delete_tasks.push(self.delete_workspace_by_id(id));
1836                }
1837                continue;
1838            }
1839
1840            let has_wsl_path = if cfg!(windows) {
1841                paths
1842                    .paths()
1843                    .iter()
1844                    .any(|path| util::paths::WslPath::from_path(path).is_some())
1845            } else {
1846                false
1847            };
1848
1849            // Delete the workspace if any of the paths are WSL paths.
1850            // If a local workspace points to WSL, this check will cause us to wait for the
1851            // WSL VM and file server to boot up. This can block for many seconds.
1852            // Supported scenarios use remote workspaces.
1853            if !has_wsl_path
1854                && Self::all_paths_exist_with_a_directory(paths.paths(), fs, Some(timestamp)).await
1855            {
1856                result.push((id, SerializedWorkspaceLocation::Local, paths, timestamp));
1857            } else {
1858                delete_tasks.push(self.delete_workspace_by_id(id));
1859            }
1860        }
1861
1862        futures::future::join_all(delete_tasks).await;
1863        Ok(result)
1864    }
1865
1866    pub async fn last_workspace(
1867        &self,
1868        fs: &dyn Fs,
1869    ) -> Result<
1870        Option<(
1871            WorkspaceId,
1872            SerializedWorkspaceLocation,
1873            PathList,
1874            DateTime<Utc>,
1875        )>,
1876    > {
1877        Ok(self.recent_workspaces_on_disk(fs).await?.into_iter().next())
1878    }
1879
1880    // Returns the locations of the workspaces that were still opened when the last
1881    // session was closed (i.e. when Zed was quit).
1882    // If `last_session_window_order` is provided, the returned locations are ordered
1883    // according to that.
1884    pub async fn last_session_workspace_locations(
1885        &self,
1886        last_session_id: &str,
1887        last_session_window_stack: Option<Vec<WindowId>>,
1888        fs: &dyn Fs,
1889    ) -> Result<Vec<SessionWorkspace>> {
1890        let mut workspaces = Vec::new();
1891
1892        for (workspace_id, paths, window_id, remote_connection_id) in
1893            self.session_workspaces(last_session_id.to_owned())?
1894        {
1895            let window_id = window_id.map(WindowId::from);
1896
1897            if let Some(remote_connection_id) = remote_connection_id {
1898                workspaces.push(SessionWorkspace {
1899                    workspace_id,
1900                    location: SerializedWorkspaceLocation::Remote(
1901                        self.remote_connection(remote_connection_id)?,
1902                    ),
1903                    paths,
1904                    window_id,
1905                });
1906            } else if paths.is_empty() {
1907                // Empty workspace with items (drafts, files) - include for restoration
1908                workspaces.push(SessionWorkspace {
1909                    workspace_id,
1910                    location: SerializedWorkspaceLocation::Local,
1911                    paths,
1912                    window_id,
1913                });
1914            } else {
1915                if Self::all_paths_exist_with_a_directory(paths.paths(), fs, None).await {
1916                    workspaces.push(SessionWorkspace {
1917                        workspace_id,
1918                        location: SerializedWorkspaceLocation::Local,
1919                        paths,
1920                        window_id,
1921                    });
1922                }
1923            }
1924        }
1925
1926        if let Some(stack) = last_session_window_stack {
1927            workspaces.sort_by_key(|workspace| {
1928                workspace
1929                    .window_id
1930                    .and_then(|id| stack.iter().position(|&order_id| order_id == id))
1931                    .unwrap_or(usize::MAX)
1932            });
1933        }
1934
1935        Ok(workspaces)
1936    }
1937
1938    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1939        Ok(self
1940            .get_pane_group(workspace_id, None)?
1941            .into_iter()
1942            .next()
1943            .unwrap_or_else(|| {
1944                SerializedPaneGroup::Pane(SerializedPane {
1945                    active: true,
1946                    children: vec![],
1947                    pinned_count: 0,
1948                })
1949            }))
1950    }
1951
1952    fn get_pane_group(
1953        &self,
1954        workspace_id: WorkspaceId,
1955        group_id: Option<GroupId>,
1956    ) -> Result<Vec<SerializedPaneGroup>> {
1957        type GroupKey = (Option<GroupId>, WorkspaceId);
1958        type GroupOrPane = (
1959            Option<GroupId>,
1960            Option<SerializedAxis>,
1961            Option<PaneId>,
1962            Option<bool>,
1963            Option<usize>,
1964            Option<String>,
1965        );
1966        self.select_bound::<GroupKey, GroupOrPane>(sql!(
1967            SELECT group_id, axis, pane_id, active, pinned_count, flexes
1968                FROM (SELECT
1969                        group_id,
1970                        axis,
1971                        NULL as pane_id,
1972                        NULL as active,
1973                        NULL as pinned_count,
1974                        position,
1975                        parent_group_id,
1976                        workspace_id,
1977                        flexes
1978                      FROM pane_groups
1979                    UNION
1980                      SELECT
1981                        NULL,
1982                        NULL,
1983                        center_panes.pane_id,
1984                        panes.active as active,
1985                        pinned_count,
1986                        position,
1987                        parent_group_id,
1988                        panes.workspace_id as workspace_id,
1989                        NULL
1990                      FROM center_panes
1991                      JOIN panes ON center_panes.pane_id = panes.pane_id)
1992                WHERE parent_group_id IS ? AND workspace_id = ?
1993                ORDER BY position
1994        ))?((group_id, workspace_id))?
1995        .into_iter()
1996        .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1997            let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1998            if let Some((group_id, axis)) = group_id.zip(axis) {
1999                let flexes = flexes
2000                    .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
2001                    .transpose()?;
2002
2003                Ok(SerializedPaneGroup::Group {
2004                    axis,
2005                    children: self.get_pane_group(workspace_id, Some(group_id))?,
2006                    flexes,
2007                })
2008            } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
2009                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
2010                    self.get_items(pane_id)?,
2011                    active,
2012                    pinned_count,
2013                )))
2014            } else {
2015                bail!("Pane Group Child was neither a pane group or a pane");
2016            }
2017        })
2018        // Filter out panes and pane groups which don't have any children or items
2019        .filter(|pane_group| match pane_group {
2020            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
2021            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
2022            _ => true,
2023        })
2024        .collect::<Result<_>>()
2025    }
2026
2027    fn save_pane_group(
2028        conn: &Connection,
2029        workspace_id: WorkspaceId,
2030        pane_group: &SerializedPaneGroup,
2031        parent: Option<(GroupId, usize)>,
2032    ) -> Result<()> {
2033        if parent.is_none() {
2034            log::debug!("Saving a pane group for workspace {workspace_id:?}");
2035        }
2036        match pane_group {
2037            SerializedPaneGroup::Group {
2038                axis,
2039                children,
2040                flexes,
2041            } => {
2042                let (parent_id, position) = parent.unzip();
2043
2044                let flex_string = flexes
2045                    .as_ref()
2046                    .map(|flexes| serde_json::json!(flexes).to_string());
2047
2048                let group_id = conn.select_row_bound::<_, i64>(sql!(
2049                    INSERT INTO pane_groups(
2050                        workspace_id,
2051                        parent_group_id,
2052                        position,
2053                        axis,
2054                        flexes
2055                    )
2056                    VALUES (?, ?, ?, ?, ?)
2057                    RETURNING group_id
2058                ))?((
2059                    workspace_id,
2060                    parent_id,
2061                    position,
2062                    *axis,
2063                    flex_string,
2064                ))?
2065                .context("Couldn't retrieve group_id from inserted pane_group")?;
2066
2067                for (position, group) in children.iter().enumerate() {
2068                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
2069                }
2070
2071                Ok(())
2072            }
2073            SerializedPaneGroup::Pane(pane) => {
2074                Self::save_pane(conn, workspace_id, pane, parent)?;
2075                Ok(())
2076            }
2077        }
2078    }
2079
2080    fn save_pane(
2081        conn: &Connection,
2082        workspace_id: WorkspaceId,
2083        pane: &SerializedPane,
2084        parent: Option<(GroupId, usize)>,
2085    ) -> Result<PaneId> {
2086        let pane_id = conn.select_row_bound::<_, i64>(sql!(
2087            INSERT INTO panes(workspace_id, active, pinned_count)
2088            VALUES (?, ?, ?)
2089            RETURNING pane_id
2090        ))?((workspace_id, pane.active, pane.pinned_count))?
2091        .context("Could not retrieve inserted pane_id")?;
2092
2093        let (parent_id, order) = parent.unzip();
2094        conn.exec_bound(sql!(
2095            INSERT INTO center_panes(pane_id, parent_group_id, position)
2096            VALUES (?, ?, ?)
2097        ))?((pane_id, parent_id, order))?;
2098
2099        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
2100
2101        Ok(pane_id)
2102    }
2103
2104    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
2105        self.select_bound(sql!(
2106            SELECT kind, item_id, active, preview FROM items
2107            WHERE pane_id = ?
2108                ORDER BY position
2109        ))?(pane_id)
2110    }
2111
2112    fn save_items(
2113        conn: &Connection,
2114        workspace_id: WorkspaceId,
2115        pane_id: PaneId,
2116        items: &[SerializedItem],
2117    ) -> Result<()> {
2118        let mut insert = conn.exec_bound(sql!(
2119            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
2120        )).context("Preparing insertion")?;
2121        for (position, item) in items.iter().enumerate() {
2122            insert((workspace_id, pane_id, position, item))?;
2123        }
2124
2125        Ok(())
2126    }
2127
2128    query! {
2129        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
2130            UPDATE workspaces
2131            SET timestamp = CURRENT_TIMESTAMP
2132            WHERE workspace_id = ?
2133        }
2134    }
2135
2136    query! {
2137        pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
2138            UPDATE workspaces
2139            SET window_state = ?2,
2140                window_x = ?3,
2141                window_y = ?4,
2142                window_width = ?5,
2143                window_height = ?6,
2144                display = ?7
2145            WHERE workspace_id = ?1
2146        }
2147    }
2148
2149    query! {
2150        pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
2151            UPDATE workspaces
2152            SET centered_layout = ?2
2153            WHERE workspace_id = ?1
2154        }
2155    }
2156
2157    query! {
2158        pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option<String>) -> Result<()> {
2159            UPDATE workspaces
2160            SET session_id = ?2
2161            WHERE workspace_id = ?1
2162        }
2163    }
2164
2165    query! {
2166        pub(crate) async fn set_session_binding(workspace_id: WorkspaceId, session_id: Option<String>, window_id: Option<u64>) -> Result<()> {
2167            UPDATE workspaces
2168            SET session_id = ?2, window_id = ?3
2169            WHERE workspace_id = ?1
2170        }
2171    }
2172
2173    pub(crate) async fn toolchains(
2174        &self,
2175        workspace_id: WorkspaceId,
2176    ) -> Result<Vec<(Toolchain, Arc<Path>, Arc<RelPath>)>> {
2177        self.write(move |this| {
2178            let mut select = this
2179                .select_bound(sql!(
2180                    SELECT
2181                        name, path, worktree_root_path, relative_worktree_path, language_name, raw_json
2182                    FROM toolchains
2183                    WHERE workspace_id = ?
2184                ))
2185                .context("select toolchains")?;
2186
2187            let toolchain: Vec<(String, String, String, String, String, String)> =
2188                select(workspace_id)?;
2189
2190            Ok(toolchain
2191                .into_iter()
2192                .filter_map(
2193                    |(name, path, worktree_root_path, relative_worktree_path, language, json)| {
2194                        Some((
2195                            Toolchain {
2196                                name: name.into(),
2197                                path: path.into(),
2198                                language_name: LanguageName::new(&language),
2199                                as_json: serde_json::Value::from_str(&json).ok()?,
2200                            },
2201                           Arc::from(worktree_root_path.as_ref()),
2202                            RelPath::from_proto(&relative_worktree_path).log_err()?,
2203                        ))
2204                    },
2205                )
2206                .collect())
2207        })
2208        .await
2209    }
2210
2211    pub async fn set_toolchain(
2212        &self,
2213        workspace_id: WorkspaceId,
2214        worktree_root_path: Arc<Path>,
2215        relative_worktree_path: Arc<RelPath>,
2216        toolchain: Toolchain,
2217    ) -> Result<()> {
2218        log::debug!(
2219            "Setting toolchain for workspace, worktree: {worktree_root_path:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
2220            toolchain.name
2221        );
2222        self.write(move |conn| {
2223            let mut insert = conn
2224                .exec_bound(sql!(
2225                    INSERT INTO toolchains(workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?,  ?, ?)
2226                    ON CONFLICT DO
2227                    UPDATE SET
2228                        name = ?5,
2229                        path = ?6,
2230                        raw_json = ?7
2231                ))
2232                .context("Preparing insertion")?;
2233
2234            insert((
2235                workspace_id,
2236                worktree_root_path.to_string_lossy().into_owned(),
2237                relative_worktree_path.as_unix_str(),
2238                toolchain.language_name.as_ref(),
2239                toolchain.name.as_ref(),
2240                toolchain.path.as_ref(),
2241                toolchain.as_json.to_string(),
2242            ))?;
2243
2244            Ok(())
2245        }).await
2246    }
2247
2248    pub(crate) async fn save_trusted_worktrees(
2249        &self,
2250        trusted_worktrees: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
2251    ) -> anyhow::Result<()> {
2252        use anyhow::Context as _;
2253        use db::sqlez::statement::Statement;
2254        use itertools::Itertools as _;
2255
2256        DB.clear_trusted_worktrees()
2257            .await
2258            .context("clearing previous trust state")?;
2259
2260        let trusted_worktrees = trusted_worktrees
2261            .into_iter()
2262            .flat_map(|(host, abs_paths)| {
2263                abs_paths
2264                    .into_iter()
2265                    .map(move |abs_path| (Some(abs_path), host.clone()))
2266            })
2267            .collect::<Vec<_>>();
2268        let mut first_worktree;
2269        let mut last_worktree = 0_usize;
2270        for (count, placeholders) in std::iter::once("(?, ?, ?)")
2271            .cycle()
2272            .take(trusted_worktrees.len())
2273            .chunks(MAX_QUERY_PLACEHOLDERS / 3)
2274            .into_iter()
2275            .map(|chunk| {
2276                let mut count = 0;
2277                let placeholders = chunk
2278                    .inspect(|_| {
2279                        count += 1;
2280                    })
2281                    .join(", ");
2282                (count, placeholders)
2283            })
2284            .collect::<Vec<_>>()
2285        {
2286            first_worktree = last_worktree;
2287            last_worktree = last_worktree + count;
2288            let query = format!(
2289                r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name)
2290VALUES {placeholders};"#
2291            );
2292
2293            let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec();
2294            self.write(move |conn| {
2295                let mut statement = Statement::prepare(conn, query)?;
2296                let mut next_index = 1;
2297                for (abs_path, host) in trusted_worktrees {
2298                    let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy());
2299                    next_index = statement.bind(
2300                        &abs_path.as_ref().map(|abs_path| abs_path.as_ref()),
2301                        next_index,
2302                    )?;
2303                    next_index = statement.bind(
2304                        &host
2305                            .as_ref()
2306                            .and_then(|host| Some(host.user_name.as_ref()?.as_str())),
2307                        next_index,
2308                    )?;
2309                    next_index = statement.bind(
2310                        &host.as_ref().map(|host| host.host_identifier.as_str()),
2311                        next_index,
2312                    )?;
2313                }
2314                statement.exec()
2315            })
2316            .await
2317            .context("inserting new trusted state")?;
2318        }
2319        Ok(())
2320    }
2321
2322    pub fn fetch_trusted_worktrees(&self) -> Result<DbTrustedPaths> {
2323        let trusted_worktrees = DB.trusted_worktrees()?;
2324        Ok(trusted_worktrees
2325            .into_iter()
2326            .filter_map(|(abs_path, user_name, host_name)| {
2327                let db_host = match (user_name, host_name) {
2328                    (None, Some(host_name)) => Some(RemoteHostLocation {
2329                        user_name: None,
2330                        host_identifier: SharedString::new(host_name),
2331                    }),
2332                    (Some(user_name), Some(host_name)) => Some(RemoteHostLocation {
2333                        user_name: Some(SharedString::new(user_name)),
2334                        host_identifier: SharedString::new(host_name),
2335                    }),
2336                    _ => None,
2337                };
2338                Some((db_host, abs_path?))
2339            })
2340            .fold(HashMap::default(), |mut acc, (remote_host, abs_path)| {
2341                acc.entry(remote_host)
2342                    .or_insert_with(HashSet::default)
2343                    .insert(abs_path);
2344                acc
2345            }))
2346    }
2347
2348    query! {
2349        fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
2350            SELECT absolute_path, user_name, host_name
2351            FROM trusted_worktrees
2352        }
2353    }
2354
2355    query! {
2356        pub async fn clear_trusted_worktrees() -> Result<()> {
2357            DELETE FROM trusted_worktrees
2358        }
2359    }
2360}
2361
2362pub fn delete_unloaded_items(
2363    alive_items: Vec<ItemId>,
2364    workspace_id: WorkspaceId,
2365    table: &'static str,
2366    db: &ThreadSafeConnection,
2367    cx: &mut App,
2368) -> Task<Result<()>> {
2369    let db = db.clone();
2370    cx.spawn(async move |_| {
2371        let placeholders = alive_items
2372            .iter()
2373            .map(|_| "?")
2374            .collect::<Vec<&str>>()
2375            .join(", ");
2376
2377        let query = format!(
2378            "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
2379        );
2380
2381        db.write(move |conn| {
2382            let mut statement = Statement::prepare(conn, query)?;
2383            let mut next_index = statement.bind(&workspace_id, 1)?;
2384            for id in alive_items {
2385                next_index = statement.bind(&id, next_index)?;
2386            }
2387            statement.exec()
2388        })
2389        .await
2390    })
2391}
2392
2393#[cfg(test)]
2394mod tests {
2395    use super::*;
2396    use crate::persistence::model::{
2397        SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, SessionWorkspace,
2398    };
2399    use gpui;
2400    use pretty_assertions::assert_eq;
2401    use remote::SshConnectionOptions;
2402    use serde_json::json;
2403    use std::{thread, time::Duration};
2404
2405    #[gpui::test]
2406    async fn test_multi_workspace_serializes_on_add_and_remove(cx: &mut gpui::TestAppContext) {
2407        use crate::multi_workspace::MultiWorkspace;
2408        use crate::persistence::read_multi_workspace_state;
2409        use feature_flags::FeatureFlagAppExt;
2410        use gpui::AppContext as _;
2411        use project::Project;
2412
2413        crate::tests::init_test(cx);
2414
2415        cx.update(|cx| {
2416            cx.set_staff(true);
2417            cx.update_flags(true, vec!["agent-v2".to_string()]);
2418        });
2419
2420        let fs = fs::FakeFs::new(cx.executor());
2421        let project1 = Project::test(fs.clone(), [], cx).await;
2422        let project2 = Project::test(fs.clone(), [], cx).await;
2423
2424        let (multi_workspace, cx) =
2425            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
2426
2427        multi_workspace.update_in(cx, |mw, _, cx| {
2428            mw.set_random_database_id(cx);
2429        });
2430
2431        let window_id =
2432            multi_workspace.update_in(cx, |_, window, _cx| window.window_handle().window_id());
2433
2434        // --- Add a second workspace ---
2435        let workspace2 = multi_workspace.update_in(cx, |mw, window, cx| {
2436            let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx));
2437            workspace.update(cx, |ws, _cx| ws.set_random_database_id());
2438            mw.activate(workspace.clone(), cx);
2439            workspace
2440        });
2441
2442        // Run background tasks so serialize has a chance to flush.
2443        cx.run_until_parked();
2444
2445        // Read back the persisted state and check that the active workspace ID was written.
2446        let state_after_add = read_multi_workspace_state(window_id);
2447        let active_workspace2_db_id = workspace2.read_with(cx, |ws, _| ws.database_id());
2448        assert_eq!(
2449            state_after_add.active_workspace_id, active_workspace2_db_id,
2450            "After adding a second workspace, the serialized active_workspace_id should match \
2451             the newly activated workspace's database id"
2452        );
2453
2454        // --- Remove the second workspace (index 1) ---
2455        multi_workspace.update_in(cx, |mw, window, cx| {
2456            mw.remove_workspace(1, window, cx);
2457        });
2458
2459        cx.run_until_parked();
2460
2461        let state_after_remove = read_multi_workspace_state(window_id);
2462        let remaining_db_id =
2463            multi_workspace.read_with(cx, |mw, cx| mw.workspace().read(cx).database_id());
2464        assert_eq!(
2465            state_after_remove.active_workspace_id, remaining_db_id,
2466            "After removing a workspace, the serialized active_workspace_id should match \
2467             the remaining active workspace's database id"
2468        );
2469    }
2470
2471    #[gpui::test]
2472    async fn test_breakpoints() {
2473        zlog::init_test();
2474
2475        let db = WorkspaceDb::open_test_db("test_breakpoints").await;
2476        let id = db.next_id().await.unwrap();
2477
2478        let path = Path::new("/tmp/test.rs");
2479
2480        let breakpoint = Breakpoint {
2481            position: 123,
2482            message: None,
2483            state: BreakpointState::Enabled,
2484            condition: None,
2485            hit_condition: None,
2486        };
2487
2488        let log_breakpoint = Breakpoint {
2489            position: 456,
2490            message: Some("Test log message".into()),
2491            state: BreakpointState::Enabled,
2492            condition: None,
2493            hit_condition: None,
2494        };
2495
2496        let disable_breakpoint = Breakpoint {
2497            position: 578,
2498            message: None,
2499            state: BreakpointState::Disabled,
2500            condition: None,
2501            hit_condition: None,
2502        };
2503
2504        let condition_breakpoint = Breakpoint {
2505            position: 789,
2506            message: None,
2507            state: BreakpointState::Enabled,
2508            condition: Some("x > 5".into()),
2509            hit_condition: None,
2510        };
2511
2512        let hit_condition_breakpoint = Breakpoint {
2513            position: 999,
2514            message: None,
2515            state: BreakpointState::Enabled,
2516            condition: None,
2517            hit_condition: Some(">= 3".into()),
2518        };
2519
2520        let workspace = SerializedWorkspace {
2521            id,
2522            paths: PathList::new(&["/tmp"]),
2523            location: SerializedWorkspaceLocation::Local,
2524            center_group: Default::default(),
2525            window_bounds: Default::default(),
2526            display: Default::default(),
2527            docks: Default::default(),
2528            centered_layout: false,
2529            breakpoints: {
2530                let mut map = collections::BTreeMap::default();
2531                map.insert(
2532                    Arc::from(path),
2533                    vec![
2534                        SourceBreakpoint {
2535                            row: breakpoint.position,
2536                            path: Arc::from(path),
2537                            message: breakpoint.message.clone(),
2538                            state: breakpoint.state,
2539                            condition: breakpoint.condition.clone(),
2540                            hit_condition: breakpoint.hit_condition.clone(),
2541                        },
2542                        SourceBreakpoint {
2543                            row: log_breakpoint.position,
2544                            path: Arc::from(path),
2545                            message: log_breakpoint.message.clone(),
2546                            state: log_breakpoint.state,
2547                            condition: log_breakpoint.condition.clone(),
2548                            hit_condition: log_breakpoint.hit_condition.clone(),
2549                        },
2550                        SourceBreakpoint {
2551                            row: disable_breakpoint.position,
2552                            path: Arc::from(path),
2553                            message: disable_breakpoint.message.clone(),
2554                            state: disable_breakpoint.state,
2555                            condition: disable_breakpoint.condition.clone(),
2556                            hit_condition: disable_breakpoint.hit_condition.clone(),
2557                        },
2558                        SourceBreakpoint {
2559                            row: condition_breakpoint.position,
2560                            path: Arc::from(path),
2561                            message: condition_breakpoint.message.clone(),
2562                            state: condition_breakpoint.state,
2563                            condition: condition_breakpoint.condition.clone(),
2564                            hit_condition: condition_breakpoint.hit_condition.clone(),
2565                        },
2566                        SourceBreakpoint {
2567                            row: hit_condition_breakpoint.position,
2568                            path: Arc::from(path),
2569                            message: hit_condition_breakpoint.message.clone(),
2570                            state: hit_condition_breakpoint.state,
2571                            condition: hit_condition_breakpoint.condition.clone(),
2572                            hit_condition: hit_condition_breakpoint.hit_condition.clone(),
2573                        },
2574                    ],
2575                );
2576                map
2577            },
2578            session_id: None,
2579            window_id: None,
2580            user_toolchains: Default::default(),
2581        };
2582
2583        db.save_workspace(workspace.clone()).await;
2584
2585        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2586        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
2587
2588        assert_eq!(loaded_breakpoints.len(), 5);
2589
2590        // normal breakpoint
2591        assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
2592        assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
2593        assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
2594        assert_eq!(
2595            loaded_breakpoints[0].hit_condition,
2596            breakpoint.hit_condition
2597        );
2598        assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
2599        assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
2600
2601        // enabled breakpoint
2602        assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
2603        assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
2604        assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
2605        assert_eq!(
2606            loaded_breakpoints[1].hit_condition,
2607            log_breakpoint.hit_condition
2608        );
2609        assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
2610        assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
2611
2612        // disable breakpoint
2613        assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
2614        assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
2615        assert_eq!(
2616            loaded_breakpoints[2].condition,
2617            disable_breakpoint.condition
2618        );
2619        assert_eq!(
2620            loaded_breakpoints[2].hit_condition,
2621            disable_breakpoint.hit_condition
2622        );
2623        assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
2624        assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
2625
2626        // condition breakpoint
2627        assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
2628        assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
2629        assert_eq!(
2630            loaded_breakpoints[3].condition,
2631            condition_breakpoint.condition
2632        );
2633        assert_eq!(
2634            loaded_breakpoints[3].hit_condition,
2635            condition_breakpoint.hit_condition
2636        );
2637        assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
2638        assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
2639
2640        // hit condition breakpoint
2641        assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
2642        assert_eq!(
2643            loaded_breakpoints[4].message,
2644            hit_condition_breakpoint.message
2645        );
2646        assert_eq!(
2647            loaded_breakpoints[4].condition,
2648            hit_condition_breakpoint.condition
2649        );
2650        assert_eq!(
2651            loaded_breakpoints[4].hit_condition,
2652            hit_condition_breakpoint.hit_condition
2653        );
2654        assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
2655        assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
2656    }
2657
2658    #[gpui::test]
2659    async fn test_remove_last_breakpoint() {
2660        zlog::init_test();
2661
2662        let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
2663        let id = db.next_id().await.unwrap();
2664
2665        let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
2666
2667        let breakpoint_to_remove = Breakpoint {
2668            position: 100,
2669            message: None,
2670            state: BreakpointState::Enabled,
2671            condition: None,
2672            hit_condition: None,
2673        };
2674
2675        let workspace = SerializedWorkspace {
2676            id,
2677            paths: PathList::new(&["/tmp"]),
2678            location: SerializedWorkspaceLocation::Local,
2679            center_group: Default::default(),
2680            window_bounds: Default::default(),
2681            display: Default::default(),
2682            docks: Default::default(),
2683            centered_layout: false,
2684            breakpoints: {
2685                let mut map = collections::BTreeMap::default();
2686                map.insert(
2687                    Arc::from(singular_path),
2688                    vec![SourceBreakpoint {
2689                        row: breakpoint_to_remove.position,
2690                        path: Arc::from(singular_path),
2691                        message: None,
2692                        state: BreakpointState::Enabled,
2693                        condition: None,
2694                        hit_condition: None,
2695                    }],
2696                );
2697                map
2698            },
2699            session_id: None,
2700            window_id: None,
2701            user_toolchains: Default::default(),
2702        };
2703
2704        db.save_workspace(workspace.clone()).await;
2705
2706        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2707        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
2708
2709        assert_eq!(loaded_breakpoints.len(), 1);
2710        assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
2711        assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
2712        assert_eq!(
2713            loaded_breakpoints[0].condition,
2714            breakpoint_to_remove.condition
2715        );
2716        assert_eq!(
2717            loaded_breakpoints[0].hit_condition,
2718            breakpoint_to_remove.hit_condition
2719        );
2720        assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
2721        assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
2722
2723        let workspace_without_breakpoint = SerializedWorkspace {
2724            id,
2725            paths: PathList::new(&["/tmp"]),
2726            location: SerializedWorkspaceLocation::Local,
2727            center_group: Default::default(),
2728            window_bounds: Default::default(),
2729            display: Default::default(),
2730            docks: Default::default(),
2731            centered_layout: false,
2732            breakpoints: collections::BTreeMap::default(),
2733            session_id: None,
2734            window_id: None,
2735            user_toolchains: Default::default(),
2736        };
2737
2738        db.save_workspace(workspace_without_breakpoint.clone())
2739            .await;
2740
2741        let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
2742        let empty_breakpoints = loaded_after_remove
2743            .breakpoints
2744            .get(&Arc::from(singular_path));
2745
2746        assert!(empty_breakpoints.is_none());
2747    }
2748
2749    #[gpui::test]
2750    async fn test_next_id_stability() {
2751        zlog::init_test();
2752
2753        let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
2754
2755        db.write(|conn| {
2756            conn.migrate(
2757                "test_table",
2758                &[sql!(
2759                    CREATE TABLE test_table(
2760                        text TEXT,
2761                        workspace_id INTEGER,
2762                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2763                        ON DELETE CASCADE
2764                    ) STRICT;
2765                )],
2766                &mut |_, _, _| false,
2767            )
2768            .unwrap();
2769        })
2770        .await;
2771
2772        let id = db.next_id().await.unwrap();
2773        // Assert the empty row got inserted
2774        assert_eq!(
2775            Some(id),
2776            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
2777                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
2778            ))
2779            .unwrap()(id)
2780            .unwrap()
2781        );
2782
2783        db.write(move |conn| {
2784            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2785                .unwrap()(("test-text-1", id))
2786            .unwrap()
2787        })
2788        .await;
2789
2790        let test_text_1 = db
2791            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2792            .unwrap()(1)
2793        .unwrap()
2794        .unwrap();
2795        assert_eq!(test_text_1, "test-text-1");
2796    }
2797
2798    #[gpui::test]
2799    async fn test_workspace_id_stability() {
2800        zlog::init_test();
2801
2802        let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
2803
2804        db.write(|conn| {
2805            conn.migrate(
2806                "test_table",
2807                &[sql!(
2808                        CREATE TABLE test_table(
2809                            text TEXT,
2810                            workspace_id INTEGER,
2811                            FOREIGN KEY(workspace_id)
2812                                REFERENCES workspaces(workspace_id)
2813                            ON DELETE CASCADE
2814                        ) STRICT;)],
2815                &mut |_, _, _| false,
2816            )
2817        })
2818        .await
2819        .unwrap();
2820
2821        let mut workspace_1 = SerializedWorkspace {
2822            id: WorkspaceId(1),
2823            paths: PathList::new(&["/tmp", "/tmp2"]),
2824            location: SerializedWorkspaceLocation::Local,
2825            center_group: Default::default(),
2826            window_bounds: Default::default(),
2827            display: Default::default(),
2828            docks: Default::default(),
2829            centered_layout: false,
2830            breakpoints: Default::default(),
2831            session_id: None,
2832            window_id: None,
2833            user_toolchains: Default::default(),
2834        };
2835
2836        let workspace_2 = SerializedWorkspace {
2837            id: WorkspaceId(2),
2838            paths: PathList::new(&["/tmp"]),
2839            location: SerializedWorkspaceLocation::Local,
2840            center_group: Default::default(),
2841            window_bounds: Default::default(),
2842            display: Default::default(),
2843            docks: Default::default(),
2844            centered_layout: false,
2845            breakpoints: Default::default(),
2846            session_id: None,
2847            window_id: None,
2848            user_toolchains: Default::default(),
2849        };
2850
2851        db.save_workspace(workspace_1.clone()).await;
2852
2853        db.write(|conn| {
2854            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2855                .unwrap()(("test-text-1", 1))
2856            .unwrap();
2857        })
2858        .await;
2859
2860        db.save_workspace(workspace_2.clone()).await;
2861
2862        db.write(|conn| {
2863            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2864                .unwrap()(("test-text-2", 2))
2865            .unwrap();
2866        })
2867        .await;
2868
2869        workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
2870        db.save_workspace(workspace_1.clone()).await;
2871        db.save_workspace(workspace_1).await;
2872        db.save_workspace(workspace_2).await;
2873
2874        let test_text_2 = db
2875            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2876            .unwrap()(2)
2877        .unwrap()
2878        .unwrap();
2879        assert_eq!(test_text_2, "test-text-2");
2880
2881        let test_text_1 = db
2882            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2883            .unwrap()(1)
2884        .unwrap()
2885        .unwrap();
2886        assert_eq!(test_text_1, "test-text-1");
2887    }
2888
2889    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
2890        SerializedPaneGroup::Group {
2891            axis: SerializedAxis(axis),
2892            flexes: None,
2893            children,
2894        }
2895    }
2896
2897    #[gpui::test]
2898    async fn test_full_workspace_serialization() {
2899        zlog::init_test();
2900
2901        let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
2902
2903        //  -----------------
2904        //  | 1,2   | 5,6   |
2905        //  | - - - |       |
2906        //  | 3,4   |       |
2907        //  -----------------
2908        let center_group = group(
2909            Axis::Horizontal,
2910            vec![
2911                group(
2912                    Axis::Vertical,
2913                    vec![
2914                        SerializedPaneGroup::Pane(SerializedPane::new(
2915                            vec![
2916                                SerializedItem::new("Terminal", 5, false, false),
2917                                SerializedItem::new("Terminal", 6, true, false),
2918                            ],
2919                            false,
2920                            0,
2921                        )),
2922                        SerializedPaneGroup::Pane(SerializedPane::new(
2923                            vec![
2924                                SerializedItem::new("Terminal", 7, true, false),
2925                                SerializedItem::new("Terminal", 8, false, false),
2926                            ],
2927                            false,
2928                            0,
2929                        )),
2930                    ],
2931                ),
2932                SerializedPaneGroup::Pane(SerializedPane::new(
2933                    vec![
2934                        SerializedItem::new("Terminal", 9, false, false),
2935                        SerializedItem::new("Terminal", 10, true, false),
2936                    ],
2937                    false,
2938                    0,
2939                )),
2940            ],
2941        );
2942
2943        let workspace = SerializedWorkspace {
2944            id: WorkspaceId(5),
2945            paths: PathList::new(&["/tmp", "/tmp2"]),
2946            location: SerializedWorkspaceLocation::Local,
2947            center_group,
2948            window_bounds: Default::default(),
2949            breakpoints: Default::default(),
2950            display: Default::default(),
2951            docks: Default::default(),
2952            centered_layout: false,
2953            session_id: None,
2954            window_id: Some(999),
2955            user_toolchains: Default::default(),
2956        };
2957
2958        db.save_workspace(workspace.clone()).await;
2959
2960        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2961        assert_eq!(workspace, round_trip_workspace.unwrap());
2962
2963        // Test guaranteed duplicate IDs
2964        db.save_workspace(workspace.clone()).await;
2965        db.save_workspace(workspace.clone()).await;
2966
2967        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2968        assert_eq!(workspace, round_trip_workspace.unwrap());
2969    }
2970
2971    #[gpui::test]
2972    async fn test_workspace_assignment() {
2973        zlog::init_test();
2974
2975        let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2976
2977        let workspace_1 = SerializedWorkspace {
2978            id: WorkspaceId(1),
2979            paths: PathList::new(&["/tmp", "/tmp2"]),
2980            location: SerializedWorkspaceLocation::Local,
2981            center_group: Default::default(),
2982            window_bounds: Default::default(),
2983            breakpoints: Default::default(),
2984            display: Default::default(),
2985            docks: Default::default(),
2986            centered_layout: false,
2987            session_id: None,
2988            window_id: Some(1),
2989            user_toolchains: Default::default(),
2990        };
2991
2992        let mut workspace_2 = SerializedWorkspace {
2993            id: WorkspaceId(2),
2994            paths: PathList::new(&["/tmp"]),
2995            location: SerializedWorkspaceLocation::Local,
2996            center_group: Default::default(),
2997            window_bounds: Default::default(),
2998            display: Default::default(),
2999            docks: Default::default(),
3000            centered_layout: false,
3001            breakpoints: Default::default(),
3002            session_id: None,
3003            window_id: Some(2),
3004            user_toolchains: Default::default(),
3005        };
3006
3007        db.save_workspace(workspace_1.clone()).await;
3008        db.save_workspace(workspace_2.clone()).await;
3009
3010        // Test that paths are treated as a set
3011        assert_eq!(
3012            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
3013            workspace_1
3014        );
3015        assert_eq!(
3016            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
3017            workspace_1
3018        );
3019
3020        // Make sure that other keys work
3021        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
3022        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
3023
3024        // Test 'mutate' case of updating a pre-existing id
3025        workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
3026
3027        db.save_workspace(workspace_2.clone()).await;
3028        assert_eq!(
3029            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
3030            workspace_2
3031        );
3032
3033        // Test other mechanism for mutating
3034        let mut workspace_3 = SerializedWorkspace {
3035            id: WorkspaceId(3),
3036            paths: PathList::new(&["/tmp2", "/tmp"]),
3037            location: SerializedWorkspaceLocation::Local,
3038            center_group: Default::default(),
3039            window_bounds: Default::default(),
3040            breakpoints: Default::default(),
3041            display: Default::default(),
3042            docks: Default::default(),
3043            centered_layout: false,
3044            session_id: None,
3045            window_id: Some(3),
3046            user_toolchains: Default::default(),
3047        };
3048
3049        db.save_workspace(workspace_3.clone()).await;
3050        assert_eq!(
3051            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
3052            workspace_3
3053        );
3054
3055        // Make sure that updating paths differently also works
3056        workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
3057        db.save_workspace(workspace_3.clone()).await;
3058        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
3059        assert_eq!(
3060            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
3061                .unwrap(),
3062            workspace_3
3063        );
3064    }
3065
3066    #[gpui::test]
3067    async fn test_session_workspaces() {
3068        zlog::init_test();
3069
3070        let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
3071
3072        let workspace_1 = SerializedWorkspace {
3073            id: WorkspaceId(1),
3074            paths: PathList::new(&["/tmp1"]),
3075            location: SerializedWorkspaceLocation::Local,
3076            center_group: Default::default(),
3077            window_bounds: Default::default(),
3078            display: Default::default(),
3079            docks: Default::default(),
3080            centered_layout: false,
3081            breakpoints: Default::default(),
3082            session_id: Some("session-id-1".to_owned()),
3083            window_id: Some(10),
3084            user_toolchains: Default::default(),
3085        };
3086
3087        let workspace_2 = SerializedWorkspace {
3088            id: WorkspaceId(2),
3089            paths: PathList::new(&["/tmp2"]),
3090            location: SerializedWorkspaceLocation::Local,
3091            center_group: Default::default(),
3092            window_bounds: Default::default(),
3093            display: Default::default(),
3094            docks: Default::default(),
3095            centered_layout: false,
3096            breakpoints: Default::default(),
3097            session_id: Some("session-id-1".to_owned()),
3098            window_id: Some(20),
3099            user_toolchains: Default::default(),
3100        };
3101
3102        let workspace_3 = SerializedWorkspace {
3103            id: WorkspaceId(3),
3104            paths: PathList::new(&["/tmp3"]),
3105            location: SerializedWorkspaceLocation::Local,
3106            center_group: Default::default(),
3107            window_bounds: Default::default(),
3108            display: Default::default(),
3109            docks: Default::default(),
3110            centered_layout: false,
3111            breakpoints: Default::default(),
3112            session_id: Some("session-id-2".to_owned()),
3113            window_id: Some(30),
3114            user_toolchains: Default::default(),
3115        };
3116
3117        let workspace_4 = SerializedWorkspace {
3118            id: WorkspaceId(4),
3119            paths: PathList::new(&["/tmp4"]),
3120            location: SerializedWorkspaceLocation::Local,
3121            center_group: Default::default(),
3122            window_bounds: Default::default(),
3123            display: Default::default(),
3124            docks: Default::default(),
3125            centered_layout: false,
3126            breakpoints: Default::default(),
3127            session_id: None,
3128            window_id: None,
3129            user_toolchains: Default::default(),
3130        };
3131
3132        let connection_id = db
3133            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3134                host: "my-host".into(),
3135                port: Some(1234),
3136                ..Default::default()
3137            }))
3138            .await
3139            .unwrap();
3140
3141        let workspace_5 = SerializedWorkspace {
3142            id: WorkspaceId(5),
3143            paths: PathList::default(),
3144            location: SerializedWorkspaceLocation::Remote(
3145                db.remote_connection(connection_id).unwrap(),
3146            ),
3147            center_group: Default::default(),
3148            window_bounds: Default::default(),
3149            display: Default::default(),
3150            docks: Default::default(),
3151            centered_layout: false,
3152            breakpoints: Default::default(),
3153            session_id: Some("session-id-2".to_owned()),
3154            window_id: Some(50),
3155            user_toolchains: Default::default(),
3156        };
3157
3158        let workspace_6 = SerializedWorkspace {
3159            id: WorkspaceId(6),
3160            paths: PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
3161            location: SerializedWorkspaceLocation::Local,
3162            center_group: Default::default(),
3163            window_bounds: Default::default(),
3164            breakpoints: Default::default(),
3165            display: Default::default(),
3166            docks: Default::default(),
3167            centered_layout: false,
3168            session_id: Some("session-id-3".to_owned()),
3169            window_id: Some(60),
3170            user_toolchains: Default::default(),
3171        };
3172
3173        db.save_workspace(workspace_1.clone()).await;
3174        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
3175        db.save_workspace(workspace_2.clone()).await;
3176        db.save_workspace(workspace_3.clone()).await;
3177        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
3178        db.save_workspace(workspace_4.clone()).await;
3179        db.save_workspace(workspace_5.clone()).await;
3180        db.save_workspace(workspace_6.clone()).await;
3181
3182        let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
3183        assert_eq!(locations.len(), 2);
3184        assert_eq!(locations[0].0, WorkspaceId(2));
3185        assert_eq!(locations[0].1, PathList::new(&["/tmp2"]));
3186        assert_eq!(locations[0].2, Some(20));
3187        assert_eq!(locations[1].0, WorkspaceId(1));
3188        assert_eq!(locations[1].1, PathList::new(&["/tmp1"]));
3189        assert_eq!(locations[1].2, Some(10));
3190
3191        let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
3192        assert_eq!(locations.len(), 2);
3193        assert_eq!(locations[0].0, WorkspaceId(5));
3194        assert_eq!(locations[0].1, PathList::default());
3195        assert_eq!(locations[0].2, Some(50));
3196        assert_eq!(locations[0].3, Some(connection_id));
3197        assert_eq!(locations[1].0, WorkspaceId(3));
3198        assert_eq!(locations[1].1, PathList::new(&["/tmp3"]));
3199        assert_eq!(locations[1].2, Some(30));
3200
3201        let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
3202        assert_eq!(locations.len(), 1);
3203        assert_eq!(locations[0].0, WorkspaceId(6));
3204        assert_eq!(
3205            locations[0].1,
3206            PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
3207        );
3208        assert_eq!(locations[0].2, Some(60));
3209    }
3210
3211    fn default_workspace<P: AsRef<Path>>(
3212        paths: &[P],
3213        center_group: &SerializedPaneGroup,
3214    ) -> SerializedWorkspace {
3215        SerializedWorkspace {
3216            id: WorkspaceId(4),
3217            paths: PathList::new(paths),
3218            location: SerializedWorkspaceLocation::Local,
3219            center_group: center_group.clone(),
3220            window_bounds: Default::default(),
3221            display: Default::default(),
3222            docks: Default::default(),
3223            breakpoints: Default::default(),
3224            centered_layout: false,
3225            session_id: None,
3226            window_id: None,
3227            user_toolchains: Default::default(),
3228        }
3229    }
3230
3231    #[gpui::test]
3232    async fn test_last_session_workspace_locations(cx: &mut gpui::TestAppContext) {
3233        let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
3234        let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
3235        let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
3236        let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
3237
3238        let fs = fs::FakeFs::new(cx.executor());
3239        fs.insert_tree(dir1.path(), json!({})).await;
3240        fs.insert_tree(dir2.path(), json!({})).await;
3241        fs.insert_tree(dir3.path(), json!({})).await;
3242        fs.insert_tree(dir4.path(), json!({})).await;
3243
3244        let db =
3245            WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
3246
3247        let workspaces = [
3248            (1, vec![dir1.path()], 9),
3249            (2, vec![dir2.path()], 5),
3250            (3, vec![dir3.path()], 8),
3251            (4, vec![dir4.path()], 2),
3252            (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
3253            (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
3254        ]
3255        .into_iter()
3256        .map(|(id, paths, window_id)| SerializedWorkspace {
3257            id: WorkspaceId(id),
3258            paths: PathList::new(paths.as_slice()),
3259            location: SerializedWorkspaceLocation::Local,
3260            center_group: Default::default(),
3261            window_bounds: Default::default(),
3262            display: Default::default(),
3263            docks: Default::default(),
3264            centered_layout: false,
3265            session_id: Some("one-session".to_owned()),
3266            breakpoints: Default::default(),
3267            window_id: Some(window_id),
3268            user_toolchains: Default::default(),
3269        })
3270        .collect::<Vec<_>>();
3271
3272        for workspace in workspaces.iter() {
3273            db.save_workspace(workspace.clone()).await;
3274        }
3275
3276        let stack = Some(Vec::from([
3277            WindowId::from(2), // Top
3278            WindowId::from(8),
3279            WindowId::from(5),
3280            WindowId::from(9),
3281            WindowId::from(3),
3282            WindowId::from(4), // Bottom
3283        ]));
3284
3285        let locations = db
3286            .last_session_workspace_locations("one-session", stack, fs.as_ref())
3287            .await
3288            .unwrap();
3289        assert_eq!(
3290            locations,
3291            [
3292                SessionWorkspace {
3293                    workspace_id: WorkspaceId(4),
3294                    location: SerializedWorkspaceLocation::Local,
3295                    paths: PathList::new(&[dir4.path()]),
3296                    window_id: Some(WindowId::from(2u64)),
3297                },
3298                SessionWorkspace {
3299                    workspace_id: WorkspaceId(3),
3300                    location: SerializedWorkspaceLocation::Local,
3301                    paths: PathList::new(&[dir3.path()]),
3302                    window_id: Some(WindowId::from(8u64)),
3303                },
3304                SessionWorkspace {
3305                    workspace_id: WorkspaceId(2),
3306                    location: SerializedWorkspaceLocation::Local,
3307                    paths: PathList::new(&[dir2.path()]),
3308                    window_id: Some(WindowId::from(5u64)),
3309                },
3310                SessionWorkspace {
3311                    workspace_id: WorkspaceId(1),
3312                    location: SerializedWorkspaceLocation::Local,
3313                    paths: PathList::new(&[dir1.path()]),
3314                    window_id: Some(WindowId::from(9u64)),
3315                },
3316                SessionWorkspace {
3317                    workspace_id: WorkspaceId(5),
3318                    location: SerializedWorkspaceLocation::Local,
3319                    paths: PathList::new(&[dir1.path(), dir2.path(), dir3.path()]),
3320                    window_id: Some(WindowId::from(3u64)),
3321                },
3322                SessionWorkspace {
3323                    workspace_id: WorkspaceId(6),
3324                    location: SerializedWorkspaceLocation::Local,
3325                    paths: PathList::new(&[dir4.path(), dir3.path(), dir2.path()]),
3326                    window_id: Some(WindowId::from(4u64)),
3327                },
3328            ]
3329        );
3330    }
3331
3332    #[gpui::test]
3333    async fn test_last_session_workspace_locations_remote(cx: &mut gpui::TestAppContext) {
3334        let fs = fs::FakeFs::new(cx.executor());
3335        let db =
3336            WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
3337                .await;
3338
3339        let remote_connections = [
3340            ("host-1", "my-user-1"),
3341            ("host-2", "my-user-2"),
3342            ("host-3", "my-user-3"),
3343            ("host-4", "my-user-4"),
3344        ]
3345        .into_iter()
3346        .map(|(host, user)| async {
3347            let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
3348                host: host.into(),
3349                username: Some(user.to_string()),
3350                ..Default::default()
3351            });
3352            db.get_or_create_remote_connection(options.clone())
3353                .await
3354                .unwrap();
3355            options
3356        })
3357        .collect::<Vec<_>>();
3358
3359        let remote_connections = futures::future::join_all(remote_connections).await;
3360
3361        let workspaces = [
3362            (1, remote_connections[0].clone(), 9),
3363            (2, remote_connections[1].clone(), 5),
3364            (3, remote_connections[2].clone(), 8),
3365            (4, remote_connections[3].clone(), 2),
3366        ]
3367        .into_iter()
3368        .map(|(id, remote_connection, window_id)| SerializedWorkspace {
3369            id: WorkspaceId(id),
3370            paths: PathList::default(),
3371            location: SerializedWorkspaceLocation::Remote(remote_connection),
3372            center_group: Default::default(),
3373            window_bounds: Default::default(),
3374            display: Default::default(),
3375            docks: Default::default(),
3376            centered_layout: false,
3377            session_id: Some("one-session".to_owned()),
3378            breakpoints: Default::default(),
3379            window_id: Some(window_id),
3380            user_toolchains: Default::default(),
3381        })
3382        .collect::<Vec<_>>();
3383
3384        for workspace in workspaces.iter() {
3385            db.save_workspace(workspace.clone()).await;
3386        }
3387
3388        let stack = Some(Vec::from([
3389            WindowId::from(2), // Top
3390            WindowId::from(8),
3391            WindowId::from(5),
3392            WindowId::from(9), // Bottom
3393        ]));
3394
3395        let have = db
3396            .last_session_workspace_locations("one-session", stack, fs.as_ref())
3397            .await
3398            .unwrap();
3399        assert_eq!(have.len(), 4);
3400        assert_eq!(
3401            have[0],
3402            SessionWorkspace {
3403                workspace_id: WorkspaceId(4),
3404                location: SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
3405                paths: PathList::default(),
3406                window_id: Some(WindowId::from(2u64)),
3407            }
3408        );
3409        assert_eq!(
3410            have[1],
3411            SessionWorkspace {
3412                workspace_id: WorkspaceId(3),
3413                location: SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
3414                paths: PathList::default(),
3415                window_id: Some(WindowId::from(8u64)),
3416            }
3417        );
3418        assert_eq!(
3419            have[2],
3420            SessionWorkspace {
3421                workspace_id: WorkspaceId(2),
3422                location: SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
3423                paths: PathList::default(),
3424                window_id: Some(WindowId::from(5u64)),
3425            }
3426        );
3427        assert_eq!(
3428            have[3],
3429            SessionWorkspace {
3430                workspace_id: WorkspaceId(1),
3431                location: SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
3432                paths: PathList::default(),
3433                window_id: Some(WindowId::from(9u64)),
3434            }
3435        );
3436    }
3437
3438    #[gpui::test]
3439    async fn test_get_or_create_ssh_project() {
3440        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
3441
3442        let host = "example.com".to_string();
3443        let port = Some(22_u16);
3444        let user = Some("user".to_string());
3445
3446        let connection_id = db
3447            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3448                host: host.clone().into(),
3449                port,
3450                username: user.clone(),
3451                ..Default::default()
3452            }))
3453            .await
3454            .unwrap();
3455
3456        // Test that calling the function again with the same parameters returns the same project
3457        let same_connection = db
3458            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3459                host: host.clone().into(),
3460                port,
3461                username: user.clone(),
3462                ..Default::default()
3463            }))
3464            .await
3465            .unwrap();
3466
3467        assert_eq!(connection_id, same_connection);
3468
3469        // Test with different parameters
3470        let host2 = "otherexample.com".to_string();
3471        let port2 = None;
3472        let user2 = Some("otheruser".to_string());
3473
3474        let different_connection = db
3475            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3476                host: host2.clone().into(),
3477                port: port2,
3478                username: user2.clone(),
3479                ..Default::default()
3480            }))
3481            .await
3482            .unwrap();
3483
3484        assert_ne!(connection_id, different_connection);
3485    }
3486
3487    #[gpui::test]
3488    async fn test_get_or_create_ssh_project_with_null_user() {
3489        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
3490
3491        let (host, port, user) = ("example.com".to_string(), None, None);
3492
3493        let connection_id = db
3494            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3495                host: host.clone().into(),
3496                port,
3497                username: None,
3498                ..Default::default()
3499            }))
3500            .await
3501            .unwrap();
3502
3503        let same_connection_id = db
3504            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3505                host: host.clone().into(),
3506                port,
3507                username: user.clone(),
3508                ..Default::default()
3509            }))
3510            .await
3511            .unwrap();
3512
3513        assert_eq!(connection_id, same_connection_id);
3514    }
3515
3516    #[gpui::test]
3517    async fn test_get_remote_connections() {
3518        let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
3519
3520        let connections = [
3521            ("example.com".to_string(), None, None),
3522            (
3523                "anotherexample.com".to_string(),
3524                Some(123_u16),
3525                Some("user2".to_string()),
3526            ),
3527            ("yetanother.com".to_string(), Some(345_u16), None),
3528        ];
3529
3530        let mut ids = Vec::new();
3531        for (host, port, user) in connections.iter() {
3532            ids.push(
3533                db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
3534                    SshConnectionOptions {
3535                        host: host.clone().into(),
3536                        port: *port,
3537                        username: user.clone(),
3538                        ..Default::default()
3539                    },
3540                ))
3541                .await
3542                .unwrap(),
3543            );
3544        }
3545
3546        let stored_connections = db.remote_connections().unwrap();
3547        assert_eq!(
3548            stored_connections,
3549            [
3550                (
3551                    ids[0],
3552                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3553                        host: "example.com".into(),
3554                        port: None,
3555                        username: None,
3556                        ..Default::default()
3557                    }),
3558                ),
3559                (
3560                    ids[1],
3561                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3562                        host: "anotherexample.com".into(),
3563                        port: Some(123),
3564                        username: Some("user2".into()),
3565                        ..Default::default()
3566                    }),
3567                ),
3568                (
3569                    ids[2],
3570                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3571                        host: "yetanother.com".into(),
3572                        port: Some(345),
3573                        username: None,
3574                        ..Default::default()
3575                    }),
3576                ),
3577            ]
3578            .into_iter()
3579            .collect::<HashMap<_, _>>(),
3580        );
3581    }
3582
3583    #[gpui::test]
3584    async fn test_simple_split() {
3585        zlog::init_test();
3586
3587        let db = WorkspaceDb::open_test_db("simple_split").await;
3588
3589        //  -----------------
3590        //  | 1,2   | 5,6   |
3591        //  | - - - |       |
3592        //  | 3,4   |       |
3593        //  -----------------
3594        let center_pane = group(
3595            Axis::Horizontal,
3596            vec![
3597                group(
3598                    Axis::Vertical,
3599                    vec![
3600                        SerializedPaneGroup::Pane(SerializedPane::new(
3601                            vec![
3602                                SerializedItem::new("Terminal", 1, false, false),
3603                                SerializedItem::new("Terminal", 2, true, false),
3604                            ],
3605                            false,
3606                            0,
3607                        )),
3608                        SerializedPaneGroup::Pane(SerializedPane::new(
3609                            vec![
3610                                SerializedItem::new("Terminal", 4, false, false),
3611                                SerializedItem::new("Terminal", 3, true, false),
3612                            ],
3613                            true,
3614                            0,
3615                        )),
3616                    ],
3617                ),
3618                SerializedPaneGroup::Pane(SerializedPane::new(
3619                    vec![
3620                        SerializedItem::new("Terminal", 5, true, false),
3621                        SerializedItem::new("Terminal", 6, false, false),
3622                    ],
3623                    false,
3624                    0,
3625                )),
3626            ],
3627        );
3628
3629        let workspace = default_workspace(&["/tmp"], &center_pane);
3630
3631        db.save_workspace(workspace.clone()).await;
3632
3633        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
3634
3635        assert_eq!(workspace.center_group, new_workspace.center_group);
3636    }
3637
3638    #[gpui::test]
3639    async fn test_cleanup_panes() {
3640        zlog::init_test();
3641
3642        let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
3643
3644        let center_pane = group(
3645            Axis::Horizontal,
3646            vec![
3647                group(
3648                    Axis::Vertical,
3649                    vec![
3650                        SerializedPaneGroup::Pane(SerializedPane::new(
3651                            vec![
3652                                SerializedItem::new("Terminal", 1, false, false),
3653                                SerializedItem::new("Terminal", 2, true, false),
3654                            ],
3655                            false,
3656                            0,
3657                        )),
3658                        SerializedPaneGroup::Pane(SerializedPane::new(
3659                            vec![
3660                                SerializedItem::new("Terminal", 4, false, false),
3661                                SerializedItem::new("Terminal", 3, true, false),
3662                            ],
3663                            true,
3664                            0,
3665                        )),
3666                    ],
3667                ),
3668                SerializedPaneGroup::Pane(SerializedPane::new(
3669                    vec![
3670                        SerializedItem::new("Terminal", 5, false, false),
3671                        SerializedItem::new("Terminal", 6, true, false),
3672                    ],
3673                    false,
3674                    0,
3675                )),
3676            ],
3677        );
3678
3679        let id = &["/tmp"];
3680
3681        let mut workspace = default_workspace(id, &center_pane);
3682
3683        db.save_workspace(workspace.clone()).await;
3684
3685        workspace.center_group = group(
3686            Axis::Vertical,
3687            vec![
3688                SerializedPaneGroup::Pane(SerializedPane::new(
3689                    vec![
3690                        SerializedItem::new("Terminal", 1, false, false),
3691                        SerializedItem::new("Terminal", 2, true, false),
3692                    ],
3693                    false,
3694                    0,
3695                )),
3696                SerializedPaneGroup::Pane(SerializedPane::new(
3697                    vec![
3698                        SerializedItem::new("Terminal", 4, true, false),
3699                        SerializedItem::new("Terminal", 3, false, false),
3700                    ],
3701                    true,
3702                    0,
3703                )),
3704            ],
3705        );
3706
3707        db.save_workspace(workspace.clone()).await;
3708
3709        let new_workspace = db.workspace_for_roots(id).unwrap();
3710
3711        assert_eq!(workspace.center_group, new_workspace.center_group);
3712    }
3713
3714    #[gpui::test]
3715    async fn test_empty_workspace_window_bounds() {
3716        zlog::init_test();
3717
3718        let db = WorkspaceDb::open_test_db("test_empty_workspace_window_bounds").await;
3719        let id = db.next_id().await.unwrap();
3720
3721        // Create a workspace with empty paths (empty workspace)
3722        let empty_paths: &[&str] = &[];
3723        let display_uuid = Uuid::new_v4();
3724        let window_bounds = SerializedWindowBounds(WindowBounds::Windowed(Bounds {
3725            origin: point(px(100.0), px(200.0)),
3726            size: size(px(800.0), px(600.0)),
3727        }));
3728
3729        let workspace = SerializedWorkspace {
3730            id,
3731            paths: PathList::new(empty_paths),
3732            location: SerializedWorkspaceLocation::Local,
3733            center_group: Default::default(),
3734            window_bounds: None,
3735            display: None,
3736            docks: Default::default(),
3737            breakpoints: Default::default(),
3738            centered_layout: false,
3739            session_id: None,
3740            window_id: None,
3741            user_toolchains: Default::default(),
3742        };
3743
3744        // Save the workspace (this creates the record with empty paths)
3745        db.save_workspace(workspace.clone()).await;
3746
3747        // Save window bounds separately (as the actual code does via set_window_open_status)
3748        db.set_window_open_status(id, window_bounds, display_uuid)
3749            .await
3750            .unwrap();
3751
3752        // Empty workspaces cannot be retrieved by paths (they'd all match).
3753        // They must be retrieved by workspace_id.
3754        assert!(db.workspace_for_roots(empty_paths).is_none());
3755
3756        // Retrieve using workspace_for_id instead
3757        let retrieved = db.workspace_for_id(id).unwrap();
3758
3759        // Verify window bounds were persisted
3760        assert_eq!(retrieved.id, id);
3761        assert!(retrieved.window_bounds.is_some());
3762        assert_eq!(retrieved.window_bounds.unwrap().0, window_bounds.0);
3763        assert!(retrieved.display.is_some());
3764        assert_eq!(retrieved.display.unwrap(), display_uuid);
3765    }
3766
3767    #[gpui::test]
3768    async fn test_last_session_workspace_locations_groups_by_window_id(
3769        cx: &mut gpui::TestAppContext,
3770    ) {
3771        let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
3772        let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
3773        let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
3774        let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
3775        let dir5 = tempfile::TempDir::with_prefix("dir5").unwrap();
3776
3777        let fs = fs::FakeFs::new(cx.executor());
3778        fs.insert_tree(dir1.path(), json!({})).await;
3779        fs.insert_tree(dir2.path(), json!({})).await;
3780        fs.insert_tree(dir3.path(), json!({})).await;
3781        fs.insert_tree(dir4.path(), json!({})).await;
3782        fs.insert_tree(dir5.path(), json!({})).await;
3783
3784        let db =
3785            WorkspaceDb::open_test_db("test_last_session_workspace_locations_groups_by_window_id")
3786                .await;
3787
3788        // Simulate two MultiWorkspace windows each containing two workspaces,
3789        // plus one single-workspace window:
3790        //   Window 10: workspace 1, workspace 2
3791        //   Window 20: workspace 3, workspace 4
3792        //   Window 30: workspace 5 (only one)
3793        //
3794        // On session restore, the caller should be able to group these by
3795        // window_id to reconstruct the MultiWorkspace windows.
3796        let workspaces_data: Vec<(i64, &Path, u64)> = vec![
3797            (1, dir1.path(), 10),
3798            (2, dir2.path(), 10),
3799            (3, dir3.path(), 20),
3800            (4, dir4.path(), 20),
3801            (5, dir5.path(), 30),
3802        ];
3803
3804        for (id, dir, window_id) in &workspaces_data {
3805            db.save_workspace(SerializedWorkspace {
3806                id: WorkspaceId(*id),
3807                paths: PathList::new(&[*dir]),
3808                location: SerializedWorkspaceLocation::Local,
3809                center_group: Default::default(),
3810                window_bounds: Default::default(),
3811                display: Default::default(),
3812                docks: Default::default(),
3813                centered_layout: false,
3814                session_id: Some("test-session".to_owned()),
3815                breakpoints: Default::default(),
3816                window_id: Some(*window_id),
3817                user_toolchains: Default::default(),
3818            })
3819            .await;
3820        }
3821
3822        let locations = db
3823            .last_session_workspace_locations("test-session", None, fs.as_ref())
3824            .await
3825            .unwrap();
3826
3827        // All 5 workspaces should be returned with their window_ids.
3828        assert_eq!(locations.len(), 5);
3829
3830        // Every entry should have a window_id so the caller can group them.
3831        for session_workspace in &locations {
3832            assert!(
3833                session_workspace.window_id.is_some(),
3834                "workspace {:?} missing window_id",
3835                session_workspace.workspace_id
3836            );
3837        }
3838
3839        // Group by window_id, simulating what the restoration code should do.
3840        let mut by_window: HashMap<WindowId, Vec<WorkspaceId>> = HashMap::default();
3841        for session_workspace in &locations {
3842            if let Some(window_id) = session_workspace.window_id {
3843                by_window
3844                    .entry(window_id)
3845                    .or_default()
3846                    .push(session_workspace.workspace_id);
3847            }
3848        }
3849
3850        // Should produce 3 windows, not 5.
3851        assert_eq!(
3852            by_window.len(),
3853            3,
3854            "Expected 3 window groups, got {}: {:?}",
3855            by_window.len(),
3856            by_window
3857        );
3858
3859        // Window 10 should contain workspaces 1 and 2.
3860        let window_10 = by_window.get(&WindowId::from(10u64)).unwrap();
3861        assert_eq!(window_10.len(), 2);
3862        assert!(window_10.contains(&WorkspaceId(1)));
3863        assert!(window_10.contains(&WorkspaceId(2)));
3864
3865        // Window 20 should contain workspaces 3 and 4.
3866        let window_20 = by_window.get(&WindowId::from(20u64)).unwrap();
3867        assert_eq!(window_20.len(), 2);
3868        assert!(window_20.contains(&WorkspaceId(3)));
3869        assert!(window_20.contains(&WorkspaceId(4)));
3870
3871        // Window 30 should contain only workspace 5.
3872        let window_30 = by_window.get(&WindowId::from(30u64)).unwrap();
3873        assert_eq!(window_30.len(), 1);
3874        assert!(window_30.contains(&WorkspaceId(5)));
3875    }
3876
3877    #[gpui::test]
3878    async fn test_read_serialized_multi_workspaces_with_state() {
3879        use crate::persistence::model::MultiWorkspaceState;
3880
3881        // Write multi-workspace state for two windows via the scoped KVP.
3882        let window_10 = WindowId::from(10u64);
3883        let window_20 = WindowId::from(20u64);
3884
3885        write_multi_workspace_state(
3886            window_10,
3887            MultiWorkspaceState {
3888                active_workspace_id: Some(WorkspaceId(2)),
3889            },
3890        )
3891        .await;
3892
3893        write_multi_workspace_state(
3894            window_20,
3895            MultiWorkspaceState {
3896                active_workspace_id: Some(WorkspaceId(3)),
3897            },
3898        )
3899        .await;
3900
3901        // Build session workspaces: two in window 10, one in window 20, one with no window.
3902        let session_workspaces = vec![
3903            SessionWorkspace {
3904                workspace_id: WorkspaceId(1),
3905                location: SerializedWorkspaceLocation::Local,
3906                paths: PathList::new(&["/a"]),
3907                window_id: Some(window_10),
3908            },
3909            SessionWorkspace {
3910                workspace_id: WorkspaceId(2),
3911                location: SerializedWorkspaceLocation::Local,
3912                paths: PathList::new(&["/b"]),
3913                window_id: Some(window_10),
3914            },
3915            SessionWorkspace {
3916                workspace_id: WorkspaceId(3),
3917                location: SerializedWorkspaceLocation::Local,
3918                paths: PathList::new(&["/c"]),
3919                window_id: Some(window_20),
3920            },
3921            SessionWorkspace {
3922                workspace_id: WorkspaceId(4),
3923                location: SerializedWorkspaceLocation::Local,
3924                paths: PathList::new(&["/d"]),
3925                window_id: None,
3926            },
3927        ];
3928
3929        let results = read_serialized_multi_workspaces(session_workspaces);
3930
3931        // Should produce 3 groups: window 10, window 20, and the orphan.
3932        assert_eq!(results.len(), 3);
3933
3934        // Window 10 group: 2 workspaces, active_workspace_id = 2.
3935        let group_10 = &results[0];
3936        assert_eq!(group_10.workspaces.len(), 2);
3937        assert_eq!(group_10.state.active_workspace_id, Some(WorkspaceId(2)));
3938
3939        // Window 20 group: 1 workspace, active_workspace_id = 3.
3940        let group_20 = &results[1];
3941        assert_eq!(group_20.workspaces.len(), 1);
3942        assert_eq!(group_20.state.active_workspace_id, Some(WorkspaceId(3)));
3943
3944        // Orphan group: no window_id, so state is default.
3945        let group_none = &results[2];
3946        assert_eq!(group_none.workspaces.len(), 1);
3947        assert_eq!(group_none.state.active_workspace_id, None);
3948    }
3949
3950    #[gpui::test]
3951    async fn test_flush_serialization_completes_before_quit(cx: &mut gpui::TestAppContext) {
3952        use crate::multi_workspace::MultiWorkspace;
3953        use feature_flags::FeatureFlagAppExt;
3954
3955        use project::Project;
3956
3957        crate::tests::init_test(cx);
3958
3959        cx.update(|cx| {
3960            cx.set_staff(true);
3961            cx.update_flags(true, vec!["agent-v2".to_string()]);
3962        });
3963
3964        let fs = fs::FakeFs::new(cx.executor());
3965        let project = Project::test(fs.clone(), [], cx).await;
3966
3967        let (multi_workspace, cx) =
3968            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3969
3970        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3971
3972        // Assign a database_id so serialization will actually persist.
3973        let workspace_id = DB.next_id().await.unwrap();
3974        workspace.update(cx, |ws, _cx| {
3975            ws.set_database_id(workspace_id);
3976        });
3977
3978        // Mutate some workspace state.
3979        DB.set_centered_layout(workspace_id, true).await.unwrap();
3980
3981        // Call flush_serialization and await the returned task directly
3982        // (without run_until_parked — the point is that awaiting the task
3983        // alone is sufficient).
3984        let task = multi_workspace.update_in(cx, |mw, window, cx| {
3985            mw.workspace()
3986                .update(cx, |ws, cx| ws.flush_serialization(window, cx))
3987        });
3988        task.await;
3989
3990        // Read the workspace back from the DB and verify serialization happened.
3991        let serialized = DB.workspace_for_id(workspace_id);
3992        assert!(
3993            serialized.is_some(),
3994            "flush_serialization should have persisted the workspace to DB"
3995        );
3996    }
3997
3998    #[gpui::test]
3999    async fn test_create_workspace_serializes_active_workspace_id_after_db_id_assigned(
4000        cx: &mut gpui::TestAppContext,
4001    ) {
4002        use crate::multi_workspace::MultiWorkspace;
4003        use crate::persistence::read_multi_workspace_state;
4004        use feature_flags::FeatureFlagAppExt;
4005
4006        use project::Project;
4007
4008        crate::tests::init_test(cx);
4009
4010        cx.update(|cx| {
4011            cx.set_staff(true);
4012            cx.update_flags(true, vec!["agent-v2".to_string()]);
4013        });
4014
4015        let fs = fs::FakeFs::new(cx.executor());
4016        let project = Project::test(fs.clone(), [], cx).await;
4017
4018        let (multi_workspace, cx) =
4019            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4020
4021        // Give the first workspace a database_id.
4022        multi_workspace.update_in(cx, |mw, _, cx| {
4023            mw.set_random_database_id(cx);
4024        });
4025
4026        let window_id =
4027            multi_workspace.update_in(cx, |_, window, _cx| window.window_handle().window_id());
4028
4029        // Create a new workspace via the MultiWorkspace API (triggers next_id()).
4030        multi_workspace.update_in(cx, |mw, window, cx| {
4031            mw.create_workspace(window, cx);
4032        });
4033
4034        // Let the async next_id() and re-serialization tasks complete.
4035        cx.run_until_parked();
4036
4037        // Read back the multi-workspace state.
4038        let state = read_multi_workspace_state(window_id);
4039
4040        // The new workspace should now have a database_id, and the multi-workspace
4041        // state should record it as the active workspace.
4042        let new_workspace_db_id =
4043            multi_workspace.read_with(cx, |mw, cx| mw.workspace().read(cx).database_id());
4044        assert!(
4045            new_workspace_db_id.is_some(),
4046            "New workspace should have a database_id after run_until_parked"
4047        );
4048        assert_eq!(
4049            state.active_workspace_id, new_workspace_db_id,
4050            "Serialized active_workspace_id should match the new workspace's database_id"
4051        );
4052    }
4053
4054    #[gpui::test]
4055    async fn test_create_workspace_individual_serialization(cx: &mut gpui::TestAppContext) {
4056        use crate::multi_workspace::MultiWorkspace;
4057        use feature_flags::FeatureFlagAppExt;
4058
4059        use project::Project;
4060
4061        crate::tests::init_test(cx);
4062
4063        cx.update(|cx| {
4064            cx.set_staff(true);
4065            cx.update_flags(true, vec!["agent-v2".to_string()]);
4066        });
4067
4068        let fs = fs::FakeFs::new(cx.executor());
4069        let project = Project::test(fs.clone(), [], cx).await;
4070
4071        let (multi_workspace, cx) =
4072            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4073
4074        multi_workspace.update_in(cx, |mw, _, cx| {
4075            mw.set_random_database_id(cx);
4076        });
4077
4078        // Create a new workspace.
4079        multi_workspace.update_in(cx, |mw, window, cx| {
4080            mw.create_workspace(window, cx);
4081        });
4082
4083        cx.run_until_parked();
4084
4085        // Get the new workspace's database_id.
4086        let new_db_id =
4087            multi_workspace.read_with(cx, |mw, cx| mw.workspace().read(cx).database_id());
4088        assert!(
4089            new_db_id.is_some(),
4090            "New workspace should have a database_id"
4091        );
4092
4093        let workspace_id = new_db_id.unwrap();
4094
4095        // The workspace should have been serialized to the DB with real data
4096        // (not just the bare DEFAULT VALUES row from next_id).
4097        let serialized = DB.workspace_for_id(workspace_id);
4098        assert!(
4099            serialized.is_some(),
4100            "Newly created workspace should be fully serialized in the DB after database_id assignment"
4101        );
4102    }
4103
4104    #[gpui::test]
4105    async fn test_remove_workspace_deletes_db_row(cx: &mut gpui::TestAppContext) {
4106        use crate::multi_workspace::MultiWorkspace;
4107        use feature_flags::FeatureFlagAppExt;
4108        use gpui::AppContext as _;
4109        use project::Project;
4110
4111        crate::tests::init_test(cx);
4112
4113        cx.update(|cx| {
4114            cx.set_staff(true);
4115            cx.update_flags(true, vec!["agent-v2".to_string()]);
4116        });
4117
4118        let fs = fs::FakeFs::new(cx.executor());
4119        let project1 = Project::test(fs.clone(), [], cx).await;
4120        let project2 = Project::test(fs.clone(), [], cx).await;
4121
4122        let (multi_workspace, cx) =
4123            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
4124
4125        multi_workspace.update_in(cx, |mw, _, cx| {
4126            mw.set_random_database_id(cx);
4127        });
4128
4129        // Get a real DB id for workspace2 so the row actually exists.
4130        let workspace2_db_id = DB.next_id().await.unwrap();
4131
4132        multi_workspace.update_in(cx, |mw, window, cx| {
4133            let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx));
4134            workspace.update(cx, |ws: &mut crate::Workspace, _cx| {
4135                ws.set_database_id(workspace2_db_id)
4136            });
4137            mw.activate(workspace.clone(), cx);
4138        });
4139
4140        // Save a full workspace row to the DB directly.
4141        DB.save_workspace(SerializedWorkspace {
4142            id: workspace2_db_id,
4143            paths: PathList::new(&["/tmp/remove_test"]),
4144            location: SerializedWorkspaceLocation::Local,
4145            center_group: Default::default(),
4146            window_bounds: Default::default(),
4147            display: Default::default(),
4148            docks: Default::default(),
4149            centered_layout: false,
4150            session_id: Some("remove-test-session".to_owned()),
4151            breakpoints: Default::default(),
4152            window_id: Some(99),
4153            user_toolchains: Default::default(),
4154        })
4155        .await;
4156
4157        assert!(
4158            DB.workspace_for_id(workspace2_db_id).is_some(),
4159            "Workspace2 should exist in DB before removal"
4160        );
4161
4162        // Remove workspace at index 1 (the second workspace).
4163        multi_workspace.update_in(cx, |mw, window, cx| {
4164            mw.remove_workspace(1, window, cx);
4165        });
4166
4167        cx.run_until_parked();
4168
4169        // The row should be deleted, not just have session_id cleared.
4170        assert!(
4171            DB.workspace_for_id(workspace2_db_id).is_none(),
4172            "Removed workspace's DB row should be deleted entirely"
4173        );
4174    }
4175
4176    #[gpui::test]
4177    async fn test_remove_workspace_not_restored_as_zombie(cx: &mut gpui::TestAppContext) {
4178        use crate::multi_workspace::MultiWorkspace;
4179        use feature_flags::FeatureFlagAppExt;
4180        use gpui::AppContext as _;
4181        use project::Project;
4182
4183        crate::tests::init_test(cx);
4184
4185        cx.update(|cx| {
4186            cx.set_staff(true);
4187            cx.update_flags(true, vec!["agent-v2".to_string()]);
4188        });
4189
4190        let fs = fs::FakeFs::new(cx.executor());
4191        let dir1 = tempfile::TempDir::with_prefix("zombie_test1").unwrap();
4192        let dir2 = tempfile::TempDir::with_prefix("zombie_test2").unwrap();
4193        fs.insert_tree(dir1.path(), json!({})).await;
4194        fs.insert_tree(dir2.path(), json!({})).await;
4195
4196        let project1 = Project::test(fs.clone(), [], cx).await;
4197        let project2 = Project::test(fs.clone(), [], cx).await;
4198
4199        // Get real DB ids so the rows actually exist.
4200        let ws1_id = DB.next_id().await.unwrap();
4201        let ws2_id = DB.next_id().await.unwrap();
4202
4203        let (multi_workspace, cx) =
4204            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
4205
4206        multi_workspace.update_in(cx, |mw, _, cx| {
4207            mw.workspace().update(cx, |ws, _cx| {
4208                ws.set_database_id(ws1_id);
4209            });
4210        });
4211
4212        multi_workspace.update_in(cx, |mw, window, cx| {
4213            let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx));
4214            workspace.update(cx, |ws: &mut crate::Workspace, _cx| {
4215                ws.set_database_id(ws2_id)
4216            });
4217            mw.activate(workspace.clone(), cx);
4218        });
4219
4220        let session_id = "test-zombie-session";
4221        let window_id_val: u64 = 42;
4222
4223        DB.save_workspace(SerializedWorkspace {
4224            id: ws1_id,
4225            paths: PathList::new(&[dir1.path()]),
4226            location: SerializedWorkspaceLocation::Local,
4227            center_group: Default::default(),
4228            window_bounds: Default::default(),
4229            display: Default::default(),
4230            docks: Default::default(),
4231            centered_layout: false,
4232            session_id: Some(session_id.to_owned()),
4233            breakpoints: Default::default(),
4234            window_id: Some(window_id_val),
4235            user_toolchains: Default::default(),
4236        })
4237        .await;
4238
4239        DB.save_workspace(SerializedWorkspace {
4240            id: ws2_id,
4241            paths: PathList::new(&[dir2.path()]),
4242            location: SerializedWorkspaceLocation::Local,
4243            center_group: Default::default(),
4244            window_bounds: Default::default(),
4245            display: Default::default(),
4246            docks: Default::default(),
4247            centered_layout: false,
4248            session_id: Some(session_id.to_owned()),
4249            breakpoints: Default::default(),
4250            window_id: Some(window_id_val),
4251            user_toolchains: Default::default(),
4252        })
4253        .await;
4254
4255        // Remove workspace2 (index 1).
4256        multi_workspace.update_in(cx, |mw, window, cx| {
4257            mw.remove_workspace(1, window, cx);
4258        });
4259
4260        cx.run_until_parked();
4261
4262        // The removed workspace should NOT appear in session restoration.
4263        let locations = DB
4264            .last_session_workspace_locations(session_id, None, fs.as_ref())
4265            .await
4266            .unwrap();
4267
4268        let restored_ids: Vec<WorkspaceId> = locations.iter().map(|sw| sw.workspace_id).collect();
4269        assert!(
4270            !restored_ids.contains(&ws2_id),
4271            "Removed workspace should not appear in session restoration list. Found: {:?}",
4272            restored_ids
4273        );
4274        assert!(
4275            restored_ids.contains(&ws1_id),
4276            "Remaining workspace should still appear in session restoration list"
4277        );
4278    }
4279
4280    #[gpui::test]
4281    async fn test_pending_removal_tasks_drained_on_flush(cx: &mut gpui::TestAppContext) {
4282        use crate::multi_workspace::MultiWorkspace;
4283        use feature_flags::FeatureFlagAppExt;
4284        use gpui::AppContext as _;
4285        use project::Project;
4286
4287        crate::tests::init_test(cx);
4288
4289        cx.update(|cx| {
4290            cx.set_staff(true);
4291            cx.update_flags(true, vec!["agent-v2".to_string()]);
4292        });
4293
4294        let fs = fs::FakeFs::new(cx.executor());
4295        let project1 = Project::test(fs.clone(), [], cx).await;
4296        let project2 = Project::test(fs.clone(), [], cx).await;
4297
4298        // Get a real DB id for workspace2 so the row actually exists.
4299        let workspace2_db_id = DB.next_id().await.unwrap();
4300
4301        let (multi_workspace, cx) =
4302            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
4303
4304        multi_workspace.update_in(cx, |mw, _, cx| {
4305            mw.set_random_database_id(cx);
4306        });
4307
4308        multi_workspace.update_in(cx, |mw, window, cx| {
4309            let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx));
4310            workspace.update(cx, |ws: &mut crate::Workspace, _cx| {
4311                ws.set_database_id(workspace2_db_id)
4312            });
4313            mw.activate(workspace.clone(), cx);
4314        });
4315
4316        // Save a full workspace row to the DB directly and let it settle.
4317        DB.save_workspace(SerializedWorkspace {
4318            id: workspace2_db_id,
4319            paths: PathList::new(&["/tmp/pending_removal_test"]),
4320            location: SerializedWorkspaceLocation::Local,
4321            center_group: Default::default(),
4322            window_bounds: Default::default(),
4323            display: Default::default(),
4324            docks: Default::default(),
4325            centered_layout: false,
4326            session_id: Some("pending-removal-session".to_owned()),
4327            breakpoints: Default::default(),
4328            window_id: Some(88),
4329            user_toolchains: Default::default(),
4330        })
4331        .await;
4332        cx.run_until_parked();
4333
4334        // Remove workspace2 — this pushes a task to pending_removal_tasks.
4335        multi_workspace.update_in(cx, |mw, window, cx| {
4336            mw.remove_workspace(1, window, cx);
4337        });
4338
4339        // Simulate the quit handler pattern: collect flush tasks + pending
4340        // removal tasks and await them all.
4341        let all_tasks = multi_workspace.update_in(cx, |mw, window, cx| {
4342            let mut tasks: Vec<Task<()>> = mw
4343                .workspaces()
4344                .iter()
4345                .map(|workspace| {
4346                    workspace.update(cx, |workspace, cx| {
4347                        workspace.flush_serialization(window, cx)
4348                    })
4349                })
4350                .collect();
4351            let mut removal_tasks = mw.take_pending_removal_tasks();
4352            // Note: removal_tasks may be empty if the background task already
4353            // completed (take_pending_removal_tasks filters out ready tasks).
4354            tasks.append(&mut removal_tasks);
4355            tasks.push(mw.flush_serialization());
4356            tasks
4357        });
4358        futures::future::join_all(all_tasks).await;
4359
4360        // After awaiting, the DB row should be deleted.
4361        assert!(
4362            DB.workspace_for_id(workspace2_db_id).is_none(),
4363            "Pending removal task should have deleted the workspace row when awaited"
4364        );
4365    }
4366
4367    #[gpui::test]
4368    async fn test_create_workspace_bounds_observer_uses_fresh_id(cx: &mut gpui::TestAppContext) {
4369        use crate::multi_workspace::MultiWorkspace;
4370        use feature_flags::FeatureFlagAppExt;
4371        use project::Project;
4372
4373        crate::tests::init_test(cx);
4374
4375        cx.update(|cx| {
4376            cx.set_staff(true);
4377            cx.update_flags(true, vec!["agent-v2".to_string()]);
4378        });
4379
4380        let fs = fs::FakeFs::new(cx.executor());
4381        let project = Project::test(fs.clone(), [], cx).await;
4382
4383        let (multi_workspace, cx) =
4384            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4385
4386        multi_workspace.update_in(cx, |mw, _, cx| {
4387            mw.set_random_database_id(cx);
4388        });
4389
4390        multi_workspace.update_in(cx, |mw, window, cx| {
4391            mw.create_workspace(window, cx);
4392        });
4393
4394        cx.run_until_parked();
4395
4396        let new_workspace_db_id =
4397            multi_workspace.read_with(cx, |mw, cx| mw.workspace().read(cx).database_id());
4398        assert!(
4399            new_workspace_db_id.is_some(),
4400            "After run_until_parked, the workspace should have a database_id"
4401        );
4402
4403        let workspace_id = new_workspace_db_id.unwrap();
4404
4405        assert!(
4406            DB.workspace_for_id(workspace_id).is_some(),
4407            "The workspace row should exist in the DB"
4408        );
4409
4410        cx.simulate_resize(gpui::size(px(1024.0), px(768.0)));
4411
4412        // Advance the clock past the 100ms debounce timer so the bounds
4413        // observer task fires
4414        cx.executor().advance_clock(Duration::from_millis(200));
4415        cx.run_until_parked();
4416
4417        let serialized = DB
4418            .workspace_for_id(workspace_id)
4419            .expect("workspace row should still exist");
4420        assert!(
4421            serialized.window_bounds.is_some(),
4422            "The bounds observer should write bounds for the workspace's real DB ID, \
4423             even when the workspace was created via create_workspace (where the ID \
4424             is assigned asynchronously after construction)."
4425        );
4426    }
4427
4428    #[gpui::test]
4429    async fn test_flush_serialization_writes_bounds(cx: &mut gpui::TestAppContext) {
4430        use crate::multi_workspace::MultiWorkspace;
4431        use feature_flags::FeatureFlagAppExt;
4432        use project::Project;
4433
4434        crate::tests::init_test(cx);
4435
4436        cx.update(|cx| {
4437            cx.set_staff(true);
4438            cx.update_flags(true, vec!["agent-v2".to_string()]);
4439        });
4440
4441        let fs = fs::FakeFs::new(cx.executor());
4442        let dir = tempfile::TempDir::with_prefix("flush_bounds_test").unwrap();
4443        fs.insert_tree(dir.path(), json!({})).await;
4444
4445        let project = Project::test(fs.clone(), [dir.path()], cx).await;
4446
4447        let (multi_workspace, cx) =
4448            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4449
4450        let workspace_id = DB.next_id().await.unwrap();
4451        multi_workspace.update_in(cx, |mw, _, cx| {
4452            mw.workspace().update(cx, |ws, _cx| {
4453                ws.set_database_id(workspace_id);
4454            });
4455        });
4456
4457        let task = multi_workspace.update_in(cx, |mw, window, cx| {
4458            mw.workspace()
4459                .update(cx, |ws, cx| ws.flush_serialization(window, cx))
4460        });
4461        task.await;
4462
4463        let after = DB
4464            .workspace_for_id(workspace_id)
4465            .expect("workspace row should exist after flush_serialization");
4466        assert!(
4467            !after.paths.is_empty(),
4468            "flush_serialization should have written paths via save_workspace"
4469        );
4470        assert!(
4471            after.window_bounds.is_some(),
4472            "flush_serialization should ensure window bounds are persisted to the DB \
4473             before the process exits."
4474        );
4475    }
4476}