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