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