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