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 anyhow::{Context as _, Result, bail};
  12use collections::{HashMap, HashSet, IndexSet};
  13use db::{
  14    kvp::KEY_VALUE_STORE,
  15    query,
  16    sqlez::{connection::Connection, domain::Domain},
  17    sqlez_macros::sql,
  18};
  19use gpui::{Axis, Bounds, Entity, Task, WindowBounds, WindowId, point, size};
  20use project::{
  21    debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
  22    trusted_worktrees::{PathTrust, RemoteHostLocation, find_worktree_in_store},
  23    worktree_store::WorktreeStore,
  24};
  25
  26use language::{LanguageName, Toolchain, ToolchainScope};
  27use project::WorktreeId;
  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};
  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
 286#[derive(Debug)]
 287pub struct Breakpoint {
 288    pub position: u32,
 289    pub message: Option<Arc<str>>,
 290    pub condition: Option<Arc<str>>,
 291    pub hit_condition: Option<Arc<str>>,
 292    pub state: BreakpointState,
 293}
 294
 295/// Wrapper for DB type of a breakpoint
 296struct BreakpointStateWrapper<'a>(Cow<'a, BreakpointState>);
 297
 298impl From<BreakpointState> for BreakpointStateWrapper<'static> {
 299    fn from(kind: BreakpointState) -> Self {
 300        BreakpointStateWrapper(Cow::Owned(kind))
 301    }
 302}
 303
 304impl StaticColumnCount for BreakpointStateWrapper<'_> {
 305    fn column_count() -> usize {
 306        1
 307    }
 308}
 309
 310impl Bind for BreakpointStateWrapper<'_> {
 311    fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
 312        statement.bind(&self.0.to_int(), start_index)
 313    }
 314}
 315
 316impl Column for BreakpointStateWrapper<'_> {
 317    fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
 318        let state = statement.column_int(start_index)?;
 319
 320        match state {
 321            0 => Ok((BreakpointState::Enabled.into(), start_index + 1)),
 322            1 => Ok((BreakpointState::Disabled.into(), start_index + 1)),
 323            _ => anyhow::bail!("Invalid BreakpointState discriminant {state}"),
 324        }
 325    }
 326}
 327
 328impl sqlez::bindable::StaticColumnCount for Breakpoint {
 329    fn column_count() -> usize {
 330        // Position, log message, condition message, and hit condition message
 331        4 + BreakpointStateWrapper::column_count()
 332    }
 333}
 334
 335impl sqlez::bindable::Bind for Breakpoint {
 336    fn bind(
 337        &self,
 338        statement: &sqlez::statement::Statement,
 339        start_index: i32,
 340    ) -> anyhow::Result<i32> {
 341        let next_index = statement.bind(&self.position, start_index)?;
 342        let next_index = statement.bind(&self.message, next_index)?;
 343        let next_index = statement.bind(&self.condition, next_index)?;
 344        let next_index = statement.bind(&self.hit_condition, next_index)?;
 345        statement.bind(
 346            &BreakpointStateWrapper(Cow::Borrowed(&self.state)),
 347            next_index,
 348        )
 349    }
 350}
 351
 352impl Column for Breakpoint {
 353    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 354        let position = statement
 355            .column_int(start_index)
 356            .with_context(|| format!("Failed to read BreakPoint at index {start_index}"))?
 357            as u32;
 358        let (message, next_index) = Option::<String>::column(statement, start_index + 1)?;
 359        let (condition, next_index) = Option::<String>::column(statement, next_index)?;
 360        let (hit_condition, next_index) = Option::<String>::column(statement, next_index)?;
 361        let (state, next_index) = BreakpointStateWrapper::column(statement, next_index)?;
 362
 363        Ok((
 364            Breakpoint {
 365                position,
 366                message: message.map(Arc::from),
 367                condition: condition.map(Arc::from),
 368                hit_condition: hit_condition.map(Arc::from),
 369                state: state.0.into_owned(),
 370            },
 371            next_index,
 372        ))
 373    }
 374}
 375
 376#[derive(Clone, Debug, PartialEq)]
 377struct SerializedPixels(gpui::Pixels);
 378impl sqlez::bindable::StaticColumnCount for SerializedPixels {}
 379
 380impl sqlez::bindable::Bind for SerializedPixels {
 381    fn bind(
 382        &self,
 383        statement: &sqlez::statement::Statement,
 384        start_index: i32,
 385    ) -> anyhow::Result<i32> {
 386        let this: i32 = u32::from(self.0) as _;
 387        this.bind(statement, start_index)
 388    }
 389}
 390
 391pub struct WorkspaceDb(ThreadSafeConnection);
 392
 393impl Domain for WorkspaceDb {
 394    const NAME: &str = stringify!(WorkspaceDb);
 395
 396    const MIGRATIONS: &[&str] = &[
 397        sql!(
 398            CREATE TABLE workspaces(
 399                workspace_id INTEGER PRIMARY KEY,
 400                workspace_location BLOB UNIQUE,
 401                dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
 402                dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
 403                dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
 404                left_sidebar_open INTEGER, // Boolean
 405                timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 406                FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
 407            ) STRICT;
 408
 409            CREATE TABLE pane_groups(
 410                group_id INTEGER PRIMARY KEY,
 411                workspace_id INTEGER NOT NULL,
 412                parent_group_id INTEGER, // NULL indicates that this is a root node
 413                position INTEGER, // NULL indicates that this is a root node
 414                axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
 415                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 416                ON DELETE CASCADE
 417                ON UPDATE CASCADE,
 418                FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 419            ) STRICT;
 420
 421            CREATE TABLE panes(
 422                pane_id INTEGER PRIMARY KEY,
 423                workspace_id INTEGER NOT NULL,
 424                active INTEGER NOT NULL, // Boolean
 425                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 426                ON DELETE CASCADE
 427                ON UPDATE CASCADE
 428            ) STRICT;
 429
 430            CREATE TABLE center_panes(
 431                pane_id INTEGER PRIMARY KEY,
 432                parent_group_id INTEGER, // NULL means that this is a root pane
 433                position INTEGER, // NULL means that this is a root pane
 434                FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 435                ON DELETE CASCADE,
 436                FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 437            ) STRICT;
 438
 439            CREATE TABLE items(
 440                item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
 441                workspace_id INTEGER NOT NULL,
 442                pane_id INTEGER NOT NULL,
 443                kind TEXT NOT NULL,
 444                position INTEGER NOT NULL,
 445                active INTEGER NOT NULL,
 446                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 447                ON DELETE CASCADE
 448                ON UPDATE CASCADE,
 449                FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 450                ON DELETE CASCADE,
 451                PRIMARY KEY(item_id, workspace_id)
 452            ) STRICT;
 453        ),
 454        sql!(
 455            ALTER TABLE workspaces ADD COLUMN window_state TEXT;
 456            ALTER TABLE workspaces ADD COLUMN window_x REAL;
 457            ALTER TABLE workspaces ADD COLUMN window_y REAL;
 458            ALTER TABLE workspaces ADD COLUMN window_width REAL;
 459            ALTER TABLE workspaces ADD COLUMN window_height REAL;
 460            ALTER TABLE workspaces ADD COLUMN display BLOB;
 461        ),
 462        // Drop foreign key constraint from workspaces.dock_pane to panes table.
 463        sql!(
 464            CREATE TABLE workspaces_2(
 465                workspace_id INTEGER PRIMARY KEY,
 466                workspace_location BLOB UNIQUE,
 467                dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
 468                dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
 469                dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
 470                left_sidebar_open INTEGER, // Boolean
 471                timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 472                window_state TEXT,
 473                window_x REAL,
 474                window_y REAL,
 475                window_width REAL,
 476                window_height REAL,
 477                display BLOB
 478            ) STRICT;
 479            INSERT INTO workspaces_2 SELECT * FROM workspaces;
 480            DROP TABLE workspaces;
 481            ALTER TABLE workspaces_2 RENAME TO workspaces;
 482        ),
 483        // Add panels related information
 484        sql!(
 485            ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
 486            ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
 487            ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
 488            ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
 489            ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
 490            ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
 491        ),
 492        // Add panel zoom persistence
 493        sql!(
 494            ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
 495            ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
 496            ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
 497        ),
 498        // Add pane group flex data
 499        sql!(
 500            ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
 501        ),
 502        // Add fullscreen field to workspace
 503        // Deprecated, `WindowBounds` holds the fullscreen state now.
 504        // Preserving so users can downgrade Zed.
 505        sql!(
 506            ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
 507        ),
 508        // Add preview field to items
 509        sql!(
 510            ALTER TABLE items ADD COLUMN preview INTEGER; //bool
 511        ),
 512        // Add centered_layout field to workspace
 513        sql!(
 514            ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
 515        ),
 516        sql!(
 517            CREATE TABLE remote_projects (
 518                remote_project_id INTEGER NOT NULL UNIQUE,
 519                path TEXT,
 520                dev_server_name TEXT
 521            );
 522            ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
 523            ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
 524        ),
 525        sql!(
 526            DROP TABLE remote_projects;
 527            CREATE TABLE dev_server_projects (
 528                id INTEGER NOT NULL UNIQUE,
 529                path TEXT,
 530                dev_server_name TEXT
 531            );
 532            ALTER TABLE workspaces DROP COLUMN remote_project_id;
 533            ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
 534        ),
 535        sql!(
 536            ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
 537        ),
 538        sql!(
 539            ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
 540        ),
 541        sql!(
 542            ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
 543        ),
 544        sql!(
 545            ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
 546        ),
 547        sql!(
 548            CREATE TABLE ssh_projects (
 549                id INTEGER PRIMARY KEY,
 550                host TEXT NOT NULL,
 551                port INTEGER,
 552                path TEXT NOT NULL,
 553                user TEXT
 554            );
 555            ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
 556        ),
 557        sql!(
 558            ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
 559        ),
 560        sql!(
 561            CREATE TABLE toolchains (
 562                workspace_id INTEGER,
 563                worktree_id INTEGER,
 564                language_name TEXT NOT NULL,
 565                name TEXT NOT NULL,
 566                path TEXT NOT NULL,
 567                PRIMARY KEY (workspace_id, worktree_id, language_name)
 568            );
 569        ),
 570        sql!(
 571            ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}";
 572        ),
 573        sql!(
 574            CREATE TABLE breakpoints (
 575                workspace_id INTEGER NOT NULL,
 576                path TEXT NOT NULL,
 577                breakpoint_location INTEGER NOT NULL,
 578                kind INTEGER NOT NULL,
 579                log_message TEXT,
 580                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 581                ON DELETE CASCADE
 582                ON UPDATE CASCADE
 583            );
 584        ),
 585        sql!(
 586            ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT;
 587            CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array);
 588            ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT;
 589        ),
 590        sql!(
 591            ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL
 592        ),
 593        sql!(
 594            ALTER TABLE breakpoints DROP COLUMN kind
 595        ),
 596        sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL),
 597        sql!(
 598            ALTER TABLE breakpoints ADD COLUMN condition TEXT;
 599            ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT;
 600        ),
 601        sql!(CREATE TABLE toolchains2 (
 602            workspace_id INTEGER,
 603            worktree_id INTEGER,
 604            language_name TEXT NOT NULL,
 605            name TEXT NOT NULL,
 606            path TEXT NOT NULL,
 607            raw_json TEXT NOT NULL,
 608            relative_worktree_path TEXT NOT NULL,
 609            PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT;
 610            INSERT INTO toolchains2
 611                SELECT * FROM toolchains;
 612            DROP TABLE toolchains;
 613            ALTER TABLE toolchains2 RENAME TO toolchains;
 614        ),
 615        sql!(
 616            CREATE TABLE ssh_connections (
 617                id INTEGER PRIMARY KEY,
 618                host TEXT NOT NULL,
 619                port INTEGER,
 620                user TEXT
 621            );
 622
 623            INSERT INTO ssh_connections (host, port, user)
 624            SELECT DISTINCT host, port, user
 625            FROM ssh_projects;
 626
 627            CREATE TABLE workspaces_2(
 628                workspace_id INTEGER PRIMARY KEY,
 629                paths TEXT,
 630                paths_order TEXT,
 631                ssh_connection_id INTEGER REFERENCES ssh_connections(id),
 632                timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 633                window_state TEXT,
 634                window_x REAL,
 635                window_y REAL,
 636                window_width REAL,
 637                window_height REAL,
 638                display BLOB,
 639                left_dock_visible INTEGER,
 640                left_dock_active_panel TEXT,
 641                right_dock_visible INTEGER,
 642                right_dock_active_panel TEXT,
 643                bottom_dock_visible INTEGER,
 644                bottom_dock_active_panel TEXT,
 645                left_dock_zoom INTEGER,
 646                right_dock_zoom INTEGER,
 647                bottom_dock_zoom INTEGER,
 648                fullscreen INTEGER,
 649                centered_layout INTEGER,
 650                session_id TEXT,
 651                window_id INTEGER
 652            ) STRICT;
 653
 654            INSERT
 655            INTO workspaces_2
 656            SELECT
 657                workspaces.workspace_id,
 658                CASE
 659                    WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths
 660                    ELSE
 661                        CASE
 662                            WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN
 663                                NULL
 664                            ELSE
 665                                replace(workspaces.local_paths_array, ',', CHAR(10))
 666                        END
 667                END as paths,
 668
 669                CASE
 670                    WHEN ssh_projects.id IS NOT NULL THEN ""
 671                    ELSE workspaces.local_paths_order_array
 672                END as paths_order,
 673
 674                CASE
 675                    WHEN ssh_projects.id IS NOT NULL THEN (
 676                        SELECT ssh_connections.id
 677                        FROM ssh_connections
 678                        WHERE
 679                            ssh_connections.host IS ssh_projects.host AND
 680                            ssh_connections.port IS ssh_projects.port AND
 681                            ssh_connections.user IS ssh_projects.user
 682                    )
 683                    ELSE NULL
 684                END as ssh_connection_id,
 685
 686                workspaces.timestamp,
 687                workspaces.window_state,
 688                workspaces.window_x,
 689                workspaces.window_y,
 690                workspaces.window_width,
 691                workspaces.window_height,
 692                workspaces.display,
 693                workspaces.left_dock_visible,
 694                workspaces.left_dock_active_panel,
 695                workspaces.right_dock_visible,
 696                workspaces.right_dock_active_panel,
 697                workspaces.bottom_dock_visible,
 698                workspaces.bottom_dock_active_panel,
 699                workspaces.left_dock_zoom,
 700                workspaces.right_dock_zoom,
 701                workspaces.bottom_dock_zoom,
 702                workspaces.fullscreen,
 703                workspaces.centered_layout,
 704                workspaces.session_id,
 705                workspaces.window_id
 706            FROM
 707                workspaces LEFT JOIN
 708                ssh_projects ON
 709                workspaces.ssh_project_id = ssh_projects.id;
 710
 711            DELETE FROM workspaces_2
 712            WHERE workspace_id NOT IN (
 713                SELECT MAX(workspace_id)
 714                FROM workspaces_2
 715                GROUP BY ssh_connection_id, paths
 716            );
 717
 718            DROP TABLE ssh_projects;
 719            DROP TABLE workspaces;
 720            ALTER TABLE workspaces_2 RENAME TO workspaces;
 721
 722            CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths);
 723        ),
 724        // Fix any data from when workspaces.paths were briefly encoded as JSON arrays
 725        sql!(
 726            UPDATE workspaces
 727            SET paths = CASE
 728                WHEN substr(paths, 1, 2) = '[' || '"' AND substr(paths, -2, 2) = '"' || ']' THEN
 729                    replace(
 730                        substr(paths, 3, length(paths) - 4),
 731                        '"' || ',' || '"',
 732                        CHAR(10)
 733                    )
 734                ELSE
 735                    replace(paths, ',', CHAR(10))
 736            END
 737            WHERE paths IS NOT NULL
 738        ),
 739        sql!(
 740            CREATE TABLE remote_connections(
 741                id INTEGER PRIMARY KEY,
 742                kind TEXT NOT NULL,
 743                host TEXT,
 744                port INTEGER,
 745                user TEXT,
 746                distro TEXT
 747            );
 748
 749            CREATE TABLE workspaces_2(
 750                workspace_id INTEGER PRIMARY KEY,
 751                paths TEXT,
 752                paths_order TEXT,
 753                remote_connection_id INTEGER REFERENCES remote_connections(id),
 754                timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 755                window_state TEXT,
 756                window_x REAL,
 757                window_y REAL,
 758                window_width REAL,
 759                window_height REAL,
 760                display BLOB,
 761                left_dock_visible INTEGER,
 762                left_dock_active_panel TEXT,
 763                right_dock_visible INTEGER,
 764                right_dock_active_panel TEXT,
 765                bottom_dock_visible INTEGER,
 766                bottom_dock_active_panel TEXT,
 767                left_dock_zoom INTEGER,
 768                right_dock_zoom INTEGER,
 769                bottom_dock_zoom INTEGER,
 770                fullscreen INTEGER,
 771                centered_layout INTEGER,
 772                session_id TEXT,
 773                window_id INTEGER
 774            ) STRICT;
 775
 776            INSERT INTO remote_connections
 777            SELECT
 778                id,
 779                "ssh" as kind,
 780                host,
 781                port,
 782                user,
 783                NULL as distro
 784            FROM ssh_connections;
 785
 786            INSERT
 787            INTO workspaces_2
 788            SELECT
 789                workspace_id,
 790                paths,
 791                paths_order,
 792                ssh_connection_id as remote_connection_id,
 793                timestamp,
 794                window_state,
 795                window_x,
 796                window_y,
 797                window_width,
 798                window_height,
 799                display,
 800                left_dock_visible,
 801                left_dock_active_panel,
 802                right_dock_visible,
 803                right_dock_active_panel,
 804                bottom_dock_visible,
 805                bottom_dock_active_panel,
 806                left_dock_zoom,
 807                right_dock_zoom,
 808                bottom_dock_zoom,
 809                fullscreen,
 810                centered_layout,
 811                session_id,
 812                window_id
 813            FROM
 814                workspaces;
 815
 816            DROP TABLE workspaces;
 817            ALTER TABLE workspaces_2 RENAME TO workspaces;
 818
 819            CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(remote_connection_id, paths);
 820        ),
 821        sql!(CREATE TABLE user_toolchains (
 822            remote_connection_id INTEGER,
 823            workspace_id INTEGER NOT NULL,
 824            worktree_id INTEGER NOT NULL,
 825            relative_worktree_path TEXT NOT NULL,
 826            language_name TEXT NOT NULL,
 827            name TEXT NOT NULL,
 828            path TEXT NOT NULL,
 829            raw_json TEXT NOT NULL,
 830
 831            PRIMARY KEY (workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json)
 832        ) STRICT;),
 833        sql!(
 834            DROP TABLE ssh_connections;
 835        ),
 836        sql!(
 837            ALTER TABLE remote_connections ADD COLUMN name TEXT;
 838            ALTER TABLE remote_connections ADD COLUMN container_id TEXT;
 839        ),
 840        sql!(
 841            CREATE TABLE IF NOT EXISTS trusted_worktrees (
 842                trust_id INTEGER PRIMARY KEY AUTOINCREMENT,
 843                absolute_path TEXT,
 844                user_name TEXT,
 845                host_name TEXT
 846            ) STRICT;
 847        ),
 848    ];
 849
 850    // Allow recovering from bad migration that was initially shipped to nightly
 851    // when introducing the ssh_connections table.
 852    fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool {
 853        old.starts_with("CREATE TABLE ssh_connections")
 854            && new.starts_with("CREATE TABLE ssh_connections")
 855    }
 856}
 857
 858db::static_connection!(DB, WorkspaceDb, []);
 859
 860impl WorkspaceDb {
 861    /// Returns a serialized workspace for the given worktree_roots. If the passed array
 862    /// is empty, the most recent workspace is returned instead. If no workspace for the
 863    /// passed roots is stored, returns none.
 864    pub(crate) fn workspace_for_roots<P: AsRef<Path>>(
 865        &self,
 866        worktree_roots: &[P],
 867    ) -> Option<SerializedWorkspace> {
 868        self.workspace_for_roots_internal(worktree_roots, None)
 869    }
 870
 871    pub(crate) fn remote_workspace_for_roots<P: AsRef<Path>>(
 872        &self,
 873        worktree_roots: &[P],
 874        remote_project_id: RemoteConnectionId,
 875    ) -> Option<SerializedWorkspace> {
 876        self.workspace_for_roots_internal(worktree_roots, Some(remote_project_id))
 877    }
 878
 879    pub(crate) fn workspace_for_roots_internal<P: AsRef<Path>>(
 880        &self,
 881        worktree_roots: &[P],
 882        remote_connection_id: Option<RemoteConnectionId>,
 883    ) -> Option<SerializedWorkspace> {
 884        // paths are sorted before db interactions to ensure that the order of the paths
 885        // doesn't affect the workspace selection for existing workspaces
 886        let root_paths = PathList::new(worktree_roots);
 887
 888        // Note that we re-assign the workspace_id here in case it's empty
 889        // and we've grabbed the most recent workspace
 890        let (
 891            workspace_id,
 892            paths,
 893            paths_order,
 894            window_bounds,
 895            display,
 896            centered_layout,
 897            docks,
 898            window_id,
 899        ): (
 900            WorkspaceId,
 901            String,
 902            String,
 903            Option<SerializedWindowBounds>,
 904            Option<Uuid>,
 905            Option<bool>,
 906            DockStructure,
 907            Option<u64>,
 908        ) = self
 909            .select_row_bound(sql! {
 910                SELECT
 911                    workspace_id,
 912                    paths,
 913                    paths_order,
 914                    window_state,
 915                    window_x,
 916                    window_y,
 917                    window_width,
 918                    window_height,
 919                    display,
 920                    centered_layout,
 921                    left_dock_visible,
 922                    left_dock_active_panel,
 923                    left_dock_zoom,
 924                    right_dock_visible,
 925                    right_dock_active_panel,
 926                    right_dock_zoom,
 927                    bottom_dock_visible,
 928                    bottom_dock_active_panel,
 929                    bottom_dock_zoom,
 930                    window_id
 931                FROM workspaces
 932                WHERE
 933                    paths IS ? AND
 934                    remote_connection_id IS ?
 935                LIMIT 1
 936            })
 937            .and_then(|mut prepared_statement| {
 938                (prepared_statement)((
 939                    root_paths.serialize().paths,
 940                    remote_connection_id.map(|id| id.0 as i32),
 941                ))
 942            })
 943            .context("No workspaces found")
 944            .warn_on_err()
 945            .flatten()?;
 946
 947        let paths = PathList::deserialize(&SerializedPathList {
 948            paths,
 949            order: paths_order,
 950        });
 951
 952        let remote_connection_options = if let Some(remote_connection_id) = remote_connection_id {
 953            self.remote_connection(remote_connection_id)
 954                .context("Get remote connection")
 955                .log_err()
 956        } else {
 957            None
 958        };
 959
 960        Some(SerializedWorkspace {
 961            id: workspace_id,
 962            location: match remote_connection_options {
 963                Some(options) => SerializedWorkspaceLocation::Remote(options),
 964                None => SerializedWorkspaceLocation::Local,
 965            },
 966            paths,
 967            center_group: self
 968                .get_center_pane_group(workspace_id)
 969                .context("Getting center group")
 970                .log_err()?,
 971            window_bounds,
 972            centered_layout: centered_layout.unwrap_or(false),
 973            display,
 974            docks,
 975            session_id: None,
 976            breakpoints: self.breakpoints(workspace_id),
 977            window_id,
 978            user_toolchains: self.user_toolchains(workspace_id, remote_connection_id),
 979        })
 980    }
 981
 982    fn breakpoints(&self, workspace_id: WorkspaceId) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
 983        let breakpoints: Result<Vec<(PathBuf, Breakpoint)>> = self
 984            .select_bound(sql! {
 985                SELECT path, breakpoint_location, log_message, condition, hit_condition, state
 986                FROM breakpoints
 987                WHERE workspace_id = ?
 988            })
 989            .and_then(|mut prepared_statement| (prepared_statement)(workspace_id));
 990
 991        match breakpoints {
 992            Ok(bp) => {
 993                if bp.is_empty() {
 994                    log::debug!("Breakpoints are empty after querying database for them");
 995                }
 996
 997                let mut map: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> = Default::default();
 998
 999                for (path, breakpoint) in bp {
1000                    let path: Arc<Path> = path.into();
1001                    map.entry(path.clone()).or_default().push(SourceBreakpoint {
1002                        row: breakpoint.position,
1003                        path,
1004                        message: breakpoint.message,
1005                        condition: breakpoint.condition,
1006                        hit_condition: breakpoint.hit_condition,
1007                        state: breakpoint.state,
1008                    });
1009                }
1010
1011                for (path, bps) in map.iter() {
1012                    log::info!(
1013                        "Got {} breakpoints from database at path: {}",
1014                        bps.len(),
1015                        path.to_string_lossy()
1016                    );
1017                }
1018
1019                map
1020            }
1021            Err(msg) => {
1022                log::error!("Breakpoints query failed with msg: {msg}");
1023                Default::default()
1024            }
1025        }
1026    }
1027
1028    fn user_toolchains(
1029        &self,
1030        workspace_id: WorkspaceId,
1031        remote_connection_id: Option<RemoteConnectionId>,
1032    ) -> BTreeMap<ToolchainScope, IndexSet<Toolchain>> {
1033        type RowKind = (WorkspaceId, u64, String, String, String, String, String);
1034
1035        let toolchains: Vec<RowKind> = self
1036            .select_bound(sql! {
1037                SELECT workspace_id, worktree_id, relative_worktree_path,
1038                language_name, name, path, raw_json
1039                FROM user_toolchains WHERE remote_connection_id IS ?1 AND (
1040                      workspace_id IN (0, ?2)
1041                )
1042            })
1043            .and_then(|mut statement| {
1044                (statement)((remote_connection_id.map(|id| id.0), workspace_id))
1045            })
1046            .unwrap_or_default();
1047        let mut ret = BTreeMap::<_, IndexSet<_>>::default();
1048
1049        for (
1050            _workspace_id,
1051            worktree_id,
1052            relative_worktree_path,
1053            language_name,
1054            name,
1055            path,
1056            raw_json,
1057        ) in toolchains
1058        {
1059            // INTEGER's that are primary keys (like workspace ids, remote connection ids and such) start at 1, so we're safe to
1060            let scope = if _workspace_id == WorkspaceId(0) {
1061                debug_assert_eq!(worktree_id, u64::MAX);
1062                debug_assert_eq!(relative_worktree_path, String::default());
1063                ToolchainScope::Global
1064            } else {
1065                debug_assert_eq!(workspace_id, _workspace_id);
1066                debug_assert_eq!(
1067                    worktree_id == u64::MAX,
1068                    relative_worktree_path == String::default()
1069                );
1070
1071                let Some(relative_path) = RelPath::unix(&relative_worktree_path).log_err() else {
1072                    continue;
1073                };
1074                if worktree_id != u64::MAX && relative_worktree_path != String::default() {
1075                    ToolchainScope::Subproject(
1076                        WorktreeId::from_usize(worktree_id as usize),
1077                        relative_path.into(),
1078                    )
1079                } else {
1080                    ToolchainScope::Project
1081                }
1082            };
1083            let Ok(as_json) = serde_json::from_str(&raw_json) else {
1084                continue;
1085            };
1086            let toolchain = Toolchain {
1087                name: SharedString::from(name),
1088                path: SharedString::from(path),
1089                language_name: LanguageName::from_proto(language_name),
1090                as_json,
1091            };
1092            ret.entry(scope).or_default().insert(toolchain);
1093        }
1094
1095        ret
1096    }
1097
1098    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
1099    /// that used this workspace previously
1100    pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
1101        let paths = workspace.paths.serialize();
1102        log::debug!("Saving workspace at location: {:?}", workspace.location);
1103        self.write(move |conn| {
1104            conn.with_savepoint("update_worktrees", || {
1105                let remote_connection_id = match workspace.location.clone() {
1106                    SerializedWorkspaceLocation::Local => None,
1107                    SerializedWorkspaceLocation::Remote(connection_options) => {
1108                        Some(Self::get_or_create_remote_connection_internal(
1109                            conn,
1110                            connection_options
1111                        )?.0)
1112                    }
1113                };
1114
1115                // Clear out panes and pane_groups
1116                conn.exec_bound(sql!(
1117                    DELETE FROM pane_groups WHERE workspace_id = ?1;
1118                    DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
1119                    .context("Clearing old panes")?;
1120
1121                conn.exec_bound(
1122                    sql!(
1123                        DELETE FROM breakpoints WHERE workspace_id = ?1;
1124                    )
1125                )?(workspace.id).context("Clearing old breakpoints")?;
1126
1127                for (path, breakpoints) in workspace.breakpoints {
1128                    for bp in breakpoints {
1129                        let state = BreakpointStateWrapper::from(bp.state);
1130                        match conn.exec_bound(sql!(
1131                            INSERT INTO breakpoints (workspace_id, path, breakpoint_location,  log_message, condition, hit_condition, state)
1132                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);))?
1133
1134                        ((
1135                            workspace.id,
1136                            path.as_ref(),
1137                            bp.row,
1138                            bp.message,
1139                            bp.condition,
1140                            bp.hit_condition,
1141                            state,
1142                        )) {
1143                            Ok(_) => {
1144                                log::debug!("Stored breakpoint at row: {} in path: {}", bp.row, path.to_string_lossy())
1145                            }
1146                            Err(err) => {
1147                                log::error!("{err}");
1148                                continue;
1149                            }
1150                        }
1151                    }
1152                }
1153
1154                conn.exec_bound(
1155                    sql!(
1156                        DELETE FROM user_toolchains WHERE workspace_id = ?1;
1157                    )
1158                )?(workspace.id).context("Clearing old user toolchains")?;
1159
1160                for (scope, toolchains) in workspace.user_toolchains {
1161                    for toolchain in toolchains {
1162                        let query = sql!(INSERT OR REPLACE INTO user_toolchains(remote_connection_id, workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8));
1163                        let (workspace_id, worktree_id, relative_worktree_path) = match scope {
1164                            ToolchainScope::Subproject(worktree_id, ref path) => (Some(workspace.id), Some(worktree_id), Some(path.as_unix_str().to_owned())),
1165                            ToolchainScope::Project => (Some(workspace.id), None, None),
1166                            ToolchainScope::Global => (None, None, None),
1167                        };
1168                        let args = (remote_connection_id, workspace_id.unwrap_or(WorkspaceId(0)), worktree_id.map_or(usize::MAX,|id| id.to_usize()), relative_worktree_path.unwrap_or_default(),
1169                        toolchain.language_name.as_ref().to_owned(), toolchain.name.to_string(), toolchain.path.to_string(), toolchain.as_json.to_string());
1170                        if let Err(err) = conn.exec_bound(query)?(args) {
1171                            log::error!("{err}");
1172                            continue;
1173                        }
1174                    }
1175                }
1176
1177                conn.exec_bound(sql!(
1178                    DELETE
1179                    FROM workspaces
1180                    WHERE
1181                        workspace_id != ?1 AND
1182                        paths IS ?2 AND
1183                        remote_connection_id IS ?3
1184                ))?((
1185                    workspace.id,
1186                    paths.paths.clone(),
1187                    remote_connection_id,
1188                ))
1189                .context("clearing out old locations")?;
1190
1191                // Upsert
1192                let query = sql!(
1193                    INSERT INTO workspaces(
1194                        workspace_id,
1195                        paths,
1196                        paths_order,
1197                        remote_connection_id,
1198                        left_dock_visible,
1199                        left_dock_active_panel,
1200                        left_dock_zoom,
1201                        right_dock_visible,
1202                        right_dock_active_panel,
1203                        right_dock_zoom,
1204                        bottom_dock_visible,
1205                        bottom_dock_active_panel,
1206                        bottom_dock_zoom,
1207                        session_id,
1208                        window_id,
1209                        timestamp
1210                    )
1211                    VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, CURRENT_TIMESTAMP)
1212                    ON CONFLICT DO
1213                    UPDATE SET
1214                        paths = ?2,
1215                        paths_order = ?3,
1216                        remote_connection_id = ?4,
1217                        left_dock_visible = ?5,
1218                        left_dock_active_panel = ?6,
1219                        left_dock_zoom = ?7,
1220                        right_dock_visible = ?8,
1221                        right_dock_active_panel = ?9,
1222                        right_dock_zoom = ?10,
1223                        bottom_dock_visible = ?11,
1224                        bottom_dock_active_panel = ?12,
1225                        bottom_dock_zoom = ?13,
1226                        session_id = ?14,
1227                        window_id = ?15,
1228                        timestamp = CURRENT_TIMESTAMP
1229                );
1230                let mut prepared_query = conn.exec_bound(query)?;
1231                let args = (
1232                    workspace.id,
1233                    paths.paths.clone(),
1234                    paths.order.clone(),
1235                    remote_connection_id,
1236                    workspace.docks,
1237                    workspace.session_id,
1238                    workspace.window_id,
1239                );
1240
1241                prepared_query(args).context("Updating workspace")?;
1242
1243                // Save center pane group
1244                Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
1245                    .context("save pane group in save workspace")?;
1246
1247                Ok(())
1248            })
1249            .log_err();
1250        })
1251        .await;
1252    }
1253
1254    pub(crate) async fn get_or_create_remote_connection(
1255        &self,
1256        options: RemoteConnectionOptions,
1257    ) -> Result<RemoteConnectionId> {
1258        self.write(move |conn| Self::get_or_create_remote_connection_internal(conn, options))
1259            .await
1260    }
1261
1262    fn get_or_create_remote_connection_internal(
1263        this: &Connection,
1264        options: RemoteConnectionOptions,
1265    ) -> Result<RemoteConnectionId> {
1266        let kind;
1267        let mut user = None;
1268        let mut host = None;
1269        let mut port = None;
1270        let mut distro = None;
1271        let mut name = None;
1272        let mut container_id = None;
1273        match options {
1274            RemoteConnectionOptions::Ssh(options) => {
1275                kind = RemoteConnectionKind::Ssh;
1276                host = Some(options.host.to_string());
1277                port = options.port;
1278                user = options.username;
1279            }
1280            RemoteConnectionOptions::Wsl(options) => {
1281                kind = RemoteConnectionKind::Wsl;
1282                distro = Some(options.distro_name);
1283                user = options.user;
1284            }
1285            RemoteConnectionOptions::Docker(options) => {
1286                kind = RemoteConnectionKind::Docker;
1287                container_id = Some(options.container_id);
1288                name = Some(options.name);
1289            }
1290        }
1291        Self::get_or_create_remote_connection_query(
1292            this,
1293            kind,
1294            host,
1295            port,
1296            user,
1297            distro,
1298            name,
1299            container_id,
1300        )
1301    }
1302
1303    fn get_or_create_remote_connection_query(
1304        this: &Connection,
1305        kind: RemoteConnectionKind,
1306        host: Option<String>,
1307        port: Option<u16>,
1308        user: Option<String>,
1309        distro: Option<String>,
1310        name: Option<String>,
1311        container_id: Option<String>,
1312    ) -> Result<RemoteConnectionId> {
1313        if let Some(id) = this.select_row_bound(sql!(
1314            SELECT id
1315            FROM remote_connections
1316            WHERE
1317                kind IS ? AND
1318                host IS ? AND
1319                port IS ? AND
1320                user IS ? AND
1321                distro IS ? AND
1322                name IS ? AND
1323                container_id IS ?
1324            LIMIT 1
1325        ))?((
1326            kind.serialize(),
1327            host.clone(),
1328            port,
1329            user.clone(),
1330            distro.clone(),
1331            name.clone(),
1332            container_id.clone(),
1333        ))? {
1334            Ok(RemoteConnectionId(id))
1335        } else {
1336            let id = this.select_row_bound(sql!(
1337                INSERT INTO remote_connections (
1338                    kind,
1339                    host,
1340                    port,
1341                    user,
1342                    distro,
1343                    name,
1344                    container_id
1345                ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
1346                RETURNING id
1347            ))?((
1348                kind.serialize(),
1349                host,
1350                port,
1351                user,
1352                distro,
1353                name,
1354                container_id,
1355            ))?
1356            .context("failed to insert remote project")?;
1357            Ok(RemoteConnectionId(id))
1358        }
1359    }
1360
1361    query! {
1362        pub async fn next_id() -> Result<WorkspaceId> {
1363            INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
1364        }
1365    }
1366
1367    fn recent_workspaces(
1368        &self,
1369    ) -> Result<Vec<(WorkspaceId, PathList, Option<RemoteConnectionId>)>> {
1370        Ok(self
1371            .recent_workspaces_query()?
1372            .into_iter()
1373            .map(|(id, paths, order, remote_connection_id)| {
1374                (
1375                    id,
1376                    PathList::deserialize(&SerializedPathList { paths, order }),
1377                    remote_connection_id.map(RemoteConnectionId),
1378                )
1379            })
1380            .collect())
1381    }
1382
1383    query! {
1384        fn recent_workspaces_query() -> Result<Vec<(WorkspaceId, String, String, Option<u64>)>> {
1385            SELECT workspace_id, paths, paths_order, remote_connection_id
1386            FROM workspaces
1387            WHERE
1388                paths IS NOT NULL OR
1389                remote_connection_id IS NOT NULL
1390            ORDER BY timestamp DESC
1391        }
1392    }
1393
1394    fn session_workspaces(
1395        &self,
1396        session_id: String,
1397    ) -> Result<Vec<(PathList, Option<u64>, Option<RemoteConnectionId>)>> {
1398        Ok(self
1399            .session_workspaces_query(session_id)?
1400            .into_iter()
1401            .map(|(paths, order, window_id, remote_connection_id)| {
1402                (
1403                    PathList::deserialize(&SerializedPathList { paths, order }),
1404                    window_id,
1405                    remote_connection_id.map(RemoteConnectionId),
1406                )
1407            })
1408            .collect())
1409    }
1410
1411    query! {
1412        fn session_workspaces_query(session_id: String) -> Result<Vec<(String, String, Option<u64>, Option<u64>)>> {
1413            SELECT paths, paths_order, window_id, remote_connection_id
1414            FROM workspaces
1415            WHERE session_id = ?1
1416            ORDER BY timestamp DESC
1417        }
1418    }
1419
1420    query! {
1421        pub fn breakpoints_for_file(workspace_id: WorkspaceId, file_path: &Path) -> Result<Vec<Breakpoint>> {
1422            SELECT breakpoint_location
1423            FROM breakpoints
1424            WHERE  workspace_id= ?1 AND path = ?2
1425        }
1426    }
1427
1428    query! {
1429        pub fn clear_breakpoints(file_path: &Path) -> Result<()> {
1430            DELETE FROM breakpoints
1431            WHERE file_path = ?2
1432        }
1433    }
1434
1435    fn remote_connections(&self) -> Result<HashMap<RemoteConnectionId, RemoteConnectionOptions>> {
1436        Ok(self.select(sql!(
1437            SELECT
1438                id, kind, host, port, user, distro, container_id, name
1439            FROM
1440                remote_connections
1441        ))?()?
1442        .into_iter()
1443        .filter_map(|(id, kind, host, port, user, distro, container_id, name)| {
1444            Some((
1445                RemoteConnectionId(id),
1446                Self::remote_connection_from_row(
1447                    kind,
1448                    host,
1449                    port,
1450                    user,
1451                    distro,
1452                    container_id,
1453                    name,
1454                )?,
1455            ))
1456        })
1457        .collect())
1458    }
1459
1460    pub(crate) fn remote_connection(
1461        &self,
1462        id: RemoteConnectionId,
1463    ) -> Result<RemoteConnectionOptions> {
1464        let (kind, host, port, user, distro, container_id, name) = self.select_row_bound(sql!(
1465            SELECT kind, host, port, user, distro, container_id, name
1466            FROM remote_connections
1467            WHERE id = ?
1468        ))?(id.0)?
1469        .context("no such remote connection")?;
1470        Self::remote_connection_from_row(kind, host, port, user, distro, container_id, name)
1471            .context("invalid remote_connection row")
1472    }
1473
1474    fn remote_connection_from_row(
1475        kind: String,
1476        host: Option<String>,
1477        port: Option<u16>,
1478        user: Option<String>,
1479        distro: Option<String>,
1480        container_id: Option<String>,
1481        name: Option<String>,
1482    ) -> Option<RemoteConnectionOptions> {
1483        match RemoteConnectionKind::deserialize(&kind)? {
1484            RemoteConnectionKind::Wsl => Some(RemoteConnectionOptions::Wsl(WslConnectionOptions {
1485                distro_name: distro?,
1486                user: user,
1487            })),
1488            RemoteConnectionKind::Ssh => Some(RemoteConnectionOptions::Ssh(SshConnectionOptions {
1489                host: host?.into(),
1490                port,
1491                username: user,
1492                ..Default::default()
1493            })),
1494            RemoteConnectionKind::Docker => {
1495                Some(RemoteConnectionOptions::Docker(DockerConnectionOptions {
1496                    container_id: container_id?,
1497                    name: name?,
1498                    upload_binary_over_docker_exec: false,
1499                }))
1500            }
1501        }
1502    }
1503
1504    query! {
1505        pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
1506            DELETE FROM workspaces
1507            WHERE workspace_id IS ?
1508        }
1509    }
1510
1511    // Returns the recent locations which are still valid on disk and deletes ones which no longer
1512    // exist.
1513    pub async fn recent_workspaces_on_disk(
1514        &self,
1515    ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
1516        let mut result = Vec::new();
1517        let mut delete_tasks = Vec::new();
1518        let remote_connections = self.remote_connections()?;
1519
1520        for (id, paths, remote_connection_id) in self.recent_workspaces()? {
1521            if let Some(remote_connection_id) = remote_connection_id {
1522                if let Some(connection_options) = remote_connections.get(&remote_connection_id) {
1523                    result.push((
1524                        id,
1525                        SerializedWorkspaceLocation::Remote(connection_options.clone()),
1526                        paths,
1527                    ));
1528                } else {
1529                    delete_tasks.push(self.delete_workspace_by_id(id));
1530                }
1531                continue;
1532            }
1533
1534            let has_wsl_path = if cfg!(windows) {
1535                paths
1536                    .paths()
1537                    .iter()
1538                    .any(|path| util::paths::WslPath::from_path(path).is_some())
1539            } else {
1540                false
1541            };
1542
1543            // Delete the workspace if any of the paths are WSL paths.
1544            // If a local workspace points to WSL, this check will cause us to wait for the
1545            // WSL VM and file server to boot up. This can block for many seconds.
1546            // Supported scenarios use remote workspaces.
1547            if !has_wsl_path && paths.paths().iter().all(|path| path.exists()) {
1548                // Only show directories in recent projects
1549                if paths.paths().iter().any(|path| path.is_dir()) {
1550                    result.push((id, SerializedWorkspaceLocation::Local, paths));
1551                }
1552            } else {
1553                delete_tasks.push(self.delete_workspace_by_id(id));
1554            }
1555        }
1556
1557        futures::future::join_all(delete_tasks).await;
1558        Ok(result)
1559    }
1560
1561    pub async fn last_workspace(&self) -> Result<Option<(SerializedWorkspaceLocation, PathList)>> {
1562        Ok(self
1563            .recent_workspaces_on_disk()
1564            .await?
1565            .into_iter()
1566            .next()
1567            .map(|(_, location, paths)| (location, paths)))
1568    }
1569
1570    // Returns the locations of the workspaces that were still opened when the last
1571    // session was closed (i.e. when Zed was quit).
1572    // If `last_session_window_order` is provided, the returned locations are ordered
1573    // according to that.
1574    pub fn last_session_workspace_locations(
1575        &self,
1576        last_session_id: &str,
1577        last_session_window_stack: Option<Vec<WindowId>>,
1578    ) -> Result<Vec<(SerializedWorkspaceLocation, PathList)>> {
1579        let mut workspaces = Vec::new();
1580
1581        for (paths, window_id, remote_connection_id) in
1582            self.session_workspaces(last_session_id.to_owned())?
1583        {
1584            if let Some(remote_connection_id) = remote_connection_id {
1585                workspaces.push((
1586                    SerializedWorkspaceLocation::Remote(
1587                        self.remote_connection(remote_connection_id)?,
1588                    ),
1589                    paths,
1590                    window_id.map(WindowId::from),
1591                ));
1592            } else if paths.paths().iter().all(|path| path.exists())
1593                && paths.paths().iter().any(|path| path.is_dir())
1594            {
1595                workspaces.push((
1596                    SerializedWorkspaceLocation::Local,
1597                    paths,
1598                    window_id.map(WindowId::from),
1599                ));
1600            }
1601        }
1602
1603        if let Some(stack) = last_session_window_stack {
1604            workspaces.sort_by_key(|(_, _, window_id)| {
1605                window_id
1606                    .and_then(|id| stack.iter().position(|&order_id| order_id == id))
1607                    .unwrap_or(usize::MAX)
1608            });
1609        }
1610
1611        Ok(workspaces
1612            .into_iter()
1613            .map(|(location, paths, _)| (location, paths))
1614            .collect::<Vec<_>>())
1615    }
1616
1617    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1618        Ok(self
1619            .get_pane_group(workspace_id, None)?
1620            .into_iter()
1621            .next()
1622            .unwrap_or_else(|| {
1623                SerializedPaneGroup::Pane(SerializedPane {
1624                    active: true,
1625                    children: vec![],
1626                    pinned_count: 0,
1627                })
1628            }))
1629    }
1630
1631    fn get_pane_group(
1632        &self,
1633        workspace_id: WorkspaceId,
1634        group_id: Option<GroupId>,
1635    ) -> Result<Vec<SerializedPaneGroup>> {
1636        type GroupKey = (Option<GroupId>, WorkspaceId);
1637        type GroupOrPane = (
1638            Option<GroupId>,
1639            Option<SerializedAxis>,
1640            Option<PaneId>,
1641            Option<bool>,
1642            Option<usize>,
1643            Option<String>,
1644        );
1645        self.select_bound::<GroupKey, GroupOrPane>(sql!(
1646            SELECT group_id, axis, pane_id, active, pinned_count, flexes
1647                FROM (SELECT
1648                        group_id,
1649                        axis,
1650                        NULL as pane_id,
1651                        NULL as active,
1652                        NULL as pinned_count,
1653                        position,
1654                        parent_group_id,
1655                        workspace_id,
1656                        flexes
1657                      FROM pane_groups
1658                    UNION
1659                      SELECT
1660                        NULL,
1661                        NULL,
1662                        center_panes.pane_id,
1663                        panes.active as active,
1664                        pinned_count,
1665                        position,
1666                        parent_group_id,
1667                        panes.workspace_id as workspace_id,
1668                        NULL
1669                      FROM center_panes
1670                      JOIN panes ON center_panes.pane_id = panes.pane_id)
1671                WHERE parent_group_id IS ? AND workspace_id = ?
1672                ORDER BY position
1673        ))?((group_id, workspace_id))?
1674        .into_iter()
1675        .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1676            let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1677            if let Some((group_id, axis)) = group_id.zip(axis) {
1678                let flexes = flexes
1679                    .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1680                    .transpose()?;
1681
1682                Ok(SerializedPaneGroup::Group {
1683                    axis,
1684                    children: self.get_pane_group(workspace_id, Some(group_id))?,
1685                    flexes,
1686                })
1687            } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
1688                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
1689                    self.get_items(pane_id)?,
1690                    active,
1691                    pinned_count,
1692                )))
1693            } else {
1694                bail!("Pane Group Child was neither a pane group or a pane");
1695            }
1696        })
1697        // Filter out panes and pane groups which don't have any children or items
1698        .filter(|pane_group| match pane_group {
1699            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
1700            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
1701            _ => true,
1702        })
1703        .collect::<Result<_>>()
1704    }
1705
1706    fn save_pane_group(
1707        conn: &Connection,
1708        workspace_id: WorkspaceId,
1709        pane_group: &SerializedPaneGroup,
1710        parent: Option<(GroupId, usize)>,
1711    ) -> Result<()> {
1712        if parent.is_none() {
1713            log::debug!("Saving a pane group for workspace {workspace_id:?}");
1714        }
1715        match pane_group {
1716            SerializedPaneGroup::Group {
1717                axis,
1718                children,
1719                flexes,
1720            } => {
1721                let (parent_id, position) = parent.unzip();
1722
1723                let flex_string = flexes
1724                    .as_ref()
1725                    .map(|flexes| serde_json::json!(flexes).to_string());
1726
1727                let group_id = conn.select_row_bound::<_, i64>(sql!(
1728                    INSERT INTO pane_groups(
1729                        workspace_id,
1730                        parent_group_id,
1731                        position,
1732                        axis,
1733                        flexes
1734                    )
1735                    VALUES (?, ?, ?, ?, ?)
1736                    RETURNING group_id
1737                ))?((
1738                    workspace_id,
1739                    parent_id,
1740                    position,
1741                    *axis,
1742                    flex_string,
1743                ))?
1744                .context("Couldn't retrieve group_id from inserted pane_group")?;
1745
1746                for (position, group) in children.iter().enumerate() {
1747                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
1748                }
1749
1750                Ok(())
1751            }
1752            SerializedPaneGroup::Pane(pane) => {
1753                Self::save_pane(conn, workspace_id, pane, parent)?;
1754                Ok(())
1755            }
1756        }
1757    }
1758
1759    fn save_pane(
1760        conn: &Connection,
1761        workspace_id: WorkspaceId,
1762        pane: &SerializedPane,
1763        parent: Option<(GroupId, usize)>,
1764    ) -> Result<PaneId> {
1765        let pane_id = conn.select_row_bound::<_, i64>(sql!(
1766            INSERT INTO panes(workspace_id, active, pinned_count)
1767            VALUES (?, ?, ?)
1768            RETURNING pane_id
1769        ))?((workspace_id, pane.active, pane.pinned_count))?
1770        .context("Could not retrieve inserted pane_id")?;
1771
1772        let (parent_id, order) = parent.unzip();
1773        conn.exec_bound(sql!(
1774            INSERT INTO center_panes(pane_id, parent_group_id, position)
1775            VALUES (?, ?, ?)
1776        ))?((pane_id, parent_id, order))?;
1777
1778        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
1779
1780        Ok(pane_id)
1781    }
1782
1783    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
1784        self.select_bound(sql!(
1785            SELECT kind, item_id, active, preview FROM items
1786            WHERE pane_id = ?
1787                ORDER BY position
1788        ))?(pane_id)
1789    }
1790
1791    fn save_items(
1792        conn: &Connection,
1793        workspace_id: WorkspaceId,
1794        pane_id: PaneId,
1795        items: &[SerializedItem],
1796    ) -> Result<()> {
1797        let mut insert = conn.exec_bound(sql!(
1798            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
1799        )).context("Preparing insertion")?;
1800        for (position, item) in items.iter().enumerate() {
1801            insert((workspace_id, pane_id, position, item))?;
1802        }
1803
1804        Ok(())
1805    }
1806
1807    query! {
1808        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
1809            UPDATE workspaces
1810            SET timestamp = CURRENT_TIMESTAMP
1811            WHERE workspace_id = ?
1812        }
1813    }
1814
1815    query! {
1816        pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
1817            UPDATE workspaces
1818            SET window_state = ?2,
1819                window_x = ?3,
1820                window_y = ?4,
1821                window_width = ?5,
1822                window_height = ?6,
1823                display = ?7
1824            WHERE workspace_id = ?1
1825        }
1826    }
1827
1828    query! {
1829        pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
1830            UPDATE workspaces
1831            SET centered_layout = ?2
1832            WHERE workspace_id = ?1
1833        }
1834    }
1835
1836    query! {
1837        pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option<String>) -> Result<()> {
1838            UPDATE workspaces
1839            SET session_id = ?2
1840            WHERE workspace_id = ?1
1841        }
1842    }
1843
1844    pub(crate) async fn toolchains(
1845        &self,
1846        workspace_id: WorkspaceId,
1847    ) -> Result<Vec<(Toolchain, WorktreeId, Arc<RelPath>)>> {
1848        self.write(move |this| {
1849            let mut select = this
1850                .select_bound(sql!(
1851                    SELECT
1852                        name, path, worktree_id, relative_worktree_path, language_name, raw_json
1853                    FROM toolchains
1854                    WHERE workspace_id = ?
1855                ))
1856                .context("select toolchains")?;
1857
1858            let toolchain: Vec<(String, String, u64, String, String, String)> =
1859                select(workspace_id)?;
1860
1861            Ok(toolchain
1862                .into_iter()
1863                .filter_map(
1864                    |(name, path, worktree_id, relative_worktree_path, language, json)| {
1865                        Some((
1866                            Toolchain {
1867                                name: name.into(),
1868                                path: path.into(),
1869                                language_name: LanguageName::new(&language),
1870                                as_json: serde_json::Value::from_str(&json).ok()?,
1871                            },
1872                            WorktreeId::from_proto(worktree_id),
1873                            RelPath::from_proto(&relative_worktree_path).log_err()?,
1874                        ))
1875                    },
1876                )
1877                .collect())
1878        })
1879        .await
1880    }
1881
1882    pub async fn set_toolchain(
1883        &self,
1884        workspace_id: WorkspaceId,
1885        worktree_id: WorktreeId,
1886        relative_worktree_path: Arc<RelPath>,
1887        toolchain: Toolchain,
1888    ) -> Result<()> {
1889        log::debug!(
1890            "Setting toolchain for workspace, worktree: {worktree_id:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
1891            toolchain.name
1892        );
1893        self.write(move |conn| {
1894            let mut insert = conn
1895                .exec_bound(sql!(
1896                    INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?,  ?, ?)
1897                    ON CONFLICT DO
1898                    UPDATE SET
1899                        name = ?5,
1900                        path = ?6,
1901                        raw_json = ?7
1902                ))
1903                .context("Preparing insertion")?;
1904
1905            insert((
1906                workspace_id,
1907                worktree_id.to_usize(),
1908                relative_worktree_path.as_unix_str(),
1909                toolchain.language_name.as_ref(),
1910                toolchain.name.as_ref(),
1911                toolchain.path.as_ref(),
1912                toolchain.as_json.to_string(),
1913            ))?;
1914
1915            Ok(())
1916        }).await
1917    }
1918
1919    pub(crate) async fn save_trusted_worktrees(
1920        &self,
1921        trusted_worktrees: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
1922        trusted_workspaces: HashSet<Option<RemoteHostLocation>>,
1923    ) -> anyhow::Result<()> {
1924        use anyhow::Context as _;
1925        use db::sqlez::statement::Statement;
1926        use itertools::Itertools as _;
1927
1928        DB.clear_trusted_worktrees()
1929            .await
1930            .context("clearing previous trust state")?;
1931
1932        let trusted_worktrees = trusted_worktrees
1933            .into_iter()
1934            .flat_map(|(host, abs_paths)| {
1935                abs_paths
1936                    .into_iter()
1937                    .map(move |abs_path| (Some(abs_path), host.clone()))
1938            })
1939            .chain(trusted_workspaces.into_iter().map(|host| (None, host)))
1940            .collect::<Vec<_>>();
1941        let mut first_worktree;
1942        let mut last_worktree = 0_usize;
1943        for (count, placeholders) in std::iter::once("(?, ?, ?)")
1944            .cycle()
1945            .take(trusted_worktrees.len())
1946            .chunks(MAX_QUERY_PLACEHOLDERS / 3)
1947            .into_iter()
1948            .map(|chunk| {
1949                let mut count = 0;
1950                let placeholders = chunk
1951                    .inspect(|_| {
1952                        count += 1;
1953                    })
1954                    .join(", ");
1955                (count, placeholders)
1956            })
1957            .collect::<Vec<_>>()
1958        {
1959            first_worktree = last_worktree;
1960            last_worktree = last_worktree + count;
1961            let query = format!(
1962                r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name)
1963VALUES {placeholders};"#
1964            );
1965
1966            let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec();
1967            self.write(move |conn| {
1968                let mut statement = Statement::prepare(conn, query)?;
1969                let mut next_index = 1;
1970                for (abs_path, host) in trusted_worktrees {
1971                    let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy());
1972                    next_index = statement.bind(
1973                        &abs_path.as_ref().map(|abs_path| abs_path.as_ref()),
1974                        next_index,
1975                    )?;
1976                    next_index = statement.bind(
1977                        &host
1978                            .as_ref()
1979                            .and_then(|host| Some(host.user_name.as_ref()?.as_str())),
1980                        next_index,
1981                    )?;
1982                    next_index = statement.bind(
1983                        &host.as_ref().map(|host| host.host_identifier.as_str()),
1984                        next_index,
1985                    )?;
1986                }
1987                statement.exec()
1988            })
1989            .await
1990            .context("inserting new trusted state")?;
1991        }
1992        Ok(())
1993    }
1994
1995    pub fn fetch_trusted_worktrees(
1996        &self,
1997        worktree_store: Option<Entity<WorktreeStore>>,
1998        host: Option<RemoteHostLocation>,
1999        cx: &App,
2000    ) -> Result<HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>> {
2001        let trusted_worktrees = DB.trusted_worktrees()?;
2002        Ok(trusted_worktrees
2003            .into_iter()
2004            .map(|(abs_path, user_name, host_name)| {
2005                let db_host = match (user_name, host_name) {
2006                    (_, None) => None,
2007                    (None, Some(host_name)) => Some(RemoteHostLocation {
2008                        user_name: None,
2009                        host_identifier: SharedString::new(host_name),
2010                    }),
2011                    (Some(user_name), Some(host_name)) => Some(RemoteHostLocation {
2012                        user_name: Some(SharedString::new(user_name)),
2013                        host_identifier: SharedString::new(host_name),
2014                    }),
2015                };
2016
2017                match abs_path {
2018                    Some(abs_path) => {
2019                        if db_host != host {
2020                            (db_host, PathTrust::AbsPath(abs_path))
2021                        } else if let Some(worktree_store) = &worktree_store {
2022                            find_worktree_in_store(worktree_store.read(cx), &abs_path, cx)
2023                                .map(PathTrust::Worktree)
2024                                .map(|trusted_worktree| (host.clone(), trusted_worktree))
2025                                .unwrap_or_else(|| (db_host.clone(), PathTrust::AbsPath(abs_path)))
2026                        } else {
2027                            (db_host, PathTrust::AbsPath(abs_path))
2028                        }
2029                    }
2030                    None => (db_host, PathTrust::Workspace),
2031                }
2032            })
2033            .fold(HashMap::default(), |mut acc, (remote_host, path_trust)| {
2034                acc.entry(remote_host)
2035                    .or_insert_with(HashSet::default)
2036                    .insert(path_trust);
2037                acc
2038            }))
2039    }
2040
2041    query! {
2042        fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
2043            SELECT absolute_path, user_name, host_name
2044            FROM trusted_worktrees
2045        }
2046    }
2047
2048    query! {
2049        pub async fn clear_trusted_worktrees() -> Result<()> {
2050            DELETE FROM trusted_worktrees
2051        }
2052    }
2053}
2054
2055pub fn delete_unloaded_items(
2056    alive_items: Vec<ItemId>,
2057    workspace_id: WorkspaceId,
2058    table: &'static str,
2059    db: &ThreadSafeConnection,
2060    cx: &mut App,
2061) -> Task<Result<()>> {
2062    let db = db.clone();
2063    cx.spawn(async move |_| {
2064        let placeholders = alive_items
2065            .iter()
2066            .map(|_| "?")
2067            .collect::<Vec<&str>>()
2068            .join(", ");
2069
2070        let query = format!(
2071            "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
2072        );
2073
2074        db.write(move |conn| {
2075            let mut statement = Statement::prepare(conn, query)?;
2076            let mut next_index = statement.bind(&workspace_id, 1)?;
2077            for id in alive_items {
2078                next_index = statement.bind(&id, next_index)?;
2079            }
2080            statement.exec()
2081        })
2082        .await
2083    })
2084}
2085
2086#[cfg(test)]
2087mod tests {
2088    use super::*;
2089    use crate::persistence::model::{
2090        SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
2091    };
2092    use gpui;
2093    use pretty_assertions::assert_eq;
2094    use remote::SshConnectionOptions;
2095    use std::{thread, time::Duration};
2096
2097    #[gpui::test]
2098    async fn test_breakpoints() {
2099        zlog::init_test();
2100
2101        let db = WorkspaceDb::open_test_db("test_breakpoints").await;
2102        let id = db.next_id().await.unwrap();
2103
2104        let path = Path::new("/tmp/test.rs");
2105
2106        let breakpoint = Breakpoint {
2107            position: 123,
2108            message: None,
2109            state: BreakpointState::Enabled,
2110            condition: None,
2111            hit_condition: None,
2112        };
2113
2114        let log_breakpoint = Breakpoint {
2115            position: 456,
2116            message: Some("Test log message".into()),
2117            state: BreakpointState::Enabled,
2118            condition: None,
2119            hit_condition: None,
2120        };
2121
2122        let disable_breakpoint = Breakpoint {
2123            position: 578,
2124            message: None,
2125            state: BreakpointState::Disabled,
2126            condition: None,
2127            hit_condition: None,
2128        };
2129
2130        let condition_breakpoint = Breakpoint {
2131            position: 789,
2132            message: None,
2133            state: BreakpointState::Enabled,
2134            condition: Some("x > 5".into()),
2135            hit_condition: None,
2136        };
2137
2138        let hit_condition_breakpoint = Breakpoint {
2139            position: 999,
2140            message: None,
2141            state: BreakpointState::Enabled,
2142            condition: None,
2143            hit_condition: Some(">= 3".into()),
2144        };
2145
2146        let workspace = SerializedWorkspace {
2147            id,
2148            paths: PathList::new(&["/tmp"]),
2149            location: SerializedWorkspaceLocation::Local,
2150            center_group: Default::default(),
2151            window_bounds: Default::default(),
2152            display: Default::default(),
2153            docks: Default::default(),
2154            centered_layout: false,
2155            breakpoints: {
2156                let mut map = collections::BTreeMap::default();
2157                map.insert(
2158                    Arc::from(path),
2159                    vec![
2160                        SourceBreakpoint {
2161                            row: breakpoint.position,
2162                            path: Arc::from(path),
2163                            message: breakpoint.message.clone(),
2164                            state: breakpoint.state,
2165                            condition: breakpoint.condition.clone(),
2166                            hit_condition: breakpoint.hit_condition.clone(),
2167                        },
2168                        SourceBreakpoint {
2169                            row: log_breakpoint.position,
2170                            path: Arc::from(path),
2171                            message: log_breakpoint.message.clone(),
2172                            state: log_breakpoint.state,
2173                            condition: log_breakpoint.condition.clone(),
2174                            hit_condition: log_breakpoint.hit_condition.clone(),
2175                        },
2176                        SourceBreakpoint {
2177                            row: disable_breakpoint.position,
2178                            path: Arc::from(path),
2179                            message: disable_breakpoint.message.clone(),
2180                            state: disable_breakpoint.state,
2181                            condition: disable_breakpoint.condition.clone(),
2182                            hit_condition: disable_breakpoint.hit_condition.clone(),
2183                        },
2184                        SourceBreakpoint {
2185                            row: condition_breakpoint.position,
2186                            path: Arc::from(path),
2187                            message: condition_breakpoint.message.clone(),
2188                            state: condition_breakpoint.state,
2189                            condition: condition_breakpoint.condition.clone(),
2190                            hit_condition: condition_breakpoint.hit_condition.clone(),
2191                        },
2192                        SourceBreakpoint {
2193                            row: hit_condition_breakpoint.position,
2194                            path: Arc::from(path),
2195                            message: hit_condition_breakpoint.message.clone(),
2196                            state: hit_condition_breakpoint.state,
2197                            condition: hit_condition_breakpoint.condition.clone(),
2198                            hit_condition: hit_condition_breakpoint.hit_condition.clone(),
2199                        },
2200                    ],
2201                );
2202                map
2203            },
2204            session_id: None,
2205            window_id: None,
2206            user_toolchains: Default::default(),
2207        };
2208
2209        db.save_workspace(workspace.clone()).await;
2210
2211        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2212        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
2213
2214        assert_eq!(loaded_breakpoints.len(), 5);
2215
2216        // normal breakpoint
2217        assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
2218        assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
2219        assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
2220        assert_eq!(
2221            loaded_breakpoints[0].hit_condition,
2222            breakpoint.hit_condition
2223        );
2224        assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
2225        assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
2226
2227        // enabled breakpoint
2228        assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
2229        assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
2230        assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
2231        assert_eq!(
2232            loaded_breakpoints[1].hit_condition,
2233            log_breakpoint.hit_condition
2234        );
2235        assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
2236        assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
2237
2238        // disable breakpoint
2239        assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
2240        assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
2241        assert_eq!(
2242            loaded_breakpoints[2].condition,
2243            disable_breakpoint.condition
2244        );
2245        assert_eq!(
2246            loaded_breakpoints[2].hit_condition,
2247            disable_breakpoint.hit_condition
2248        );
2249        assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
2250        assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
2251
2252        // condition breakpoint
2253        assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
2254        assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
2255        assert_eq!(
2256            loaded_breakpoints[3].condition,
2257            condition_breakpoint.condition
2258        );
2259        assert_eq!(
2260            loaded_breakpoints[3].hit_condition,
2261            condition_breakpoint.hit_condition
2262        );
2263        assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
2264        assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
2265
2266        // hit condition breakpoint
2267        assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
2268        assert_eq!(
2269            loaded_breakpoints[4].message,
2270            hit_condition_breakpoint.message
2271        );
2272        assert_eq!(
2273            loaded_breakpoints[4].condition,
2274            hit_condition_breakpoint.condition
2275        );
2276        assert_eq!(
2277            loaded_breakpoints[4].hit_condition,
2278            hit_condition_breakpoint.hit_condition
2279        );
2280        assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
2281        assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
2282    }
2283
2284    #[gpui::test]
2285    async fn test_remove_last_breakpoint() {
2286        zlog::init_test();
2287
2288        let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
2289        let id = db.next_id().await.unwrap();
2290
2291        let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
2292
2293        let breakpoint_to_remove = Breakpoint {
2294            position: 100,
2295            message: None,
2296            state: BreakpointState::Enabled,
2297            condition: None,
2298            hit_condition: None,
2299        };
2300
2301        let workspace = SerializedWorkspace {
2302            id,
2303            paths: PathList::new(&["/tmp"]),
2304            location: SerializedWorkspaceLocation::Local,
2305            center_group: Default::default(),
2306            window_bounds: Default::default(),
2307            display: Default::default(),
2308            docks: Default::default(),
2309            centered_layout: false,
2310            breakpoints: {
2311                let mut map = collections::BTreeMap::default();
2312                map.insert(
2313                    Arc::from(singular_path),
2314                    vec![SourceBreakpoint {
2315                        row: breakpoint_to_remove.position,
2316                        path: Arc::from(singular_path),
2317                        message: None,
2318                        state: BreakpointState::Enabled,
2319                        condition: None,
2320                        hit_condition: None,
2321                    }],
2322                );
2323                map
2324            },
2325            session_id: None,
2326            window_id: None,
2327            user_toolchains: Default::default(),
2328        };
2329
2330        db.save_workspace(workspace.clone()).await;
2331
2332        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2333        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
2334
2335        assert_eq!(loaded_breakpoints.len(), 1);
2336        assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
2337        assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
2338        assert_eq!(
2339            loaded_breakpoints[0].condition,
2340            breakpoint_to_remove.condition
2341        );
2342        assert_eq!(
2343            loaded_breakpoints[0].hit_condition,
2344            breakpoint_to_remove.hit_condition
2345        );
2346        assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
2347        assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
2348
2349        let workspace_without_breakpoint = SerializedWorkspace {
2350            id,
2351            paths: PathList::new(&["/tmp"]),
2352            location: SerializedWorkspaceLocation::Local,
2353            center_group: Default::default(),
2354            window_bounds: Default::default(),
2355            display: Default::default(),
2356            docks: Default::default(),
2357            centered_layout: false,
2358            breakpoints: collections::BTreeMap::default(),
2359            session_id: None,
2360            window_id: None,
2361            user_toolchains: Default::default(),
2362        };
2363
2364        db.save_workspace(workspace_without_breakpoint.clone())
2365            .await;
2366
2367        let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
2368        let empty_breakpoints = loaded_after_remove
2369            .breakpoints
2370            .get(&Arc::from(singular_path));
2371
2372        assert!(empty_breakpoints.is_none());
2373    }
2374
2375    #[gpui::test]
2376    async fn test_next_id_stability() {
2377        zlog::init_test();
2378
2379        let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
2380
2381        db.write(|conn| {
2382            conn.migrate(
2383                "test_table",
2384                &[sql!(
2385                    CREATE TABLE test_table(
2386                        text TEXT,
2387                        workspace_id INTEGER,
2388                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2389                        ON DELETE CASCADE
2390                    ) STRICT;
2391                )],
2392                |_, _, _| false,
2393            )
2394            .unwrap();
2395        })
2396        .await;
2397
2398        let id = db.next_id().await.unwrap();
2399        // Assert the empty row got inserted
2400        assert_eq!(
2401            Some(id),
2402            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
2403                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
2404            ))
2405            .unwrap()(id)
2406            .unwrap()
2407        );
2408
2409        db.write(move |conn| {
2410            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2411                .unwrap()(("test-text-1", id))
2412            .unwrap()
2413        })
2414        .await;
2415
2416        let test_text_1 = db
2417            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2418            .unwrap()(1)
2419        .unwrap()
2420        .unwrap();
2421        assert_eq!(test_text_1, "test-text-1");
2422    }
2423
2424    #[gpui::test]
2425    async fn test_workspace_id_stability() {
2426        zlog::init_test();
2427
2428        let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
2429
2430        db.write(|conn| {
2431            conn.migrate(
2432                "test_table",
2433                &[sql!(
2434                        CREATE TABLE test_table(
2435                            text TEXT,
2436                            workspace_id INTEGER,
2437                            FOREIGN KEY(workspace_id)
2438                                REFERENCES workspaces(workspace_id)
2439                            ON DELETE CASCADE
2440                        ) STRICT;)],
2441                |_, _, _| false,
2442            )
2443        })
2444        .await
2445        .unwrap();
2446
2447        let mut workspace_1 = SerializedWorkspace {
2448            id: WorkspaceId(1),
2449            paths: PathList::new(&["/tmp", "/tmp2"]),
2450            location: SerializedWorkspaceLocation::Local,
2451            center_group: Default::default(),
2452            window_bounds: Default::default(),
2453            display: Default::default(),
2454            docks: Default::default(),
2455            centered_layout: false,
2456            breakpoints: Default::default(),
2457            session_id: None,
2458            window_id: None,
2459            user_toolchains: Default::default(),
2460        };
2461
2462        let workspace_2 = SerializedWorkspace {
2463            id: WorkspaceId(2),
2464            paths: PathList::new(&["/tmp"]),
2465            location: SerializedWorkspaceLocation::Local,
2466            center_group: Default::default(),
2467            window_bounds: Default::default(),
2468            display: Default::default(),
2469            docks: Default::default(),
2470            centered_layout: false,
2471            breakpoints: Default::default(),
2472            session_id: None,
2473            window_id: None,
2474            user_toolchains: Default::default(),
2475        };
2476
2477        db.save_workspace(workspace_1.clone()).await;
2478
2479        db.write(|conn| {
2480            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2481                .unwrap()(("test-text-1", 1))
2482            .unwrap();
2483        })
2484        .await;
2485
2486        db.save_workspace(workspace_2.clone()).await;
2487
2488        db.write(|conn| {
2489            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2490                .unwrap()(("test-text-2", 2))
2491            .unwrap();
2492        })
2493        .await;
2494
2495        workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
2496        db.save_workspace(workspace_1.clone()).await;
2497        db.save_workspace(workspace_1).await;
2498        db.save_workspace(workspace_2).await;
2499
2500        let test_text_2 = db
2501            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2502            .unwrap()(2)
2503        .unwrap()
2504        .unwrap();
2505        assert_eq!(test_text_2, "test-text-2");
2506
2507        let test_text_1 = db
2508            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2509            .unwrap()(1)
2510        .unwrap()
2511        .unwrap();
2512        assert_eq!(test_text_1, "test-text-1");
2513    }
2514
2515    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
2516        SerializedPaneGroup::Group {
2517            axis: SerializedAxis(axis),
2518            flexes: None,
2519            children,
2520        }
2521    }
2522
2523    #[gpui::test]
2524    async fn test_full_workspace_serialization() {
2525        zlog::init_test();
2526
2527        let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
2528
2529        //  -----------------
2530        //  | 1,2   | 5,6   |
2531        //  | - - - |       |
2532        //  | 3,4   |       |
2533        //  -----------------
2534        let center_group = group(
2535            Axis::Horizontal,
2536            vec![
2537                group(
2538                    Axis::Vertical,
2539                    vec![
2540                        SerializedPaneGroup::Pane(SerializedPane::new(
2541                            vec![
2542                                SerializedItem::new("Terminal", 5, false, false),
2543                                SerializedItem::new("Terminal", 6, true, false),
2544                            ],
2545                            false,
2546                            0,
2547                        )),
2548                        SerializedPaneGroup::Pane(SerializedPane::new(
2549                            vec![
2550                                SerializedItem::new("Terminal", 7, true, false),
2551                                SerializedItem::new("Terminal", 8, false, false),
2552                            ],
2553                            false,
2554                            0,
2555                        )),
2556                    ],
2557                ),
2558                SerializedPaneGroup::Pane(SerializedPane::new(
2559                    vec![
2560                        SerializedItem::new("Terminal", 9, false, false),
2561                        SerializedItem::new("Terminal", 10, true, false),
2562                    ],
2563                    false,
2564                    0,
2565                )),
2566            ],
2567        );
2568
2569        let workspace = SerializedWorkspace {
2570            id: WorkspaceId(5),
2571            paths: PathList::new(&["/tmp", "/tmp2"]),
2572            location: SerializedWorkspaceLocation::Local,
2573            center_group,
2574            window_bounds: Default::default(),
2575            breakpoints: Default::default(),
2576            display: Default::default(),
2577            docks: Default::default(),
2578            centered_layout: false,
2579            session_id: None,
2580            window_id: Some(999),
2581            user_toolchains: Default::default(),
2582        };
2583
2584        db.save_workspace(workspace.clone()).await;
2585
2586        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2587        assert_eq!(workspace, round_trip_workspace.unwrap());
2588
2589        // Test guaranteed duplicate IDs
2590        db.save_workspace(workspace.clone()).await;
2591        db.save_workspace(workspace.clone()).await;
2592
2593        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2594        assert_eq!(workspace, round_trip_workspace.unwrap());
2595    }
2596
2597    #[gpui::test]
2598    async fn test_workspace_assignment() {
2599        zlog::init_test();
2600
2601        let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2602
2603        let workspace_1 = SerializedWorkspace {
2604            id: WorkspaceId(1),
2605            paths: PathList::new(&["/tmp", "/tmp2"]),
2606            location: SerializedWorkspaceLocation::Local,
2607            center_group: Default::default(),
2608            window_bounds: Default::default(),
2609            breakpoints: Default::default(),
2610            display: Default::default(),
2611            docks: Default::default(),
2612            centered_layout: false,
2613            session_id: None,
2614            window_id: Some(1),
2615            user_toolchains: Default::default(),
2616        };
2617
2618        let mut workspace_2 = SerializedWorkspace {
2619            id: WorkspaceId(2),
2620            paths: PathList::new(&["/tmp"]),
2621            location: SerializedWorkspaceLocation::Local,
2622            center_group: Default::default(),
2623            window_bounds: Default::default(),
2624            display: Default::default(),
2625            docks: Default::default(),
2626            centered_layout: false,
2627            breakpoints: Default::default(),
2628            session_id: None,
2629            window_id: Some(2),
2630            user_toolchains: Default::default(),
2631        };
2632
2633        db.save_workspace(workspace_1.clone()).await;
2634        db.save_workspace(workspace_2.clone()).await;
2635
2636        // Test that paths are treated as a set
2637        assert_eq!(
2638            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2639            workspace_1
2640        );
2641        assert_eq!(
2642            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
2643            workspace_1
2644        );
2645
2646        // Make sure that other keys work
2647        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
2648        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
2649
2650        // Test 'mutate' case of updating a pre-existing id
2651        workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
2652
2653        db.save_workspace(workspace_2.clone()).await;
2654        assert_eq!(
2655            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2656            workspace_2
2657        );
2658
2659        // Test other mechanism for mutating
2660        let mut workspace_3 = SerializedWorkspace {
2661            id: WorkspaceId(3),
2662            paths: PathList::new(&["/tmp2", "/tmp"]),
2663            location: SerializedWorkspaceLocation::Local,
2664            center_group: Default::default(),
2665            window_bounds: Default::default(),
2666            breakpoints: Default::default(),
2667            display: Default::default(),
2668            docks: Default::default(),
2669            centered_layout: false,
2670            session_id: None,
2671            window_id: Some(3),
2672            user_toolchains: Default::default(),
2673        };
2674
2675        db.save_workspace(workspace_3.clone()).await;
2676        assert_eq!(
2677            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2678            workspace_3
2679        );
2680
2681        // Make sure that updating paths differently also works
2682        workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
2683        db.save_workspace(workspace_3.clone()).await;
2684        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2685        assert_eq!(
2686            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2687                .unwrap(),
2688            workspace_3
2689        );
2690    }
2691
2692    #[gpui::test]
2693    async fn test_session_workspaces() {
2694        zlog::init_test();
2695
2696        let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
2697
2698        let workspace_1 = SerializedWorkspace {
2699            id: WorkspaceId(1),
2700            paths: PathList::new(&["/tmp1"]),
2701            location: SerializedWorkspaceLocation::Local,
2702            center_group: Default::default(),
2703            window_bounds: Default::default(),
2704            display: Default::default(),
2705            docks: Default::default(),
2706            centered_layout: false,
2707            breakpoints: Default::default(),
2708            session_id: Some("session-id-1".to_owned()),
2709            window_id: Some(10),
2710            user_toolchains: Default::default(),
2711        };
2712
2713        let workspace_2 = SerializedWorkspace {
2714            id: WorkspaceId(2),
2715            paths: PathList::new(&["/tmp2"]),
2716            location: SerializedWorkspaceLocation::Local,
2717            center_group: Default::default(),
2718            window_bounds: Default::default(),
2719            display: Default::default(),
2720            docks: Default::default(),
2721            centered_layout: false,
2722            breakpoints: Default::default(),
2723            session_id: Some("session-id-1".to_owned()),
2724            window_id: Some(20),
2725            user_toolchains: Default::default(),
2726        };
2727
2728        let workspace_3 = SerializedWorkspace {
2729            id: WorkspaceId(3),
2730            paths: PathList::new(&["/tmp3"]),
2731            location: SerializedWorkspaceLocation::Local,
2732            center_group: Default::default(),
2733            window_bounds: Default::default(),
2734            display: Default::default(),
2735            docks: Default::default(),
2736            centered_layout: false,
2737            breakpoints: Default::default(),
2738            session_id: Some("session-id-2".to_owned()),
2739            window_id: Some(30),
2740            user_toolchains: Default::default(),
2741        };
2742
2743        let workspace_4 = SerializedWorkspace {
2744            id: WorkspaceId(4),
2745            paths: PathList::new(&["/tmp4"]),
2746            location: SerializedWorkspaceLocation::Local,
2747            center_group: Default::default(),
2748            window_bounds: Default::default(),
2749            display: Default::default(),
2750            docks: Default::default(),
2751            centered_layout: false,
2752            breakpoints: Default::default(),
2753            session_id: None,
2754            window_id: None,
2755            user_toolchains: Default::default(),
2756        };
2757
2758        let connection_id = db
2759            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2760                host: "my-host".into(),
2761                port: Some(1234),
2762                ..Default::default()
2763            }))
2764            .await
2765            .unwrap();
2766
2767        let workspace_5 = SerializedWorkspace {
2768            id: WorkspaceId(5),
2769            paths: PathList::default(),
2770            location: SerializedWorkspaceLocation::Remote(
2771                db.remote_connection(connection_id).unwrap(),
2772            ),
2773            center_group: Default::default(),
2774            window_bounds: Default::default(),
2775            display: Default::default(),
2776            docks: Default::default(),
2777            centered_layout: false,
2778            breakpoints: Default::default(),
2779            session_id: Some("session-id-2".to_owned()),
2780            window_id: Some(50),
2781            user_toolchains: Default::default(),
2782        };
2783
2784        let workspace_6 = SerializedWorkspace {
2785            id: WorkspaceId(6),
2786            paths: PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2787            location: SerializedWorkspaceLocation::Local,
2788            center_group: Default::default(),
2789            window_bounds: Default::default(),
2790            breakpoints: Default::default(),
2791            display: Default::default(),
2792            docks: Default::default(),
2793            centered_layout: false,
2794            session_id: Some("session-id-3".to_owned()),
2795            window_id: Some(60),
2796            user_toolchains: Default::default(),
2797        };
2798
2799        db.save_workspace(workspace_1.clone()).await;
2800        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2801        db.save_workspace(workspace_2.clone()).await;
2802        db.save_workspace(workspace_3.clone()).await;
2803        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2804        db.save_workspace(workspace_4.clone()).await;
2805        db.save_workspace(workspace_5.clone()).await;
2806        db.save_workspace(workspace_6.clone()).await;
2807
2808        let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2809        assert_eq!(locations.len(), 2);
2810        assert_eq!(locations[0].0, PathList::new(&["/tmp2"]));
2811        assert_eq!(locations[0].1, Some(20));
2812        assert_eq!(locations[1].0, PathList::new(&["/tmp1"]));
2813        assert_eq!(locations[1].1, Some(10));
2814
2815        let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
2816        assert_eq!(locations.len(), 2);
2817        assert_eq!(locations[0].0, PathList::default());
2818        assert_eq!(locations[0].1, Some(50));
2819        assert_eq!(locations[0].2, Some(connection_id));
2820        assert_eq!(locations[1].0, PathList::new(&["/tmp3"]));
2821        assert_eq!(locations[1].1, Some(30));
2822
2823        let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
2824        assert_eq!(locations.len(), 1);
2825        assert_eq!(
2826            locations[0].0,
2827            PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2828        );
2829        assert_eq!(locations[0].1, Some(60));
2830    }
2831
2832    fn default_workspace<P: AsRef<Path>>(
2833        paths: &[P],
2834        center_group: &SerializedPaneGroup,
2835    ) -> SerializedWorkspace {
2836        SerializedWorkspace {
2837            id: WorkspaceId(4),
2838            paths: PathList::new(paths),
2839            location: SerializedWorkspaceLocation::Local,
2840            center_group: center_group.clone(),
2841            window_bounds: Default::default(),
2842            display: Default::default(),
2843            docks: Default::default(),
2844            breakpoints: Default::default(),
2845            centered_layout: false,
2846            session_id: None,
2847            window_id: None,
2848            user_toolchains: Default::default(),
2849        }
2850    }
2851
2852    #[gpui::test]
2853    async fn test_last_session_workspace_locations() {
2854        let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
2855        let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
2856        let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
2857        let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
2858
2859        let db =
2860            WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
2861
2862        let workspaces = [
2863            (1, vec![dir1.path()], 9),
2864            (2, vec![dir2.path()], 5),
2865            (3, vec![dir3.path()], 8),
2866            (4, vec![dir4.path()], 2),
2867            (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
2868            (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
2869        ]
2870        .into_iter()
2871        .map(|(id, paths, window_id)| SerializedWorkspace {
2872            id: WorkspaceId(id),
2873            paths: PathList::new(paths.as_slice()),
2874            location: SerializedWorkspaceLocation::Local,
2875            center_group: Default::default(),
2876            window_bounds: Default::default(),
2877            display: Default::default(),
2878            docks: Default::default(),
2879            centered_layout: false,
2880            session_id: Some("one-session".to_owned()),
2881            breakpoints: Default::default(),
2882            window_id: Some(window_id),
2883            user_toolchains: Default::default(),
2884        })
2885        .collect::<Vec<_>>();
2886
2887        for workspace in workspaces.iter() {
2888            db.save_workspace(workspace.clone()).await;
2889        }
2890
2891        let stack = Some(Vec::from([
2892            WindowId::from(2), // Top
2893            WindowId::from(8),
2894            WindowId::from(5),
2895            WindowId::from(9),
2896            WindowId::from(3),
2897            WindowId::from(4), // Bottom
2898        ]));
2899
2900        let locations = db
2901            .last_session_workspace_locations("one-session", stack)
2902            .unwrap();
2903        assert_eq!(
2904            locations,
2905            [
2906                (
2907                    SerializedWorkspaceLocation::Local,
2908                    PathList::new(&[dir4.path()])
2909                ),
2910                (
2911                    SerializedWorkspaceLocation::Local,
2912                    PathList::new(&[dir3.path()])
2913                ),
2914                (
2915                    SerializedWorkspaceLocation::Local,
2916                    PathList::new(&[dir2.path()])
2917                ),
2918                (
2919                    SerializedWorkspaceLocation::Local,
2920                    PathList::new(&[dir1.path()])
2921                ),
2922                (
2923                    SerializedWorkspaceLocation::Local,
2924                    PathList::new(&[dir1.path(), dir2.path(), dir3.path()])
2925                ),
2926                (
2927                    SerializedWorkspaceLocation::Local,
2928                    PathList::new(&[dir4.path(), dir3.path(), dir2.path()])
2929                ),
2930            ]
2931        );
2932    }
2933
2934    #[gpui::test]
2935    async fn test_last_session_workspace_locations_remote() {
2936        let db =
2937            WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
2938                .await;
2939
2940        let remote_connections = [
2941            ("host-1", "my-user-1"),
2942            ("host-2", "my-user-2"),
2943            ("host-3", "my-user-3"),
2944            ("host-4", "my-user-4"),
2945        ]
2946        .into_iter()
2947        .map(|(host, user)| async {
2948            let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
2949                host: host.into(),
2950                username: Some(user.to_string()),
2951                ..Default::default()
2952            });
2953            db.get_or_create_remote_connection(options.clone())
2954                .await
2955                .unwrap();
2956            options
2957        })
2958        .collect::<Vec<_>>();
2959
2960        let remote_connections = futures::future::join_all(remote_connections).await;
2961
2962        let workspaces = [
2963            (1, remote_connections[0].clone(), 9),
2964            (2, remote_connections[1].clone(), 5),
2965            (3, remote_connections[2].clone(), 8),
2966            (4, remote_connections[3].clone(), 2),
2967        ]
2968        .into_iter()
2969        .map(|(id, remote_connection, window_id)| SerializedWorkspace {
2970            id: WorkspaceId(id),
2971            paths: PathList::default(),
2972            location: SerializedWorkspaceLocation::Remote(remote_connection),
2973            center_group: Default::default(),
2974            window_bounds: Default::default(),
2975            display: Default::default(),
2976            docks: Default::default(),
2977            centered_layout: false,
2978            session_id: Some("one-session".to_owned()),
2979            breakpoints: Default::default(),
2980            window_id: Some(window_id),
2981            user_toolchains: Default::default(),
2982        })
2983        .collect::<Vec<_>>();
2984
2985        for workspace in workspaces.iter() {
2986            db.save_workspace(workspace.clone()).await;
2987        }
2988
2989        let stack = Some(Vec::from([
2990            WindowId::from(2), // Top
2991            WindowId::from(8),
2992            WindowId::from(5),
2993            WindowId::from(9), // Bottom
2994        ]));
2995
2996        let have = db
2997            .last_session_workspace_locations("one-session", stack)
2998            .unwrap();
2999        assert_eq!(have.len(), 4);
3000        assert_eq!(
3001            have[0],
3002            (
3003                SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
3004                PathList::default()
3005            )
3006        );
3007        assert_eq!(
3008            have[1],
3009            (
3010                SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
3011                PathList::default()
3012            )
3013        );
3014        assert_eq!(
3015            have[2],
3016            (
3017                SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
3018                PathList::default()
3019            )
3020        );
3021        assert_eq!(
3022            have[3],
3023            (
3024                SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
3025                PathList::default()
3026            )
3027        );
3028    }
3029
3030    #[gpui::test]
3031    async fn test_get_or_create_ssh_project() {
3032        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
3033
3034        let host = "example.com".to_string();
3035        let port = Some(22_u16);
3036        let user = Some("user".to_string());
3037
3038        let connection_id = db
3039            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3040                host: host.clone().into(),
3041                port,
3042                username: user.clone(),
3043                ..Default::default()
3044            }))
3045            .await
3046            .unwrap();
3047
3048        // Test that calling the function again with the same parameters returns the same project
3049        let same_connection = db
3050            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3051                host: host.clone().into(),
3052                port,
3053                username: user.clone(),
3054                ..Default::default()
3055            }))
3056            .await
3057            .unwrap();
3058
3059        assert_eq!(connection_id, same_connection);
3060
3061        // Test with different parameters
3062        let host2 = "otherexample.com".to_string();
3063        let port2 = None;
3064        let user2 = Some("otheruser".to_string());
3065
3066        let different_connection = db
3067            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3068                host: host2.clone().into(),
3069                port: port2,
3070                username: user2.clone(),
3071                ..Default::default()
3072            }))
3073            .await
3074            .unwrap();
3075
3076        assert_ne!(connection_id, different_connection);
3077    }
3078
3079    #[gpui::test]
3080    async fn test_get_or_create_ssh_project_with_null_user() {
3081        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
3082
3083        let (host, port, user) = ("example.com".to_string(), None, None);
3084
3085        let connection_id = db
3086            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3087                host: host.clone().into(),
3088                port,
3089                username: None,
3090                ..Default::default()
3091            }))
3092            .await
3093            .unwrap();
3094
3095        let same_connection_id = db
3096            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3097                host: host.clone().into(),
3098                port,
3099                username: user.clone(),
3100                ..Default::default()
3101            }))
3102            .await
3103            .unwrap();
3104
3105        assert_eq!(connection_id, same_connection_id);
3106    }
3107
3108    #[gpui::test]
3109    async fn test_get_remote_connections() {
3110        let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
3111
3112        let connections = [
3113            ("example.com".to_string(), None, None),
3114            (
3115                "anotherexample.com".to_string(),
3116                Some(123_u16),
3117                Some("user2".to_string()),
3118            ),
3119            ("yetanother.com".to_string(), Some(345_u16), None),
3120        ];
3121
3122        let mut ids = Vec::new();
3123        for (host, port, user) in connections.iter() {
3124            ids.push(
3125                db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
3126                    SshConnectionOptions {
3127                        host: host.clone().into(),
3128                        port: *port,
3129                        username: user.clone(),
3130                        ..Default::default()
3131                    },
3132                ))
3133                .await
3134                .unwrap(),
3135            );
3136        }
3137
3138        let stored_connections = db.remote_connections().unwrap();
3139        assert_eq!(
3140            stored_connections,
3141            [
3142                (
3143                    ids[0],
3144                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3145                        host: "example.com".into(),
3146                        port: None,
3147                        username: None,
3148                        ..Default::default()
3149                    }),
3150                ),
3151                (
3152                    ids[1],
3153                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3154                        host: "anotherexample.com".into(),
3155                        port: Some(123),
3156                        username: Some("user2".into()),
3157                        ..Default::default()
3158                    }),
3159                ),
3160                (
3161                    ids[2],
3162                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3163                        host: "yetanother.com".into(),
3164                        port: Some(345),
3165                        username: None,
3166                        ..Default::default()
3167                    }),
3168                ),
3169            ]
3170            .into_iter()
3171            .collect::<HashMap<_, _>>(),
3172        );
3173    }
3174
3175    #[gpui::test]
3176    async fn test_simple_split() {
3177        zlog::init_test();
3178
3179        let db = WorkspaceDb::open_test_db("simple_split").await;
3180
3181        //  -----------------
3182        //  | 1,2   | 5,6   |
3183        //  | - - - |       |
3184        //  | 3,4   |       |
3185        //  -----------------
3186        let center_pane = group(
3187            Axis::Horizontal,
3188            vec![
3189                group(
3190                    Axis::Vertical,
3191                    vec![
3192                        SerializedPaneGroup::Pane(SerializedPane::new(
3193                            vec![
3194                                SerializedItem::new("Terminal", 1, false, false),
3195                                SerializedItem::new("Terminal", 2, true, false),
3196                            ],
3197                            false,
3198                            0,
3199                        )),
3200                        SerializedPaneGroup::Pane(SerializedPane::new(
3201                            vec![
3202                                SerializedItem::new("Terminal", 4, false, false),
3203                                SerializedItem::new("Terminal", 3, true, false),
3204                            ],
3205                            true,
3206                            0,
3207                        )),
3208                    ],
3209                ),
3210                SerializedPaneGroup::Pane(SerializedPane::new(
3211                    vec![
3212                        SerializedItem::new("Terminal", 5, true, false),
3213                        SerializedItem::new("Terminal", 6, false, false),
3214                    ],
3215                    false,
3216                    0,
3217                )),
3218            ],
3219        );
3220
3221        let workspace = default_workspace(&["/tmp"], &center_pane);
3222
3223        db.save_workspace(workspace.clone()).await;
3224
3225        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
3226
3227        assert_eq!(workspace.center_group, new_workspace.center_group);
3228    }
3229
3230    #[gpui::test]
3231    async fn test_cleanup_panes() {
3232        zlog::init_test();
3233
3234        let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
3235
3236        let center_pane = group(
3237            Axis::Horizontal,
3238            vec![
3239                group(
3240                    Axis::Vertical,
3241                    vec![
3242                        SerializedPaneGroup::Pane(SerializedPane::new(
3243                            vec![
3244                                SerializedItem::new("Terminal", 1, false, false),
3245                                SerializedItem::new("Terminal", 2, true, false),
3246                            ],
3247                            false,
3248                            0,
3249                        )),
3250                        SerializedPaneGroup::Pane(SerializedPane::new(
3251                            vec![
3252                                SerializedItem::new("Terminal", 4, false, false),
3253                                SerializedItem::new("Terminal", 3, true, false),
3254                            ],
3255                            true,
3256                            0,
3257                        )),
3258                    ],
3259                ),
3260                SerializedPaneGroup::Pane(SerializedPane::new(
3261                    vec![
3262                        SerializedItem::new("Terminal", 5, false, false),
3263                        SerializedItem::new("Terminal", 6, true, false),
3264                    ],
3265                    false,
3266                    0,
3267                )),
3268            ],
3269        );
3270
3271        let id = &["/tmp"];
3272
3273        let mut workspace = default_workspace(id, &center_pane);
3274
3275        db.save_workspace(workspace.clone()).await;
3276
3277        workspace.center_group = group(
3278            Axis::Vertical,
3279            vec![
3280                SerializedPaneGroup::Pane(SerializedPane::new(
3281                    vec![
3282                        SerializedItem::new("Terminal", 1, false, false),
3283                        SerializedItem::new("Terminal", 2, true, false),
3284                    ],
3285                    false,
3286                    0,
3287                )),
3288                SerializedPaneGroup::Pane(SerializedPane::new(
3289                    vec![
3290                        SerializedItem::new("Terminal", 4, true, false),
3291                        SerializedItem::new("Terminal", 3, false, false),
3292                    ],
3293                    true,
3294                    0,
3295                )),
3296            ],
3297        );
3298
3299        db.save_workspace(workspace.clone()).await;
3300
3301        let new_workspace = db.workspace_for_roots(id).unwrap();
3302
3303        assert_eq!(workspace.center_group, new_workspace.center_group);
3304    }
3305}