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