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