persistence.rs

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