persistence.rs

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