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    ) -> anyhow::Result<()> {
1923        use anyhow::Context as _;
1924        use db::sqlez::statement::Statement;
1925        use itertools::Itertools as _;
1926
1927        DB.clear_trusted_worktrees()
1928            .await
1929            .context("clearing previous trust state")?;
1930
1931        let trusted_worktrees = trusted_worktrees
1932            .into_iter()
1933            .flat_map(|(host, abs_paths)| {
1934                abs_paths
1935                    .into_iter()
1936                    .map(move |abs_path| (Some(abs_path), host.clone()))
1937            })
1938            .collect::<Vec<_>>();
1939        let mut first_worktree;
1940        let mut last_worktree = 0_usize;
1941        for (count, placeholders) in std::iter::once("(?, ?, ?)")
1942            .cycle()
1943            .take(trusted_worktrees.len())
1944            .chunks(MAX_QUERY_PLACEHOLDERS / 3)
1945            .into_iter()
1946            .map(|chunk| {
1947                let mut count = 0;
1948                let placeholders = chunk
1949                    .inspect(|_| {
1950                        count += 1;
1951                    })
1952                    .join(", ");
1953                (count, placeholders)
1954            })
1955            .collect::<Vec<_>>()
1956        {
1957            first_worktree = last_worktree;
1958            last_worktree = last_worktree + count;
1959            let query = format!(
1960                r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name)
1961VALUES {placeholders};"#
1962            );
1963
1964            let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec();
1965            self.write(move |conn| {
1966                let mut statement = Statement::prepare(conn, query)?;
1967                let mut next_index = 1;
1968                for (abs_path, host) in trusted_worktrees {
1969                    let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy());
1970                    next_index = statement.bind(
1971                        &abs_path.as_ref().map(|abs_path| abs_path.as_ref()),
1972                        next_index,
1973                    )?;
1974                    next_index = statement.bind(
1975                        &host
1976                            .as_ref()
1977                            .and_then(|host| Some(host.user_name.as_ref()?.as_str())),
1978                        next_index,
1979                    )?;
1980                    next_index = statement.bind(
1981                        &host.as_ref().map(|host| host.host_identifier.as_str()),
1982                        next_index,
1983                    )?;
1984                }
1985                statement.exec()
1986            })
1987            .await
1988            .context("inserting new trusted state")?;
1989        }
1990        Ok(())
1991    }
1992
1993    pub fn fetch_trusted_worktrees(
1994        &self,
1995        worktree_store: Option<Entity<WorktreeStore>>,
1996        host: Option<RemoteHostLocation>,
1997        cx: &App,
1998    ) -> Result<HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>> {
1999        let trusted_worktrees = DB.trusted_worktrees()?;
2000        Ok(trusted_worktrees
2001            .into_iter()
2002            .filter_map(|(abs_path, user_name, host_name)| {
2003                let db_host = match (user_name, host_name) {
2004                    (_, None) => None,
2005                    (None, Some(host_name)) => Some(RemoteHostLocation {
2006                        user_name: None,
2007                        host_identifier: SharedString::new(host_name),
2008                    }),
2009                    (Some(user_name), Some(host_name)) => Some(RemoteHostLocation {
2010                        user_name: Some(SharedString::new(user_name)),
2011                        host_identifier: SharedString::new(host_name),
2012                    }),
2013                };
2014
2015                let abs_path = abs_path?;
2016                Some(if db_host != host {
2017                    (db_host, PathTrust::AbsPath(abs_path))
2018                } else if let Some(worktree_store) = &worktree_store {
2019                    find_worktree_in_store(worktree_store.read(cx), &abs_path, cx)
2020                        .map(PathTrust::Worktree)
2021                        .map(|trusted_worktree| (host.clone(), trusted_worktree))
2022                        .unwrap_or_else(|| (db_host.clone(), PathTrust::AbsPath(abs_path)))
2023                } else {
2024                    (db_host, PathTrust::AbsPath(abs_path))
2025                })
2026            })
2027            .fold(HashMap::default(), |mut acc, (remote_host, path_trust)| {
2028                acc.entry(remote_host)
2029                    .or_insert_with(HashSet::default)
2030                    .insert(path_trust);
2031                acc
2032            }))
2033    }
2034
2035    query! {
2036        fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
2037            SELECT absolute_path, user_name, host_name
2038            FROM trusted_worktrees
2039        }
2040    }
2041
2042    query! {
2043        pub async fn clear_trusted_worktrees() -> Result<()> {
2044            DELETE FROM trusted_worktrees
2045        }
2046    }
2047}
2048
2049pub fn delete_unloaded_items(
2050    alive_items: Vec<ItemId>,
2051    workspace_id: WorkspaceId,
2052    table: &'static str,
2053    db: &ThreadSafeConnection,
2054    cx: &mut App,
2055) -> Task<Result<()>> {
2056    let db = db.clone();
2057    cx.spawn(async move |_| {
2058        let placeholders = alive_items
2059            .iter()
2060            .map(|_| "?")
2061            .collect::<Vec<&str>>()
2062            .join(", ");
2063
2064        let query = format!(
2065            "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
2066        );
2067
2068        db.write(move |conn| {
2069            let mut statement = Statement::prepare(conn, query)?;
2070            let mut next_index = statement.bind(&workspace_id, 1)?;
2071            for id in alive_items {
2072                next_index = statement.bind(&id, next_index)?;
2073            }
2074            statement.exec()
2075        })
2076        .await
2077    })
2078}
2079
2080#[cfg(test)]
2081mod tests {
2082    use super::*;
2083    use crate::persistence::model::{
2084        SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
2085    };
2086    use gpui;
2087    use pretty_assertions::assert_eq;
2088    use remote::SshConnectionOptions;
2089    use std::{thread, time::Duration};
2090
2091    #[gpui::test]
2092    async fn test_breakpoints() {
2093        zlog::init_test();
2094
2095        let db = WorkspaceDb::open_test_db("test_breakpoints").await;
2096        let id = db.next_id().await.unwrap();
2097
2098        let path = Path::new("/tmp/test.rs");
2099
2100        let breakpoint = Breakpoint {
2101            position: 123,
2102            message: None,
2103            state: BreakpointState::Enabled,
2104            condition: None,
2105            hit_condition: None,
2106        };
2107
2108        let log_breakpoint = Breakpoint {
2109            position: 456,
2110            message: Some("Test log message".into()),
2111            state: BreakpointState::Enabled,
2112            condition: None,
2113            hit_condition: None,
2114        };
2115
2116        let disable_breakpoint = Breakpoint {
2117            position: 578,
2118            message: None,
2119            state: BreakpointState::Disabled,
2120            condition: None,
2121            hit_condition: None,
2122        };
2123
2124        let condition_breakpoint = Breakpoint {
2125            position: 789,
2126            message: None,
2127            state: BreakpointState::Enabled,
2128            condition: Some("x > 5".into()),
2129            hit_condition: None,
2130        };
2131
2132        let hit_condition_breakpoint = Breakpoint {
2133            position: 999,
2134            message: None,
2135            state: BreakpointState::Enabled,
2136            condition: None,
2137            hit_condition: Some(">= 3".into()),
2138        };
2139
2140        let workspace = SerializedWorkspace {
2141            id,
2142            paths: PathList::new(&["/tmp"]),
2143            location: SerializedWorkspaceLocation::Local,
2144            center_group: Default::default(),
2145            window_bounds: Default::default(),
2146            display: Default::default(),
2147            docks: Default::default(),
2148            centered_layout: false,
2149            breakpoints: {
2150                let mut map = collections::BTreeMap::default();
2151                map.insert(
2152                    Arc::from(path),
2153                    vec![
2154                        SourceBreakpoint {
2155                            row: breakpoint.position,
2156                            path: Arc::from(path),
2157                            message: breakpoint.message.clone(),
2158                            state: breakpoint.state,
2159                            condition: breakpoint.condition.clone(),
2160                            hit_condition: breakpoint.hit_condition.clone(),
2161                        },
2162                        SourceBreakpoint {
2163                            row: log_breakpoint.position,
2164                            path: Arc::from(path),
2165                            message: log_breakpoint.message.clone(),
2166                            state: log_breakpoint.state,
2167                            condition: log_breakpoint.condition.clone(),
2168                            hit_condition: log_breakpoint.hit_condition.clone(),
2169                        },
2170                        SourceBreakpoint {
2171                            row: disable_breakpoint.position,
2172                            path: Arc::from(path),
2173                            message: disable_breakpoint.message.clone(),
2174                            state: disable_breakpoint.state,
2175                            condition: disable_breakpoint.condition.clone(),
2176                            hit_condition: disable_breakpoint.hit_condition.clone(),
2177                        },
2178                        SourceBreakpoint {
2179                            row: condition_breakpoint.position,
2180                            path: Arc::from(path),
2181                            message: condition_breakpoint.message.clone(),
2182                            state: condition_breakpoint.state,
2183                            condition: condition_breakpoint.condition.clone(),
2184                            hit_condition: condition_breakpoint.hit_condition.clone(),
2185                        },
2186                        SourceBreakpoint {
2187                            row: hit_condition_breakpoint.position,
2188                            path: Arc::from(path),
2189                            message: hit_condition_breakpoint.message.clone(),
2190                            state: hit_condition_breakpoint.state,
2191                            condition: hit_condition_breakpoint.condition.clone(),
2192                            hit_condition: hit_condition_breakpoint.hit_condition.clone(),
2193                        },
2194                    ],
2195                );
2196                map
2197            },
2198            session_id: None,
2199            window_id: None,
2200            user_toolchains: Default::default(),
2201        };
2202
2203        db.save_workspace(workspace.clone()).await;
2204
2205        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2206        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
2207
2208        assert_eq!(loaded_breakpoints.len(), 5);
2209
2210        // normal breakpoint
2211        assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
2212        assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
2213        assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
2214        assert_eq!(
2215            loaded_breakpoints[0].hit_condition,
2216            breakpoint.hit_condition
2217        );
2218        assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
2219        assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
2220
2221        // enabled breakpoint
2222        assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
2223        assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
2224        assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
2225        assert_eq!(
2226            loaded_breakpoints[1].hit_condition,
2227            log_breakpoint.hit_condition
2228        );
2229        assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
2230        assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
2231
2232        // disable breakpoint
2233        assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
2234        assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
2235        assert_eq!(
2236            loaded_breakpoints[2].condition,
2237            disable_breakpoint.condition
2238        );
2239        assert_eq!(
2240            loaded_breakpoints[2].hit_condition,
2241            disable_breakpoint.hit_condition
2242        );
2243        assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
2244        assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
2245
2246        // condition breakpoint
2247        assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
2248        assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
2249        assert_eq!(
2250            loaded_breakpoints[3].condition,
2251            condition_breakpoint.condition
2252        );
2253        assert_eq!(
2254            loaded_breakpoints[3].hit_condition,
2255            condition_breakpoint.hit_condition
2256        );
2257        assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
2258        assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
2259
2260        // hit condition breakpoint
2261        assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
2262        assert_eq!(
2263            loaded_breakpoints[4].message,
2264            hit_condition_breakpoint.message
2265        );
2266        assert_eq!(
2267            loaded_breakpoints[4].condition,
2268            hit_condition_breakpoint.condition
2269        );
2270        assert_eq!(
2271            loaded_breakpoints[4].hit_condition,
2272            hit_condition_breakpoint.hit_condition
2273        );
2274        assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
2275        assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
2276    }
2277
2278    #[gpui::test]
2279    async fn test_remove_last_breakpoint() {
2280        zlog::init_test();
2281
2282        let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
2283        let id = db.next_id().await.unwrap();
2284
2285        let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
2286
2287        let breakpoint_to_remove = Breakpoint {
2288            position: 100,
2289            message: None,
2290            state: BreakpointState::Enabled,
2291            condition: None,
2292            hit_condition: None,
2293        };
2294
2295        let workspace = SerializedWorkspace {
2296            id,
2297            paths: PathList::new(&["/tmp"]),
2298            location: SerializedWorkspaceLocation::Local,
2299            center_group: Default::default(),
2300            window_bounds: Default::default(),
2301            display: Default::default(),
2302            docks: Default::default(),
2303            centered_layout: false,
2304            breakpoints: {
2305                let mut map = collections::BTreeMap::default();
2306                map.insert(
2307                    Arc::from(singular_path),
2308                    vec![SourceBreakpoint {
2309                        row: breakpoint_to_remove.position,
2310                        path: Arc::from(singular_path),
2311                        message: None,
2312                        state: BreakpointState::Enabled,
2313                        condition: None,
2314                        hit_condition: None,
2315                    }],
2316                );
2317                map
2318            },
2319            session_id: None,
2320            window_id: None,
2321            user_toolchains: Default::default(),
2322        };
2323
2324        db.save_workspace(workspace.clone()).await;
2325
2326        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2327        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
2328
2329        assert_eq!(loaded_breakpoints.len(), 1);
2330        assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
2331        assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
2332        assert_eq!(
2333            loaded_breakpoints[0].condition,
2334            breakpoint_to_remove.condition
2335        );
2336        assert_eq!(
2337            loaded_breakpoints[0].hit_condition,
2338            breakpoint_to_remove.hit_condition
2339        );
2340        assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
2341        assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
2342
2343        let workspace_without_breakpoint = SerializedWorkspace {
2344            id,
2345            paths: PathList::new(&["/tmp"]),
2346            location: SerializedWorkspaceLocation::Local,
2347            center_group: Default::default(),
2348            window_bounds: Default::default(),
2349            display: Default::default(),
2350            docks: Default::default(),
2351            centered_layout: false,
2352            breakpoints: collections::BTreeMap::default(),
2353            session_id: None,
2354            window_id: None,
2355            user_toolchains: Default::default(),
2356        };
2357
2358        db.save_workspace(workspace_without_breakpoint.clone())
2359            .await;
2360
2361        let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
2362        let empty_breakpoints = loaded_after_remove
2363            .breakpoints
2364            .get(&Arc::from(singular_path));
2365
2366        assert!(empty_breakpoints.is_none());
2367    }
2368
2369    #[gpui::test]
2370    async fn test_next_id_stability() {
2371        zlog::init_test();
2372
2373        let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
2374
2375        db.write(|conn| {
2376            conn.migrate(
2377                "test_table",
2378                &[sql!(
2379                    CREATE TABLE test_table(
2380                        text TEXT,
2381                        workspace_id INTEGER,
2382                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2383                        ON DELETE CASCADE
2384                    ) STRICT;
2385                )],
2386                |_, _, _| false,
2387            )
2388            .unwrap();
2389        })
2390        .await;
2391
2392        let id = db.next_id().await.unwrap();
2393        // Assert the empty row got inserted
2394        assert_eq!(
2395            Some(id),
2396            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
2397                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
2398            ))
2399            .unwrap()(id)
2400            .unwrap()
2401        );
2402
2403        db.write(move |conn| {
2404            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2405                .unwrap()(("test-text-1", id))
2406            .unwrap()
2407        })
2408        .await;
2409
2410        let test_text_1 = db
2411            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2412            .unwrap()(1)
2413        .unwrap()
2414        .unwrap();
2415        assert_eq!(test_text_1, "test-text-1");
2416    }
2417
2418    #[gpui::test]
2419    async fn test_workspace_id_stability() {
2420        zlog::init_test();
2421
2422        let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
2423
2424        db.write(|conn| {
2425            conn.migrate(
2426                "test_table",
2427                &[sql!(
2428                        CREATE TABLE test_table(
2429                            text TEXT,
2430                            workspace_id INTEGER,
2431                            FOREIGN KEY(workspace_id)
2432                                REFERENCES workspaces(workspace_id)
2433                            ON DELETE CASCADE
2434                        ) STRICT;)],
2435                |_, _, _| false,
2436            )
2437        })
2438        .await
2439        .unwrap();
2440
2441        let mut workspace_1 = SerializedWorkspace {
2442            id: WorkspaceId(1),
2443            paths: PathList::new(&["/tmp", "/tmp2"]),
2444            location: SerializedWorkspaceLocation::Local,
2445            center_group: Default::default(),
2446            window_bounds: Default::default(),
2447            display: Default::default(),
2448            docks: Default::default(),
2449            centered_layout: false,
2450            breakpoints: Default::default(),
2451            session_id: None,
2452            window_id: None,
2453            user_toolchains: Default::default(),
2454        };
2455
2456        let workspace_2 = SerializedWorkspace {
2457            id: WorkspaceId(2),
2458            paths: PathList::new(&["/tmp"]),
2459            location: SerializedWorkspaceLocation::Local,
2460            center_group: Default::default(),
2461            window_bounds: Default::default(),
2462            display: Default::default(),
2463            docks: Default::default(),
2464            centered_layout: false,
2465            breakpoints: Default::default(),
2466            session_id: None,
2467            window_id: None,
2468            user_toolchains: Default::default(),
2469        };
2470
2471        db.save_workspace(workspace_1.clone()).await;
2472
2473        db.write(|conn| {
2474            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2475                .unwrap()(("test-text-1", 1))
2476            .unwrap();
2477        })
2478        .await;
2479
2480        db.save_workspace(workspace_2.clone()).await;
2481
2482        db.write(|conn| {
2483            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2484                .unwrap()(("test-text-2", 2))
2485            .unwrap();
2486        })
2487        .await;
2488
2489        workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
2490        db.save_workspace(workspace_1.clone()).await;
2491        db.save_workspace(workspace_1).await;
2492        db.save_workspace(workspace_2).await;
2493
2494        let test_text_2 = db
2495            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2496            .unwrap()(2)
2497        .unwrap()
2498        .unwrap();
2499        assert_eq!(test_text_2, "test-text-2");
2500
2501        let test_text_1 = db
2502            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2503            .unwrap()(1)
2504        .unwrap()
2505        .unwrap();
2506        assert_eq!(test_text_1, "test-text-1");
2507    }
2508
2509    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
2510        SerializedPaneGroup::Group {
2511            axis: SerializedAxis(axis),
2512            flexes: None,
2513            children,
2514        }
2515    }
2516
2517    #[gpui::test]
2518    async fn test_full_workspace_serialization() {
2519        zlog::init_test();
2520
2521        let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
2522
2523        //  -----------------
2524        //  | 1,2   | 5,6   |
2525        //  | - - - |       |
2526        //  | 3,4   |       |
2527        //  -----------------
2528        let center_group = group(
2529            Axis::Horizontal,
2530            vec![
2531                group(
2532                    Axis::Vertical,
2533                    vec![
2534                        SerializedPaneGroup::Pane(SerializedPane::new(
2535                            vec![
2536                                SerializedItem::new("Terminal", 5, false, false),
2537                                SerializedItem::new("Terminal", 6, true, false),
2538                            ],
2539                            false,
2540                            0,
2541                        )),
2542                        SerializedPaneGroup::Pane(SerializedPane::new(
2543                            vec![
2544                                SerializedItem::new("Terminal", 7, true, false),
2545                                SerializedItem::new("Terminal", 8, false, false),
2546                            ],
2547                            false,
2548                            0,
2549                        )),
2550                    ],
2551                ),
2552                SerializedPaneGroup::Pane(SerializedPane::new(
2553                    vec![
2554                        SerializedItem::new("Terminal", 9, false, false),
2555                        SerializedItem::new("Terminal", 10, true, false),
2556                    ],
2557                    false,
2558                    0,
2559                )),
2560            ],
2561        );
2562
2563        let workspace = SerializedWorkspace {
2564            id: WorkspaceId(5),
2565            paths: PathList::new(&["/tmp", "/tmp2"]),
2566            location: SerializedWorkspaceLocation::Local,
2567            center_group,
2568            window_bounds: Default::default(),
2569            breakpoints: Default::default(),
2570            display: Default::default(),
2571            docks: Default::default(),
2572            centered_layout: false,
2573            session_id: None,
2574            window_id: Some(999),
2575            user_toolchains: Default::default(),
2576        };
2577
2578        db.save_workspace(workspace.clone()).await;
2579
2580        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2581        assert_eq!(workspace, round_trip_workspace.unwrap());
2582
2583        // Test guaranteed duplicate IDs
2584        db.save_workspace(workspace.clone()).await;
2585        db.save_workspace(workspace.clone()).await;
2586
2587        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2588        assert_eq!(workspace, round_trip_workspace.unwrap());
2589    }
2590
2591    #[gpui::test]
2592    async fn test_workspace_assignment() {
2593        zlog::init_test();
2594
2595        let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2596
2597        let workspace_1 = SerializedWorkspace {
2598            id: WorkspaceId(1),
2599            paths: PathList::new(&["/tmp", "/tmp2"]),
2600            location: SerializedWorkspaceLocation::Local,
2601            center_group: Default::default(),
2602            window_bounds: Default::default(),
2603            breakpoints: Default::default(),
2604            display: Default::default(),
2605            docks: Default::default(),
2606            centered_layout: false,
2607            session_id: None,
2608            window_id: Some(1),
2609            user_toolchains: Default::default(),
2610        };
2611
2612        let mut workspace_2 = SerializedWorkspace {
2613            id: WorkspaceId(2),
2614            paths: PathList::new(&["/tmp"]),
2615            location: SerializedWorkspaceLocation::Local,
2616            center_group: Default::default(),
2617            window_bounds: Default::default(),
2618            display: Default::default(),
2619            docks: Default::default(),
2620            centered_layout: false,
2621            breakpoints: Default::default(),
2622            session_id: None,
2623            window_id: Some(2),
2624            user_toolchains: Default::default(),
2625        };
2626
2627        db.save_workspace(workspace_1.clone()).await;
2628        db.save_workspace(workspace_2.clone()).await;
2629
2630        // Test that paths are treated as a set
2631        assert_eq!(
2632            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2633            workspace_1
2634        );
2635        assert_eq!(
2636            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
2637            workspace_1
2638        );
2639
2640        // Make sure that other keys work
2641        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
2642        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
2643
2644        // Test 'mutate' case of updating a pre-existing id
2645        workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
2646
2647        db.save_workspace(workspace_2.clone()).await;
2648        assert_eq!(
2649            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2650            workspace_2
2651        );
2652
2653        // Test other mechanism for mutating
2654        let mut workspace_3 = SerializedWorkspace {
2655            id: WorkspaceId(3),
2656            paths: PathList::new(&["/tmp2", "/tmp"]),
2657            location: SerializedWorkspaceLocation::Local,
2658            center_group: Default::default(),
2659            window_bounds: Default::default(),
2660            breakpoints: Default::default(),
2661            display: Default::default(),
2662            docks: Default::default(),
2663            centered_layout: false,
2664            session_id: None,
2665            window_id: Some(3),
2666            user_toolchains: Default::default(),
2667        };
2668
2669        db.save_workspace(workspace_3.clone()).await;
2670        assert_eq!(
2671            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2672            workspace_3
2673        );
2674
2675        // Make sure that updating paths differently also works
2676        workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
2677        db.save_workspace(workspace_3.clone()).await;
2678        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2679        assert_eq!(
2680            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2681                .unwrap(),
2682            workspace_3
2683        );
2684    }
2685
2686    #[gpui::test]
2687    async fn test_session_workspaces() {
2688        zlog::init_test();
2689
2690        let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
2691
2692        let workspace_1 = SerializedWorkspace {
2693            id: WorkspaceId(1),
2694            paths: PathList::new(&["/tmp1"]),
2695            location: SerializedWorkspaceLocation::Local,
2696            center_group: Default::default(),
2697            window_bounds: Default::default(),
2698            display: Default::default(),
2699            docks: Default::default(),
2700            centered_layout: false,
2701            breakpoints: Default::default(),
2702            session_id: Some("session-id-1".to_owned()),
2703            window_id: Some(10),
2704            user_toolchains: Default::default(),
2705        };
2706
2707        let workspace_2 = SerializedWorkspace {
2708            id: WorkspaceId(2),
2709            paths: PathList::new(&["/tmp2"]),
2710            location: SerializedWorkspaceLocation::Local,
2711            center_group: Default::default(),
2712            window_bounds: Default::default(),
2713            display: Default::default(),
2714            docks: Default::default(),
2715            centered_layout: false,
2716            breakpoints: Default::default(),
2717            session_id: Some("session-id-1".to_owned()),
2718            window_id: Some(20),
2719            user_toolchains: Default::default(),
2720        };
2721
2722        let workspace_3 = SerializedWorkspace {
2723            id: WorkspaceId(3),
2724            paths: PathList::new(&["/tmp3"]),
2725            location: SerializedWorkspaceLocation::Local,
2726            center_group: Default::default(),
2727            window_bounds: Default::default(),
2728            display: Default::default(),
2729            docks: Default::default(),
2730            centered_layout: false,
2731            breakpoints: Default::default(),
2732            session_id: Some("session-id-2".to_owned()),
2733            window_id: Some(30),
2734            user_toolchains: Default::default(),
2735        };
2736
2737        let workspace_4 = SerializedWorkspace {
2738            id: WorkspaceId(4),
2739            paths: PathList::new(&["/tmp4"]),
2740            location: SerializedWorkspaceLocation::Local,
2741            center_group: Default::default(),
2742            window_bounds: Default::default(),
2743            display: Default::default(),
2744            docks: Default::default(),
2745            centered_layout: false,
2746            breakpoints: Default::default(),
2747            session_id: None,
2748            window_id: None,
2749            user_toolchains: Default::default(),
2750        };
2751
2752        let connection_id = db
2753            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2754                host: "my-host".into(),
2755                port: Some(1234),
2756                ..Default::default()
2757            }))
2758            .await
2759            .unwrap();
2760
2761        let workspace_5 = SerializedWorkspace {
2762            id: WorkspaceId(5),
2763            paths: PathList::default(),
2764            location: SerializedWorkspaceLocation::Remote(
2765                db.remote_connection(connection_id).unwrap(),
2766            ),
2767            center_group: Default::default(),
2768            window_bounds: Default::default(),
2769            display: Default::default(),
2770            docks: Default::default(),
2771            centered_layout: false,
2772            breakpoints: Default::default(),
2773            session_id: Some("session-id-2".to_owned()),
2774            window_id: Some(50),
2775            user_toolchains: Default::default(),
2776        };
2777
2778        let workspace_6 = SerializedWorkspace {
2779            id: WorkspaceId(6),
2780            paths: PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2781            location: SerializedWorkspaceLocation::Local,
2782            center_group: Default::default(),
2783            window_bounds: Default::default(),
2784            breakpoints: Default::default(),
2785            display: Default::default(),
2786            docks: Default::default(),
2787            centered_layout: false,
2788            session_id: Some("session-id-3".to_owned()),
2789            window_id: Some(60),
2790            user_toolchains: Default::default(),
2791        };
2792
2793        db.save_workspace(workspace_1.clone()).await;
2794        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2795        db.save_workspace(workspace_2.clone()).await;
2796        db.save_workspace(workspace_3.clone()).await;
2797        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2798        db.save_workspace(workspace_4.clone()).await;
2799        db.save_workspace(workspace_5.clone()).await;
2800        db.save_workspace(workspace_6.clone()).await;
2801
2802        let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2803        assert_eq!(locations.len(), 2);
2804        assert_eq!(locations[0].0, PathList::new(&["/tmp2"]));
2805        assert_eq!(locations[0].1, Some(20));
2806        assert_eq!(locations[1].0, PathList::new(&["/tmp1"]));
2807        assert_eq!(locations[1].1, Some(10));
2808
2809        let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
2810        assert_eq!(locations.len(), 2);
2811        assert_eq!(locations[0].0, PathList::default());
2812        assert_eq!(locations[0].1, Some(50));
2813        assert_eq!(locations[0].2, Some(connection_id));
2814        assert_eq!(locations[1].0, PathList::new(&["/tmp3"]));
2815        assert_eq!(locations[1].1, Some(30));
2816
2817        let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
2818        assert_eq!(locations.len(), 1);
2819        assert_eq!(
2820            locations[0].0,
2821            PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2822        );
2823        assert_eq!(locations[0].1, Some(60));
2824    }
2825
2826    fn default_workspace<P: AsRef<Path>>(
2827        paths: &[P],
2828        center_group: &SerializedPaneGroup,
2829    ) -> SerializedWorkspace {
2830        SerializedWorkspace {
2831            id: WorkspaceId(4),
2832            paths: PathList::new(paths),
2833            location: SerializedWorkspaceLocation::Local,
2834            center_group: center_group.clone(),
2835            window_bounds: Default::default(),
2836            display: Default::default(),
2837            docks: Default::default(),
2838            breakpoints: Default::default(),
2839            centered_layout: false,
2840            session_id: None,
2841            window_id: None,
2842            user_toolchains: Default::default(),
2843        }
2844    }
2845
2846    #[gpui::test]
2847    async fn test_last_session_workspace_locations() {
2848        let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
2849        let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
2850        let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
2851        let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
2852
2853        let db =
2854            WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
2855
2856        let workspaces = [
2857            (1, vec![dir1.path()], 9),
2858            (2, vec![dir2.path()], 5),
2859            (3, vec![dir3.path()], 8),
2860            (4, vec![dir4.path()], 2),
2861            (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
2862            (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
2863        ]
2864        .into_iter()
2865        .map(|(id, paths, window_id)| SerializedWorkspace {
2866            id: WorkspaceId(id),
2867            paths: PathList::new(paths.as_slice()),
2868            location: SerializedWorkspaceLocation::Local,
2869            center_group: Default::default(),
2870            window_bounds: Default::default(),
2871            display: Default::default(),
2872            docks: Default::default(),
2873            centered_layout: false,
2874            session_id: Some("one-session".to_owned()),
2875            breakpoints: Default::default(),
2876            window_id: Some(window_id),
2877            user_toolchains: Default::default(),
2878        })
2879        .collect::<Vec<_>>();
2880
2881        for workspace in workspaces.iter() {
2882            db.save_workspace(workspace.clone()).await;
2883        }
2884
2885        let stack = Some(Vec::from([
2886            WindowId::from(2), // Top
2887            WindowId::from(8),
2888            WindowId::from(5),
2889            WindowId::from(9),
2890            WindowId::from(3),
2891            WindowId::from(4), // Bottom
2892        ]));
2893
2894        let locations = db
2895            .last_session_workspace_locations("one-session", stack)
2896            .unwrap();
2897        assert_eq!(
2898            locations,
2899            [
2900                (
2901                    SerializedWorkspaceLocation::Local,
2902                    PathList::new(&[dir4.path()])
2903                ),
2904                (
2905                    SerializedWorkspaceLocation::Local,
2906                    PathList::new(&[dir3.path()])
2907                ),
2908                (
2909                    SerializedWorkspaceLocation::Local,
2910                    PathList::new(&[dir2.path()])
2911                ),
2912                (
2913                    SerializedWorkspaceLocation::Local,
2914                    PathList::new(&[dir1.path()])
2915                ),
2916                (
2917                    SerializedWorkspaceLocation::Local,
2918                    PathList::new(&[dir1.path(), dir2.path(), dir3.path()])
2919                ),
2920                (
2921                    SerializedWorkspaceLocation::Local,
2922                    PathList::new(&[dir4.path(), dir3.path(), dir2.path()])
2923                ),
2924            ]
2925        );
2926    }
2927
2928    #[gpui::test]
2929    async fn test_last_session_workspace_locations_remote() {
2930        let db =
2931            WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
2932                .await;
2933
2934        let remote_connections = [
2935            ("host-1", "my-user-1"),
2936            ("host-2", "my-user-2"),
2937            ("host-3", "my-user-3"),
2938            ("host-4", "my-user-4"),
2939        ]
2940        .into_iter()
2941        .map(|(host, user)| async {
2942            let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
2943                host: host.into(),
2944                username: Some(user.to_string()),
2945                ..Default::default()
2946            });
2947            db.get_or_create_remote_connection(options.clone())
2948                .await
2949                .unwrap();
2950            options
2951        })
2952        .collect::<Vec<_>>();
2953
2954        let remote_connections = futures::future::join_all(remote_connections).await;
2955
2956        let workspaces = [
2957            (1, remote_connections[0].clone(), 9),
2958            (2, remote_connections[1].clone(), 5),
2959            (3, remote_connections[2].clone(), 8),
2960            (4, remote_connections[3].clone(), 2),
2961        ]
2962        .into_iter()
2963        .map(|(id, remote_connection, window_id)| SerializedWorkspace {
2964            id: WorkspaceId(id),
2965            paths: PathList::default(),
2966            location: SerializedWorkspaceLocation::Remote(remote_connection),
2967            center_group: Default::default(),
2968            window_bounds: Default::default(),
2969            display: Default::default(),
2970            docks: Default::default(),
2971            centered_layout: false,
2972            session_id: Some("one-session".to_owned()),
2973            breakpoints: Default::default(),
2974            window_id: Some(window_id),
2975            user_toolchains: Default::default(),
2976        })
2977        .collect::<Vec<_>>();
2978
2979        for workspace in workspaces.iter() {
2980            db.save_workspace(workspace.clone()).await;
2981        }
2982
2983        let stack = Some(Vec::from([
2984            WindowId::from(2), // Top
2985            WindowId::from(8),
2986            WindowId::from(5),
2987            WindowId::from(9), // Bottom
2988        ]));
2989
2990        let have = db
2991            .last_session_workspace_locations("one-session", stack)
2992            .unwrap();
2993        assert_eq!(have.len(), 4);
2994        assert_eq!(
2995            have[0],
2996            (
2997                SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
2998                PathList::default()
2999            )
3000        );
3001        assert_eq!(
3002            have[1],
3003            (
3004                SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
3005                PathList::default()
3006            )
3007        );
3008        assert_eq!(
3009            have[2],
3010            (
3011                SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
3012                PathList::default()
3013            )
3014        );
3015        assert_eq!(
3016            have[3],
3017            (
3018                SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
3019                PathList::default()
3020            )
3021        );
3022    }
3023
3024    #[gpui::test]
3025    async fn test_get_or_create_ssh_project() {
3026        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
3027
3028        let host = "example.com".to_string();
3029        let port = Some(22_u16);
3030        let user = Some("user".to_string());
3031
3032        let connection_id = db
3033            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3034                host: host.clone().into(),
3035                port,
3036                username: user.clone(),
3037                ..Default::default()
3038            }))
3039            .await
3040            .unwrap();
3041
3042        // Test that calling the function again with the same parameters returns the same project
3043        let same_connection = db
3044            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3045                host: host.clone().into(),
3046                port,
3047                username: user.clone(),
3048                ..Default::default()
3049            }))
3050            .await
3051            .unwrap();
3052
3053        assert_eq!(connection_id, same_connection);
3054
3055        // Test with different parameters
3056        let host2 = "otherexample.com".to_string();
3057        let port2 = None;
3058        let user2 = Some("otheruser".to_string());
3059
3060        let different_connection = db
3061            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3062                host: host2.clone().into(),
3063                port: port2,
3064                username: user2.clone(),
3065                ..Default::default()
3066            }))
3067            .await
3068            .unwrap();
3069
3070        assert_ne!(connection_id, different_connection);
3071    }
3072
3073    #[gpui::test]
3074    async fn test_get_or_create_ssh_project_with_null_user() {
3075        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
3076
3077        let (host, port, user) = ("example.com".to_string(), None, None);
3078
3079        let connection_id = db
3080            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3081                host: host.clone().into(),
3082                port,
3083                username: None,
3084                ..Default::default()
3085            }))
3086            .await
3087            .unwrap();
3088
3089        let same_connection_id = db
3090            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3091                host: host.clone().into(),
3092                port,
3093                username: user.clone(),
3094                ..Default::default()
3095            }))
3096            .await
3097            .unwrap();
3098
3099        assert_eq!(connection_id, same_connection_id);
3100    }
3101
3102    #[gpui::test]
3103    async fn test_get_remote_connections() {
3104        let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
3105
3106        let connections = [
3107            ("example.com".to_string(), None, None),
3108            (
3109                "anotherexample.com".to_string(),
3110                Some(123_u16),
3111                Some("user2".to_string()),
3112            ),
3113            ("yetanother.com".to_string(), Some(345_u16), None),
3114        ];
3115
3116        let mut ids = Vec::new();
3117        for (host, port, user) in connections.iter() {
3118            ids.push(
3119                db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
3120                    SshConnectionOptions {
3121                        host: host.clone().into(),
3122                        port: *port,
3123                        username: user.clone(),
3124                        ..Default::default()
3125                    },
3126                ))
3127                .await
3128                .unwrap(),
3129            );
3130        }
3131
3132        let stored_connections = db.remote_connections().unwrap();
3133        assert_eq!(
3134            stored_connections,
3135            [
3136                (
3137                    ids[0],
3138                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3139                        host: "example.com".into(),
3140                        port: None,
3141                        username: None,
3142                        ..Default::default()
3143                    }),
3144                ),
3145                (
3146                    ids[1],
3147                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3148                        host: "anotherexample.com".into(),
3149                        port: Some(123),
3150                        username: Some("user2".into()),
3151                        ..Default::default()
3152                    }),
3153                ),
3154                (
3155                    ids[2],
3156                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
3157                        host: "yetanother.com".into(),
3158                        port: Some(345),
3159                        username: None,
3160                        ..Default::default()
3161                    }),
3162                ),
3163            ]
3164            .into_iter()
3165            .collect::<HashMap<_, _>>(),
3166        );
3167    }
3168
3169    #[gpui::test]
3170    async fn test_simple_split() {
3171        zlog::init_test();
3172
3173        let db = WorkspaceDb::open_test_db("simple_split").await;
3174
3175        //  -----------------
3176        //  | 1,2   | 5,6   |
3177        //  | - - - |       |
3178        //  | 3,4   |       |
3179        //  -----------------
3180        let center_pane = group(
3181            Axis::Horizontal,
3182            vec![
3183                group(
3184                    Axis::Vertical,
3185                    vec![
3186                        SerializedPaneGroup::Pane(SerializedPane::new(
3187                            vec![
3188                                SerializedItem::new("Terminal", 1, false, false),
3189                                SerializedItem::new("Terminal", 2, true, false),
3190                            ],
3191                            false,
3192                            0,
3193                        )),
3194                        SerializedPaneGroup::Pane(SerializedPane::new(
3195                            vec![
3196                                SerializedItem::new("Terminal", 4, false, false),
3197                                SerializedItem::new("Terminal", 3, true, false),
3198                            ],
3199                            true,
3200                            0,
3201                        )),
3202                    ],
3203                ),
3204                SerializedPaneGroup::Pane(SerializedPane::new(
3205                    vec![
3206                        SerializedItem::new("Terminal", 5, true, false),
3207                        SerializedItem::new("Terminal", 6, false, false),
3208                    ],
3209                    false,
3210                    0,
3211                )),
3212            ],
3213        );
3214
3215        let workspace = default_workspace(&["/tmp"], &center_pane);
3216
3217        db.save_workspace(workspace.clone()).await;
3218
3219        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
3220
3221        assert_eq!(workspace.center_group, new_workspace.center_group);
3222    }
3223
3224    #[gpui::test]
3225    async fn test_cleanup_panes() {
3226        zlog::init_test();
3227
3228        let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
3229
3230        let center_pane = group(
3231            Axis::Horizontal,
3232            vec![
3233                group(
3234                    Axis::Vertical,
3235                    vec![
3236                        SerializedPaneGroup::Pane(SerializedPane::new(
3237                            vec![
3238                                SerializedItem::new("Terminal", 1, false, false),
3239                                SerializedItem::new("Terminal", 2, true, false),
3240                            ],
3241                            false,
3242                            0,
3243                        )),
3244                        SerializedPaneGroup::Pane(SerializedPane::new(
3245                            vec![
3246                                SerializedItem::new("Terminal", 4, false, false),
3247                                SerializedItem::new("Terminal", 3, true, false),
3248                            ],
3249                            true,
3250                            0,
3251                        )),
3252                    ],
3253                ),
3254                SerializedPaneGroup::Pane(SerializedPane::new(
3255                    vec![
3256                        SerializedItem::new("Terminal", 5, false, false),
3257                        SerializedItem::new("Terminal", 6, true, false),
3258                    ],
3259                    false,
3260                    0,
3261                )),
3262            ],
3263        );
3264
3265        let id = &["/tmp"];
3266
3267        let mut workspace = default_workspace(id, &center_pane);
3268
3269        db.save_workspace(workspace.clone()).await;
3270
3271        workspace.center_group = group(
3272            Axis::Vertical,
3273            vec![
3274                SerializedPaneGroup::Pane(SerializedPane::new(
3275                    vec![
3276                        SerializedItem::new("Terminal", 1, false, false),
3277                        SerializedItem::new("Terminal", 2, true, false),
3278                    ],
3279                    false,
3280                    0,
3281                )),
3282                SerializedPaneGroup::Pane(SerializedPane::new(
3283                    vec![
3284                        SerializedItem::new("Terminal", 4, true, false),
3285                        SerializedItem::new("Terminal", 3, false, false),
3286                    ],
3287                    true,
3288                    0,
3289                )),
3290            ],
3291        );
3292
3293        db.save_workspace(workspace.clone()).await;
3294
3295        let new_workspace = db.workspace_for_roots(id).unwrap();
3296
3297        assert_eq!(workspace.center_group, new_workspace.center_group);
3298    }
3299}