persistence.rs

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