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