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 = self.0.0 as i32;
 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            .map(|mut prepared_statement| {
 795                (prepared_statement)((
 796                    root_paths.serialize().paths,
 797                    remote_connection_id.map(|id| id.0 as i32),
 798                ))
 799                .unwrap()
 800            })
 801            .context("No workspaces found")
 802            .warn_on_err()
 803            .flatten()?;
 804
 805        let paths = PathList::deserialize(&SerializedPathList {
 806            paths,
 807            order: paths_order,
 808        });
 809
 810        Some(SerializedWorkspace {
 811            id: workspace_id,
 812            location: SerializedWorkspaceLocation::Local,
 813            paths,
 814            center_group: self
 815                .get_center_pane_group(workspace_id)
 816                .context("Getting center group")
 817                .log_err()?,
 818            window_bounds,
 819            centered_layout: centered_layout.unwrap_or(false),
 820            display,
 821            docks,
 822            session_id: None,
 823            breakpoints: self.breakpoints(workspace_id),
 824            window_id,
 825            user_toolchains: self.user_toolchains(workspace_id, remote_connection_id),
 826        })
 827    }
 828
 829    fn breakpoints(&self, workspace_id: WorkspaceId) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
 830        let breakpoints: Result<Vec<(PathBuf, Breakpoint)>> = self
 831            .select_bound(sql! {
 832                SELECT path, breakpoint_location, log_message, condition, hit_condition, state
 833                FROM breakpoints
 834                WHERE workspace_id = ?
 835            })
 836            .and_then(|mut prepared_statement| (prepared_statement)(workspace_id));
 837
 838        match breakpoints {
 839            Ok(bp) => {
 840                if bp.is_empty() {
 841                    log::debug!("Breakpoints are empty after querying database for them");
 842                }
 843
 844                let mut map: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> = Default::default();
 845
 846                for (path, breakpoint) in bp {
 847                    let path: Arc<Path> = path.into();
 848                    map.entry(path.clone()).or_default().push(SourceBreakpoint {
 849                        row: breakpoint.position,
 850                        path,
 851                        message: breakpoint.message,
 852                        condition: breakpoint.condition,
 853                        hit_condition: breakpoint.hit_condition,
 854                        state: breakpoint.state,
 855                    });
 856                }
 857
 858                for (path, bps) in map.iter() {
 859                    log::info!(
 860                        "Got {} breakpoints from database at path: {}",
 861                        bps.len(),
 862                        path.to_string_lossy()
 863                    );
 864                }
 865
 866                map
 867            }
 868            Err(msg) => {
 869                log::error!("Breakpoints query failed with msg: {msg}");
 870                Default::default()
 871            }
 872        }
 873    }
 874
 875    fn user_toolchains(
 876        &self,
 877        workspace_id: WorkspaceId,
 878        remote_connection_id: Option<RemoteConnectionId>,
 879    ) -> BTreeMap<ToolchainScope, IndexSet<Toolchain>> {
 880        type RowKind = (WorkspaceId, u64, String, String, String, String, String);
 881
 882        let toolchains: Vec<RowKind> = self
 883            .select_bound(sql! {
 884                SELECT workspace_id, worktree_id, relative_worktree_path,
 885                language_name, name, path, raw_json
 886                FROM user_toolchains WHERE remote_connection_id IS ?1 AND (
 887                      workspace_id IN (0, ?2)
 888                )
 889            })
 890            .and_then(|mut statement| {
 891                (statement)((remote_connection_id.map(|id| id.0), workspace_id))
 892            })
 893            .unwrap_or_default();
 894        let mut ret = BTreeMap::<_, IndexSet<_>>::default();
 895
 896        for (
 897            _workspace_id,
 898            worktree_id,
 899            relative_worktree_path,
 900            language_name,
 901            name,
 902            path,
 903            raw_json,
 904        ) in toolchains
 905        {
 906            // INTEGER's that are primary keys (like workspace ids, remote connection ids and such) start at 1, so we're safe to
 907            let scope = if _workspace_id == WorkspaceId(0) {
 908                debug_assert_eq!(worktree_id, u64::MAX);
 909                debug_assert_eq!(relative_worktree_path, String::default());
 910                ToolchainScope::Global
 911            } else {
 912                debug_assert_eq!(workspace_id, _workspace_id);
 913                debug_assert_eq!(
 914                    worktree_id == u64::MAX,
 915                    relative_worktree_path == String::default()
 916                );
 917
 918                let Some(relative_path) = RelPath::unix(&relative_worktree_path).log_err() else {
 919                    continue;
 920                };
 921                if worktree_id != u64::MAX && relative_worktree_path != String::default() {
 922                    ToolchainScope::Subproject(
 923                        WorktreeId::from_usize(worktree_id as usize),
 924                        relative_path.into(),
 925                    )
 926                } else {
 927                    ToolchainScope::Project
 928                }
 929            };
 930            let Ok(as_json) = serde_json::from_str(&raw_json) else {
 931                continue;
 932            };
 933            let toolchain = Toolchain {
 934                name: SharedString::from(name),
 935                path: SharedString::from(path),
 936                language_name: LanguageName::from_proto(language_name),
 937                as_json,
 938            };
 939            ret.entry(scope).or_default().insert(toolchain);
 940        }
 941
 942        ret
 943    }
 944
 945    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
 946    /// that used this workspace previously
 947    pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
 948        let paths = workspace.paths.serialize();
 949        log::debug!("Saving workspace at location: {:?}", workspace.location);
 950        self.write(move |conn| {
 951            conn.with_savepoint("update_worktrees", || {
 952                let remote_connection_id = match workspace.location.clone() {
 953                    SerializedWorkspaceLocation::Local => None,
 954                    SerializedWorkspaceLocation::Remote(connection_options) => {
 955                        Some(Self::get_or_create_remote_connection_internal(
 956                            conn,
 957                            connection_options
 958                        )?.0)
 959                    }
 960                };
 961
 962                // Clear out panes and pane_groups
 963                conn.exec_bound(sql!(
 964                    DELETE FROM pane_groups WHERE workspace_id = ?1;
 965                    DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
 966                    .context("Clearing old panes")?;
 967
 968                conn.exec_bound(
 969                    sql!(
 970                        DELETE FROM breakpoints WHERE workspace_id = ?1;
 971                    )
 972                )?(workspace.id).context("Clearing old breakpoints")?;
 973
 974                for (path, breakpoints) in workspace.breakpoints {
 975                    for bp in breakpoints {
 976                        let state = BreakpointStateWrapper::from(bp.state);
 977                        match conn.exec_bound(sql!(
 978                            INSERT INTO breakpoints (workspace_id, path, breakpoint_location,  log_message, condition, hit_condition, state)
 979                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);))?
 980
 981                        ((
 982                            workspace.id,
 983                            path.as_ref(),
 984                            bp.row,
 985                            bp.message,
 986                            bp.condition,
 987                            bp.hit_condition,
 988                            state,
 989                        )) {
 990                            Ok(_) => {
 991                                log::debug!("Stored breakpoint at row: {} in path: {}", bp.row, path.to_string_lossy())
 992                            }
 993                            Err(err) => {
 994                                log::error!("{err}");
 995                                continue;
 996                            }
 997                        }
 998                    }
 999                }
1000                for (scope, toolchains) in workspace.user_toolchains {
1001                    for toolchain in toolchains {
1002                        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));
1003                        let (workspace_id, worktree_id, relative_worktree_path) = match scope {
1004                            ToolchainScope::Subproject(worktree_id, ref path) => (Some(workspace.id), Some(worktree_id), Some(path.as_unix_str().to_owned())),
1005                            ToolchainScope::Project => (Some(workspace.id), None, None),
1006                            ToolchainScope::Global => (None, None, None),
1007                        };
1008                        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(),
1009                        toolchain.language_name.as_ref().to_owned(), toolchain.name.to_string(), toolchain.path.to_string(), toolchain.as_json.to_string());
1010                        if let Err(err) = conn.exec_bound(query)?(args) {
1011                            log::error!("{err}");
1012                            continue;
1013                        }
1014                    }
1015                }
1016
1017                conn.exec_bound(sql!(
1018                    DELETE
1019                    FROM workspaces
1020                    WHERE
1021                        workspace_id != ?1 AND
1022                        paths IS ?2 AND
1023                        remote_connection_id IS ?3
1024                ))?((
1025                    workspace.id,
1026                    paths.paths.clone(),
1027                    remote_connection_id,
1028                ))
1029                .context("clearing out old locations")?;
1030
1031                // Upsert
1032                let query = sql!(
1033                    INSERT INTO workspaces(
1034                        workspace_id,
1035                        paths,
1036                        paths_order,
1037                        remote_connection_id,
1038                        left_dock_visible,
1039                        left_dock_active_panel,
1040                        left_dock_zoom,
1041                        right_dock_visible,
1042                        right_dock_active_panel,
1043                        right_dock_zoom,
1044                        bottom_dock_visible,
1045                        bottom_dock_active_panel,
1046                        bottom_dock_zoom,
1047                        session_id,
1048                        window_id,
1049                        timestamp
1050                    )
1051                    VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, CURRENT_TIMESTAMP)
1052                    ON CONFLICT DO
1053                    UPDATE SET
1054                        paths = ?2,
1055                        paths_order = ?3,
1056                        remote_connection_id = ?4,
1057                        left_dock_visible = ?5,
1058                        left_dock_active_panel = ?6,
1059                        left_dock_zoom = ?7,
1060                        right_dock_visible = ?8,
1061                        right_dock_active_panel = ?9,
1062                        right_dock_zoom = ?10,
1063                        bottom_dock_visible = ?11,
1064                        bottom_dock_active_panel = ?12,
1065                        bottom_dock_zoom = ?13,
1066                        session_id = ?14,
1067                        window_id = ?15,
1068                        timestamp = CURRENT_TIMESTAMP
1069                );
1070                let mut prepared_query = conn.exec_bound(query)?;
1071                let args = (
1072                    workspace.id,
1073                    paths.paths.clone(),
1074                    paths.order.clone(),
1075                    remote_connection_id,
1076                    workspace.docks,
1077                    workspace.session_id,
1078                    workspace.window_id,
1079                );
1080
1081                prepared_query(args).context("Updating workspace")?;
1082
1083                // Save center pane group
1084                Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
1085                    .context("save pane group in save workspace")?;
1086
1087                Ok(())
1088            })
1089            .log_err();
1090        })
1091        .await;
1092    }
1093
1094    pub(crate) async fn get_or_create_remote_connection(
1095        &self,
1096        options: RemoteConnectionOptions,
1097    ) -> Result<RemoteConnectionId> {
1098        self.write(move |conn| Self::get_or_create_remote_connection_internal(conn, options))
1099            .await
1100    }
1101
1102    fn get_or_create_remote_connection_internal(
1103        this: &Connection,
1104        options: RemoteConnectionOptions,
1105    ) -> Result<RemoteConnectionId> {
1106        let kind;
1107        let user;
1108        let mut host = None;
1109        let mut port = None;
1110        let mut distro = None;
1111        match options {
1112            RemoteConnectionOptions::Ssh(options) => {
1113                kind = RemoteConnectionKind::Ssh;
1114                host = Some(options.host);
1115                port = options.port;
1116                user = options.username;
1117            }
1118            RemoteConnectionOptions::Wsl(options) => {
1119                kind = RemoteConnectionKind::Wsl;
1120                distro = Some(options.distro_name);
1121                user = options.user;
1122            }
1123        }
1124        Self::get_or_create_remote_connection_query(this, kind, host, port, user, distro)
1125    }
1126
1127    fn get_or_create_remote_connection_query(
1128        this: &Connection,
1129        kind: RemoteConnectionKind,
1130        host: Option<String>,
1131        port: Option<u16>,
1132        user: Option<String>,
1133        distro: Option<String>,
1134    ) -> Result<RemoteConnectionId> {
1135        if let Some(id) = this.select_row_bound(sql!(
1136            SELECT id
1137            FROM remote_connections
1138            WHERE
1139                kind IS ? AND
1140                host IS ? AND
1141                port IS ? AND
1142                user IS ? AND
1143                distro IS ?
1144            LIMIT 1
1145        ))?((
1146            kind.serialize(),
1147            host.clone(),
1148            port,
1149            user.clone(),
1150            distro.clone(),
1151        ))? {
1152            Ok(RemoteConnectionId(id))
1153        } else {
1154            let id = this.select_row_bound(sql!(
1155                INSERT INTO remote_connections (
1156                    kind,
1157                    host,
1158                    port,
1159                    user,
1160                    distro
1161                ) VALUES (?1, ?2, ?3, ?4, ?5)
1162                RETURNING id
1163            ))?((kind.serialize(), host, port, user, distro))?
1164            .context("failed to insert remote project")?;
1165            Ok(RemoteConnectionId(id))
1166        }
1167    }
1168
1169    query! {
1170        pub async fn next_id() -> Result<WorkspaceId> {
1171            INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
1172        }
1173    }
1174
1175    fn recent_workspaces(
1176        &self,
1177    ) -> Result<Vec<(WorkspaceId, PathList, Option<RemoteConnectionId>)>> {
1178        Ok(self
1179            .recent_workspaces_query()?
1180            .into_iter()
1181            .map(|(id, paths, order, remote_connection_id)| {
1182                (
1183                    id,
1184                    PathList::deserialize(&SerializedPathList { paths, order }),
1185                    remote_connection_id.map(RemoteConnectionId),
1186                )
1187            })
1188            .collect())
1189    }
1190
1191    query! {
1192        fn recent_workspaces_query() -> Result<Vec<(WorkspaceId, String, String, Option<u64>)>> {
1193            SELECT workspace_id, paths, paths_order, remote_connection_id
1194            FROM workspaces
1195            WHERE
1196                paths IS NOT NULL OR
1197                remote_connection_id IS NOT NULL
1198            ORDER BY timestamp DESC
1199        }
1200    }
1201
1202    fn session_workspaces(
1203        &self,
1204        session_id: String,
1205    ) -> Result<Vec<(PathList, Option<u64>, Option<RemoteConnectionId>)>> {
1206        Ok(self
1207            .session_workspaces_query(session_id)?
1208            .into_iter()
1209            .map(|(paths, order, window_id, remote_connection_id)| {
1210                (
1211                    PathList::deserialize(&SerializedPathList { paths, order }),
1212                    window_id,
1213                    remote_connection_id.map(RemoteConnectionId),
1214                )
1215            })
1216            .collect())
1217    }
1218
1219    query! {
1220        fn session_workspaces_query(session_id: String) -> Result<Vec<(String, String, Option<u64>, Option<u64>)>> {
1221            SELECT paths, paths_order, window_id, remote_connection_id
1222            FROM workspaces
1223            WHERE session_id = ?1
1224            ORDER BY timestamp DESC
1225        }
1226    }
1227
1228    query! {
1229        pub fn breakpoints_for_file(workspace_id: WorkspaceId, file_path: &Path) -> Result<Vec<Breakpoint>> {
1230            SELECT breakpoint_location
1231            FROM breakpoints
1232            WHERE  workspace_id= ?1 AND path = ?2
1233        }
1234    }
1235
1236    query! {
1237        pub fn clear_breakpoints(file_path: &Path) -> Result<()> {
1238            DELETE FROM breakpoints
1239            WHERE file_path = ?2
1240        }
1241    }
1242
1243    fn remote_connections(&self) -> Result<HashMap<RemoteConnectionId, RemoteConnectionOptions>> {
1244        Ok(self.select(sql!(
1245            SELECT
1246                id, kind, host, port, user, distro
1247            FROM
1248                remote_connections
1249        ))?()?
1250        .into_iter()
1251        .filter_map(|(id, kind, host, port, user, distro)| {
1252            Some((
1253                RemoteConnectionId(id),
1254                Self::remote_connection_from_row(kind, host, port, user, distro)?,
1255            ))
1256        })
1257        .collect())
1258    }
1259
1260    pub(crate) fn remote_connection(
1261        &self,
1262        id: RemoteConnectionId,
1263    ) -> Result<RemoteConnectionOptions> {
1264        let (kind, host, port, user, distro) = self.select_row_bound(sql!(
1265            SELECT kind, host, port, user, distro
1266            FROM remote_connections
1267            WHERE id = ?
1268        ))?(id.0)?
1269        .context("no such remote connection")?;
1270        Self::remote_connection_from_row(kind, host, port, user, distro)
1271            .context("invalid remote_connection row")
1272    }
1273
1274    fn remote_connection_from_row(
1275        kind: String,
1276        host: Option<String>,
1277        port: Option<u16>,
1278        user: Option<String>,
1279        distro: Option<String>,
1280    ) -> Option<RemoteConnectionOptions> {
1281        match RemoteConnectionKind::deserialize(&kind)? {
1282            RemoteConnectionKind::Wsl => Some(RemoteConnectionOptions::Wsl(WslConnectionOptions {
1283                distro_name: distro?,
1284                user: user,
1285            })),
1286            RemoteConnectionKind::Ssh => Some(RemoteConnectionOptions::Ssh(SshConnectionOptions {
1287                host: host?,
1288                port,
1289                username: user,
1290                ..Default::default()
1291            })),
1292        }
1293    }
1294
1295    pub(crate) fn last_window(
1296        &self,
1297    ) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
1298        let mut prepared_query =
1299            self.select::<(Option<Uuid>, Option<SerializedWindowBounds>)>(sql!(
1300                SELECT
1301                display,
1302                window_state, window_x, window_y, window_width, window_height
1303                FROM workspaces
1304                WHERE paths
1305                IS NOT NULL
1306                ORDER BY timestamp DESC
1307                LIMIT 1
1308            ))?;
1309        let result = prepared_query()?;
1310        Ok(result.into_iter().next().unwrap_or((None, None)))
1311    }
1312
1313    query! {
1314        pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
1315            DELETE FROM workspaces
1316            WHERE workspace_id IS ?
1317        }
1318    }
1319
1320    // Returns the recent locations which are still valid on disk and deletes ones which no longer
1321    // exist.
1322    pub async fn recent_workspaces_on_disk(
1323        &self,
1324    ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
1325        let mut result = Vec::new();
1326        let mut delete_tasks = Vec::new();
1327        let remote_connections = self.remote_connections()?;
1328
1329        for (id, paths, remote_connection_id) in self.recent_workspaces()? {
1330            if let Some(remote_connection_id) = remote_connection_id {
1331                if let Some(connection_options) = remote_connections.get(&remote_connection_id) {
1332                    result.push((
1333                        id,
1334                        SerializedWorkspaceLocation::Remote(connection_options.clone()),
1335                        paths,
1336                    ));
1337                } else {
1338                    delete_tasks.push(self.delete_workspace_by_id(id));
1339                }
1340                continue;
1341            }
1342
1343            if paths.paths().iter().all(|path| path.exists())
1344                && paths.paths().iter().any(|path| path.is_dir())
1345            {
1346                result.push((id, SerializedWorkspaceLocation::Local, paths));
1347            } else {
1348                delete_tasks.push(self.delete_workspace_by_id(id));
1349            }
1350        }
1351
1352        futures::future::join_all(delete_tasks).await;
1353        Ok(result)
1354    }
1355
1356    pub async fn last_workspace(&self) -> Result<Option<(SerializedWorkspaceLocation, PathList)>> {
1357        Ok(self
1358            .recent_workspaces_on_disk()
1359            .await?
1360            .into_iter()
1361            .next()
1362            .map(|(_, location, paths)| (location, paths)))
1363    }
1364
1365    // Returns the locations of the workspaces that were still opened when the last
1366    // session was closed (i.e. when Zed was quit).
1367    // If `last_session_window_order` is provided, the returned locations are ordered
1368    // according to that.
1369    pub fn last_session_workspace_locations(
1370        &self,
1371        last_session_id: &str,
1372        last_session_window_stack: Option<Vec<WindowId>>,
1373    ) -> Result<Vec<(SerializedWorkspaceLocation, PathList)>> {
1374        let mut workspaces = Vec::new();
1375
1376        for (paths, window_id, remote_connection_id) in
1377            self.session_workspaces(last_session_id.to_owned())?
1378        {
1379            if let Some(remote_connection_id) = remote_connection_id {
1380                workspaces.push((
1381                    SerializedWorkspaceLocation::Remote(
1382                        self.remote_connection(remote_connection_id)?,
1383                    ),
1384                    paths,
1385                    window_id.map(WindowId::from),
1386                ));
1387            } else if paths.paths().iter().all(|path| path.exists())
1388                && paths.paths().iter().any(|path| path.is_dir())
1389            {
1390                workspaces.push((
1391                    SerializedWorkspaceLocation::Local,
1392                    paths,
1393                    window_id.map(WindowId::from),
1394                ));
1395            }
1396        }
1397
1398        if let Some(stack) = last_session_window_stack {
1399            workspaces.sort_by_key(|(_, _, window_id)| {
1400                window_id
1401                    .and_then(|id| stack.iter().position(|&order_id| order_id == id))
1402                    .unwrap_or(usize::MAX)
1403            });
1404        }
1405
1406        Ok(workspaces
1407            .into_iter()
1408            .map(|(location, paths, _)| (location, paths))
1409            .collect::<Vec<_>>())
1410    }
1411
1412    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1413        Ok(self
1414            .get_pane_group(workspace_id, None)?
1415            .into_iter()
1416            .next()
1417            .unwrap_or_else(|| {
1418                SerializedPaneGroup::Pane(SerializedPane {
1419                    active: true,
1420                    children: vec![],
1421                    pinned_count: 0,
1422                })
1423            }))
1424    }
1425
1426    fn get_pane_group(
1427        &self,
1428        workspace_id: WorkspaceId,
1429        group_id: Option<GroupId>,
1430    ) -> Result<Vec<SerializedPaneGroup>> {
1431        type GroupKey = (Option<GroupId>, WorkspaceId);
1432        type GroupOrPane = (
1433            Option<GroupId>,
1434            Option<SerializedAxis>,
1435            Option<PaneId>,
1436            Option<bool>,
1437            Option<usize>,
1438            Option<String>,
1439        );
1440        self.select_bound::<GroupKey, GroupOrPane>(sql!(
1441            SELECT group_id, axis, pane_id, active, pinned_count, flexes
1442                FROM (SELECT
1443                        group_id,
1444                        axis,
1445                        NULL as pane_id,
1446                        NULL as active,
1447                        NULL as pinned_count,
1448                        position,
1449                        parent_group_id,
1450                        workspace_id,
1451                        flexes
1452                      FROM pane_groups
1453                    UNION
1454                      SELECT
1455                        NULL,
1456                        NULL,
1457                        center_panes.pane_id,
1458                        panes.active as active,
1459                        pinned_count,
1460                        position,
1461                        parent_group_id,
1462                        panes.workspace_id as workspace_id,
1463                        NULL
1464                      FROM center_panes
1465                      JOIN panes ON center_panes.pane_id = panes.pane_id)
1466                WHERE parent_group_id IS ? AND workspace_id = ?
1467                ORDER BY position
1468        ))?((group_id, workspace_id))?
1469        .into_iter()
1470        .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1471            let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1472            if let Some((group_id, axis)) = group_id.zip(axis) {
1473                let flexes = flexes
1474                    .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1475                    .transpose()?;
1476
1477                Ok(SerializedPaneGroup::Group {
1478                    axis,
1479                    children: self.get_pane_group(workspace_id, Some(group_id))?,
1480                    flexes,
1481                })
1482            } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
1483                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
1484                    self.get_items(pane_id)?,
1485                    active,
1486                    pinned_count,
1487                )))
1488            } else {
1489                bail!("Pane Group Child was neither a pane group or a pane");
1490            }
1491        })
1492        // Filter out panes and pane groups which don't have any children or items
1493        .filter(|pane_group| match pane_group {
1494            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
1495            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
1496            _ => true,
1497        })
1498        .collect::<Result<_>>()
1499    }
1500
1501    fn save_pane_group(
1502        conn: &Connection,
1503        workspace_id: WorkspaceId,
1504        pane_group: &SerializedPaneGroup,
1505        parent: Option<(GroupId, usize)>,
1506    ) -> Result<()> {
1507        if parent.is_none() {
1508            log::debug!("Saving a pane group for workspace {workspace_id:?}");
1509        }
1510        match pane_group {
1511            SerializedPaneGroup::Group {
1512                axis,
1513                children,
1514                flexes,
1515            } => {
1516                let (parent_id, position) = parent.unzip();
1517
1518                let flex_string = flexes
1519                    .as_ref()
1520                    .map(|flexes| serde_json::json!(flexes).to_string());
1521
1522                let group_id = conn.select_row_bound::<_, i64>(sql!(
1523                    INSERT INTO pane_groups(
1524                        workspace_id,
1525                        parent_group_id,
1526                        position,
1527                        axis,
1528                        flexes
1529                    )
1530                    VALUES (?, ?, ?, ?, ?)
1531                    RETURNING group_id
1532                ))?((
1533                    workspace_id,
1534                    parent_id,
1535                    position,
1536                    *axis,
1537                    flex_string,
1538                ))?
1539                .context("Couldn't retrieve group_id from inserted pane_group")?;
1540
1541                for (position, group) in children.iter().enumerate() {
1542                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
1543                }
1544
1545                Ok(())
1546            }
1547            SerializedPaneGroup::Pane(pane) => {
1548                Self::save_pane(conn, workspace_id, pane, parent)?;
1549                Ok(())
1550            }
1551        }
1552    }
1553
1554    fn save_pane(
1555        conn: &Connection,
1556        workspace_id: WorkspaceId,
1557        pane: &SerializedPane,
1558        parent: Option<(GroupId, usize)>,
1559    ) -> Result<PaneId> {
1560        let pane_id = conn.select_row_bound::<_, i64>(sql!(
1561            INSERT INTO panes(workspace_id, active, pinned_count)
1562            VALUES (?, ?, ?)
1563            RETURNING pane_id
1564        ))?((workspace_id, pane.active, pane.pinned_count))?
1565        .context("Could not retrieve inserted pane_id")?;
1566
1567        let (parent_id, order) = parent.unzip();
1568        conn.exec_bound(sql!(
1569            INSERT INTO center_panes(pane_id, parent_group_id, position)
1570            VALUES (?, ?, ?)
1571        ))?((pane_id, parent_id, order))?;
1572
1573        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
1574
1575        Ok(pane_id)
1576    }
1577
1578    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
1579        self.select_bound(sql!(
1580            SELECT kind, item_id, active, preview FROM items
1581            WHERE pane_id = ?
1582                ORDER BY position
1583        ))?(pane_id)
1584    }
1585
1586    fn save_items(
1587        conn: &Connection,
1588        workspace_id: WorkspaceId,
1589        pane_id: PaneId,
1590        items: &[SerializedItem],
1591    ) -> Result<()> {
1592        let mut insert = conn.exec_bound(sql!(
1593            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
1594        )).context("Preparing insertion")?;
1595        for (position, item) in items.iter().enumerate() {
1596            insert((workspace_id, pane_id, position, item))?;
1597        }
1598
1599        Ok(())
1600    }
1601
1602    query! {
1603        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
1604            UPDATE workspaces
1605            SET timestamp = CURRENT_TIMESTAMP
1606            WHERE workspace_id = ?
1607        }
1608    }
1609
1610    query! {
1611        pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
1612            UPDATE workspaces
1613            SET window_state = ?2,
1614                window_x = ?3,
1615                window_y = ?4,
1616                window_width = ?5,
1617                window_height = ?6,
1618                display = ?7
1619            WHERE workspace_id = ?1
1620        }
1621    }
1622
1623    query! {
1624        pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
1625            UPDATE workspaces
1626            SET centered_layout = ?2
1627            WHERE workspace_id = ?1
1628        }
1629    }
1630
1631    query! {
1632        pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option<String>) -> Result<()> {
1633            UPDATE workspaces
1634            SET session_id = ?2
1635            WHERE workspace_id = ?1
1636        }
1637    }
1638
1639    pub async fn toolchain(
1640        &self,
1641        workspace_id: WorkspaceId,
1642        worktree_id: WorktreeId,
1643        relative_worktree_path: Arc<RelPath>,
1644        language_name: LanguageName,
1645    ) -> Result<Option<Toolchain>> {
1646        self.write(move |this| {
1647            let mut select = this
1648                .select_bound(sql!(
1649                    SELECT
1650                        name, path, raw_json
1651                    FROM toolchains
1652                    WHERE
1653                        workspace_id = ? AND
1654                        language_name = ? AND
1655                        worktree_id = ? AND
1656                        relative_worktree_path = ?
1657                ))
1658                .context("select toolchain")?;
1659
1660            let toolchain: Vec<(String, String, String)> = select((
1661                workspace_id,
1662                language_name.as_ref().to_string(),
1663                worktree_id.to_usize(),
1664                relative_worktree_path.as_unix_str().to_string(),
1665            ))?;
1666
1667            Ok(toolchain
1668                .into_iter()
1669                .next()
1670                .and_then(|(name, path, raw_json)| {
1671                    Some(Toolchain {
1672                        name: name.into(),
1673                        path: path.into(),
1674                        language_name,
1675                        as_json: serde_json::Value::from_str(&raw_json).ok()?,
1676                    })
1677                }))
1678        })
1679        .await
1680    }
1681
1682    pub(crate) async fn toolchains(
1683        &self,
1684        workspace_id: WorkspaceId,
1685    ) -> Result<Vec<(Toolchain, WorktreeId, Arc<RelPath>)>> {
1686        self.write(move |this| {
1687            let mut select = this
1688                .select_bound(sql!(
1689                    SELECT
1690                        name, path, worktree_id, relative_worktree_path, language_name, raw_json
1691                    FROM toolchains
1692                    WHERE workspace_id = ?
1693                ))
1694                .context("select toolchains")?;
1695
1696            let toolchain: Vec<(String, String, u64, String, String, String)> =
1697                select(workspace_id)?;
1698
1699            Ok(toolchain
1700                .into_iter()
1701                .filter_map(
1702                    |(name, path, worktree_id, relative_worktree_path, language, json)| {
1703                        Some((
1704                            Toolchain {
1705                                name: name.into(),
1706                                path: path.into(),
1707                                language_name: LanguageName::new(&language),
1708                                as_json: serde_json::Value::from_str(&json).ok()?,
1709                            },
1710                            WorktreeId::from_proto(worktree_id),
1711                            RelPath::from_proto(&relative_worktree_path).log_err()?,
1712                        ))
1713                    },
1714                )
1715                .collect())
1716        })
1717        .await
1718    }
1719
1720    pub async fn set_toolchain(
1721        &self,
1722        workspace_id: WorkspaceId,
1723        worktree_id: WorktreeId,
1724        relative_worktree_path: Arc<RelPath>,
1725        toolchain: Toolchain,
1726    ) -> Result<()> {
1727        log::debug!(
1728            "Setting toolchain for workspace, worktree: {worktree_id:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
1729            toolchain.name
1730        );
1731        self.write(move |conn| {
1732            let mut insert = conn
1733                .exec_bound(sql!(
1734                    INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?,  ?, ?)
1735                    ON CONFLICT DO
1736                    UPDATE SET
1737                        name = ?5,
1738                        path = ?6,
1739                        raw_json = ?7
1740                ))
1741                .context("Preparing insertion")?;
1742
1743            insert((
1744                workspace_id,
1745                worktree_id.to_usize(),
1746                relative_worktree_path.as_unix_str(),
1747                toolchain.language_name.as_ref(),
1748                toolchain.name.as_ref(),
1749                toolchain.path.as_ref(),
1750                toolchain.as_json.to_string(),
1751            ))?;
1752
1753            Ok(())
1754        }).await
1755    }
1756}
1757
1758pub fn delete_unloaded_items(
1759    alive_items: Vec<ItemId>,
1760    workspace_id: WorkspaceId,
1761    table: &'static str,
1762    db: &ThreadSafeConnection,
1763    cx: &mut App,
1764) -> Task<Result<()>> {
1765    let db = db.clone();
1766    cx.spawn(async move |_| {
1767        let placeholders = alive_items
1768            .iter()
1769            .map(|_| "?")
1770            .collect::<Vec<&str>>()
1771            .join(", ");
1772
1773        let query = format!(
1774            "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
1775        );
1776
1777        db.write(move |conn| {
1778            let mut statement = Statement::prepare(conn, query)?;
1779            let mut next_index = statement.bind(&workspace_id, 1)?;
1780            for id in alive_items {
1781                next_index = statement.bind(&id, next_index)?;
1782            }
1783            statement.exec()
1784        })
1785        .await
1786    })
1787}
1788
1789#[cfg(test)]
1790mod tests {
1791    use super::*;
1792    use crate::persistence::model::{
1793        SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
1794    };
1795    use gpui;
1796    use pretty_assertions::assert_eq;
1797    use remote::SshConnectionOptions;
1798    use std::{thread, time::Duration};
1799
1800    #[gpui::test]
1801    async fn test_breakpoints() {
1802        zlog::init_test();
1803
1804        let db = WorkspaceDb::open_test_db("test_breakpoints").await;
1805        let id = db.next_id().await.unwrap();
1806
1807        let path = Path::new("/tmp/test.rs");
1808
1809        let breakpoint = Breakpoint {
1810            position: 123,
1811            message: None,
1812            state: BreakpointState::Enabled,
1813            condition: None,
1814            hit_condition: None,
1815        };
1816
1817        let log_breakpoint = Breakpoint {
1818            position: 456,
1819            message: Some("Test log message".into()),
1820            state: BreakpointState::Enabled,
1821            condition: None,
1822            hit_condition: None,
1823        };
1824
1825        let disable_breakpoint = Breakpoint {
1826            position: 578,
1827            message: None,
1828            state: BreakpointState::Disabled,
1829            condition: None,
1830            hit_condition: None,
1831        };
1832
1833        let condition_breakpoint = Breakpoint {
1834            position: 789,
1835            message: None,
1836            state: BreakpointState::Enabled,
1837            condition: Some("x > 5".into()),
1838            hit_condition: None,
1839        };
1840
1841        let hit_condition_breakpoint = Breakpoint {
1842            position: 999,
1843            message: None,
1844            state: BreakpointState::Enabled,
1845            condition: None,
1846            hit_condition: Some(">= 3".into()),
1847        };
1848
1849        let workspace = SerializedWorkspace {
1850            id,
1851            paths: PathList::new(&["/tmp"]),
1852            location: SerializedWorkspaceLocation::Local,
1853            center_group: Default::default(),
1854            window_bounds: Default::default(),
1855            display: Default::default(),
1856            docks: Default::default(),
1857            centered_layout: false,
1858            breakpoints: {
1859                let mut map = collections::BTreeMap::default();
1860                map.insert(
1861                    Arc::from(path),
1862                    vec![
1863                        SourceBreakpoint {
1864                            row: breakpoint.position,
1865                            path: Arc::from(path),
1866                            message: breakpoint.message.clone(),
1867                            state: breakpoint.state,
1868                            condition: breakpoint.condition.clone(),
1869                            hit_condition: breakpoint.hit_condition.clone(),
1870                        },
1871                        SourceBreakpoint {
1872                            row: log_breakpoint.position,
1873                            path: Arc::from(path),
1874                            message: log_breakpoint.message.clone(),
1875                            state: log_breakpoint.state,
1876                            condition: log_breakpoint.condition.clone(),
1877                            hit_condition: log_breakpoint.hit_condition.clone(),
1878                        },
1879                        SourceBreakpoint {
1880                            row: disable_breakpoint.position,
1881                            path: Arc::from(path),
1882                            message: disable_breakpoint.message.clone(),
1883                            state: disable_breakpoint.state,
1884                            condition: disable_breakpoint.condition.clone(),
1885                            hit_condition: disable_breakpoint.hit_condition.clone(),
1886                        },
1887                        SourceBreakpoint {
1888                            row: condition_breakpoint.position,
1889                            path: Arc::from(path),
1890                            message: condition_breakpoint.message.clone(),
1891                            state: condition_breakpoint.state,
1892                            condition: condition_breakpoint.condition.clone(),
1893                            hit_condition: condition_breakpoint.hit_condition.clone(),
1894                        },
1895                        SourceBreakpoint {
1896                            row: hit_condition_breakpoint.position,
1897                            path: Arc::from(path),
1898                            message: hit_condition_breakpoint.message.clone(),
1899                            state: hit_condition_breakpoint.state,
1900                            condition: hit_condition_breakpoint.condition.clone(),
1901                            hit_condition: hit_condition_breakpoint.hit_condition.clone(),
1902                        },
1903                    ],
1904                );
1905                map
1906            },
1907            session_id: None,
1908            window_id: None,
1909            user_toolchains: Default::default(),
1910        };
1911
1912        db.save_workspace(workspace.clone()).await;
1913
1914        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
1915        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
1916
1917        assert_eq!(loaded_breakpoints.len(), 5);
1918
1919        // normal breakpoint
1920        assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
1921        assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
1922        assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
1923        assert_eq!(
1924            loaded_breakpoints[0].hit_condition,
1925            breakpoint.hit_condition
1926        );
1927        assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
1928        assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
1929
1930        // enabled breakpoint
1931        assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
1932        assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
1933        assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
1934        assert_eq!(
1935            loaded_breakpoints[1].hit_condition,
1936            log_breakpoint.hit_condition
1937        );
1938        assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
1939        assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
1940
1941        // disable breakpoint
1942        assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
1943        assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
1944        assert_eq!(
1945            loaded_breakpoints[2].condition,
1946            disable_breakpoint.condition
1947        );
1948        assert_eq!(
1949            loaded_breakpoints[2].hit_condition,
1950            disable_breakpoint.hit_condition
1951        );
1952        assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
1953        assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
1954
1955        // condition breakpoint
1956        assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
1957        assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
1958        assert_eq!(
1959            loaded_breakpoints[3].condition,
1960            condition_breakpoint.condition
1961        );
1962        assert_eq!(
1963            loaded_breakpoints[3].hit_condition,
1964            condition_breakpoint.hit_condition
1965        );
1966        assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
1967        assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
1968
1969        // hit condition breakpoint
1970        assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
1971        assert_eq!(
1972            loaded_breakpoints[4].message,
1973            hit_condition_breakpoint.message
1974        );
1975        assert_eq!(
1976            loaded_breakpoints[4].condition,
1977            hit_condition_breakpoint.condition
1978        );
1979        assert_eq!(
1980            loaded_breakpoints[4].hit_condition,
1981            hit_condition_breakpoint.hit_condition
1982        );
1983        assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
1984        assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
1985    }
1986
1987    #[gpui::test]
1988    async fn test_remove_last_breakpoint() {
1989        zlog::init_test();
1990
1991        let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
1992        let id = db.next_id().await.unwrap();
1993
1994        let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
1995
1996        let breakpoint_to_remove = Breakpoint {
1997            position: 100,
1998            message: None,
1999            state: BreakpointState::Enabled,
2000            condition: None,
2001            hit_condition: None,
2002        };
2003
2004        let workspace = SerializedWorkspace {
2005            id,
2006            paths: PathList::new(&["/tmp"]),
2007            location: SerializedWorkspaceLocation::Local,
2008            center_group: Default::default(),
2009            window_bounds: Default::default(),
2010            display: Default::default(),
2011            docks: Default::default(),
2012            centered_layout: false,
2013            breakpoints: {
2014                let mut map = collections::BTreeMap::default();
2015                map.insert(
2016                    Arc::from(singular_path),
2017                    vec![SourceBreakpoint {
2018                        row: breakpoint_to_remove.position,
2019                        path: Arc::from(singular_path),
2020                        message: None,
2021                        state: BreakpointState::Enabled,
2022                        condition: None,
2023                        hit_condition: None,
2024                    }],
2025                );
2026                map
2027            },
2028            session_id: None,
2029            window_id: None,
2030            user_toolchains: Default::default(),
2031        };
2032
2033        db.save_workspace(workspace.clone()).await;
2034
2035        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2036        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
2037
2038        assert_eq!(loaded_breakpoints.len(), 1);
2039        assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
2040        assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
2041        assert_eq!(
2042            loaded_breakpoints[0].condition,
2043            breakpoint_to_remove.condition
2044        );
2045        assert_eq!(
2046            loaded_breakpoints[0].hit_condition,
2047            breakpoint_to_remove.hit_condition
2048        );
2049        assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
2050        assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
2051
2052        let workspace_without_breakpoint = SerializedWorkspace {
2053            id,
2054            paths: PathList::new(&["/tmp"]),
2055            location: SerializedWorkspaceLocation::Local,
2056            center_group: Default::default(),
2057            window_bounds: Default::default(),
2058            display: Default::default(),
2059            docks: Default::default(),
2060            centered_layout: false,
2061            breakpoints: collections::BTreeMap::default(),
2062            session_id: None,
2063            window_id: None,
2064            user_toolchains: Default::default(),
2065        };
2066
2067        db.save_workspace(workspace_without_breakpoint.clone())
2068            .await;
2069
2070        let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
2071        let empty_breakpoints = loaded_after_remove
2072            .breakpoints
2073            .get(&Arc::from(singular_path));
2074
2075        assert!(empty_breakpoints.is_none());
2076    }
2077
2078    #[gpui::test]
2079    async fn test_next_id_stability() {
2080        zlog::init_test();
2081
2082        let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
2083
2084        db.write(|conn| {
2085            conn.migrate(
2086                "test_table",
2087                &[sql!(
2088                    CREATE TABLE test_table(
2089                        text TEXT,
2090                        workspace_id INTEGER,
2091                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2092                        ON DELETE CASCADE
2093                    ) STRICT;
2094                )],
2095                |_, _, _| false,
2096            )
2097            .unwrap();
2098        })
2099        .await;
2100
2101        let id = db.next_id().await.unwrap();
2102        // Assert the empty row got inserted
2103        assert_eq!(
2104            Some(id),
2105            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
2106                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
2107            ))
2108            .unwrap()(id)
2109            .unwrap()
2110        );
2111
2112        db.write(move |conn| {
2113            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2114                .unwrap()(("test-text-1", id))
2115            .unwrap()
2116        })
2117        .await;
2118
2119        let test_text_1 = db
2120            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2121            .unwrap()(1)
2122        .unwrap()
2123        .unwrap();
2124        assert_eq!(test_text_1, "test-text-1");
2125    }
2126
2127    #[gpui::test]
2128    async fn test_workspace_id_stability() {
2129        zlog::init_test();
2130
2131        let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
2132
2133        db.write(|conn| {
2134            conn.migrate(
2135                "test_table",
2136                &[sql!(
2137                        CREATE TABLE test_table(
2138                            text TEXT,
2139                            workspace_id INTEGER,
2140                            FOREIGN KEY(workspace_id)
2141                                REFERENCES workspaces(workspace_id)
2142                            ON DELETE CASCADE
2143                        ) STRICT;)],
2144                |_, _, _| false,
2145            )
2146        })
2147        .await
2148        .unwrap();
2149
2150        let mut workspace_1 = SerializedWorkspace {
2151            id: WorkspaceId(1),
2152            paths: PathList::new(&["/tmp", "/tmp2"]),
2153            location: SerializedWorkspaceLocation::Local,
2154            center_group: Default::default(),
2155            window_bounds: Default::default(),
2156            display: Default::default(),
2157            docks: Default::default(),
2158            centered_layout: false,
2159            breakpoints: Default::default(),
2160            session_id: None,
2161            window_id: None,
2162            user_toolchains: Default::default(),
2163        };
2164
2165        let workspace_2 = SerializedWorkspace {
2166            id: WorkspaceId(2),
2167            paths: PathList::new(&["/tmp"]),
2168            location: SerializedWorkspaceLocation::Local,
2169            center_group: Default::default(),
2170            window_bounds: Default::default(),
2171            display: Default::default(),
2172            docks: Default::default(),
2173            centered_layout: false,
2174            breakpoints: Default::default(),
2175            session_id: None,
2176            window_id: None,
2177            user_toolchains: Default::default(),
2178        };
2179
2180        db.save_workspace(workspace_1.clone()).await;
2181
2182        db.write(|conn| {
2183            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2184                .unwrap()(("test-text-1", 1))
2185            .unwrap();
2186        })
2187        .await;
2188
2189        db.save_workspace(workspace_2.clone()).await;
2190
2191        db.write(|conn| {
2192            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2193                .unwrap()(("test-text-2", 2))
2194            .unwrap();
2195        })
2196        .await;
2197
2198        workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
2199        db.save_workspace(workspace_1.clone()).await;
2200        db.save_workspace(workspace_1).await;
2201        db.save_workspace(workspace_2).await;
2202
2203        let test_text_2 = db
2204            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2205            .unwrap()(2)
2206        .unwrap()
2207        .unwrap();
2208        assert_eq!(test_text_2, "test-text-2");
2209
2210        let test_text_1 = db
2211            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2212            .unwrap()(1)
2213        .unwrap()
2214        .unwrap();
2215        assert_eq!(test_text_1, "test-text-1");
2216    }
2217
2218    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
2219        SerializedPaneGroup::Group {
2220            axis: SerializedAxis(axis),
2221            flexes: None,
2222            children,
2223        }
2224    }
2225
2226    #[gpui::test]
2227    async fn test_full_workspace_serialization() {
2228        zlog::init_test();
2229
2230        let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
2231
2232        //  -----------------
2233        //  | 1,2   | 5,6   |
2234        //  | - - - |       |
2235        //  | 3,4   |       |
2236        //  -----------------
2237        let center_group = group(
2238            Axis::Horizontal,
2239            vec![
2240                group(
2241                    Axis::Vertical,
2242                    vec![
2243                        SerializedPaneGroup::Pane(SerializedPane::new(
2244                            vec![
2245                                SerializedItem::new("Terminal", 5, false, false),
2246                                SerializedItem::new("Terminal", 6, true, false),
2247                            ],
2248                            false,
2249                            0,
2250                        )),
2251                        SerializedPaneGroup::Pane(SerializedPane::new(
2252                            vec![
2253                                SerializedItem::new("Terminal", 7, true, false),
2254                                SerializedItem::new("Terminal", 8, false, false),
2255                            ],
2256                            false,
2257                            0,
2258                        )),
2259                    ],
2260                ),
2261                SerializedPaneGroup::Pane(SerializedPane::new(
2262                    vec![
2263                        SerializedItem::new("Terminal", 9, false, false),
2264                        SerializedItem::new("Terminal", 10, true, false),
2265                    ],
2266                    false,
2267                    0,
2268                )),
2269            ],
2270        );
2271
2272        let workspace = SerializedWorkspace {
2273            id: WorkspaceId(5),
2274            paths: PathList::new(&["/tmp", "/tmp2"]),
2275            location: SerializedWorkspaceLocation::Local,
2276            center_group,
2277            window_bounds: Default::default(),
2278            breakpoints: Default::default(),
2279            display: Default::default(),
2280            docks: Default::default(),
2281            centered_layout: false,
2282            session_id: None,
2283            window_id: Some(999),
2284            user_toolchains: Default::default(),
2285        };
2286
2287        db.save_workspace(workspace.clone()).await;
2288
2289        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2290        assert_eq!(workspace, round_trip_workspace.unwrap());
2291
2292        // Test guaranteed duplicate IDs
2293        db.save_workspace(workspace.clone()).await;
2294        db.save_workspace(workspace.clone()).await;
2295
2296        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2297        assert_eq!(workspace, round_trip_workspace.unwrap());
2298    }
2299
2300    #[gpui::test]
2301    async fn test_workspace_assignment() {
2302        zlog::init_test();
2303
2304        let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2305
2306        let workspace_1 = SerializedWorkspace {
2307            id: WorkspaceId(1),
2308            paths: PathList::new(&["/tmp", "/tmp2"]),
2309            location: SerializedWorkspaceLocation::Local,
2310            center_group: Default::default(),
2311            window_bounds: Default::default(),
2312            breakpoints: Default::default(),
2313            display: Default::default(),
2314            docks: Default::default(),
2315            centered_layout: false,
2316            session_id: None,
2317            window_id: Some(1),
2318            user_toolchains: Default::default(),
2319        };
2320
2321        let mut workspace_2 = SerializedWorkspace {
2322            id: WorkspaceId(2),
2323            paths: PathList::new(&["/tmp"]),
2324            location: SerializedWorkspaceLocation::Local,
2325            center_group: Default::default(),
2326            window_bounds: Default::default(),
2327            display: Default::default(),
2328            docks: Default::default(),
2329            centered_layout: false,
2330            breakpoints: Default::default(),
2331            session_id: None,
2332            window_id: Some(2),
2333            user_toolchains: Default::default(),
2334        };
2335
2336        db.save_workspace(workspace_1.clone()).await;
2337        db.save_workspace(workspace_2.clone()).await;
2338
2339        // Test that paths are treated as a set
2340        assert_eq!(
2341            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2342            workspace_1
2343        );
2344        assert_eq!(
2345            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
2346            workspace_1
2347        );
2348
2349        // Make sure that other keys work
2350        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
2351        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
2352
2353        // Test 'mutate' case of updating a pre-existing id
2354        workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
2355
2356        db.save_workspace(workspace_2.clone()).await;
2357        assert_eq!(
2358            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2359            workspace_2
2360        );
2361
2362        // Test other mechanism for mutating
2363        let mut workspace_3 = SerializedWorkspace {
2364            id: WorkspaceId(3),
2365            paths: PathList::new(&["/tmp2", "/tmp"]),
2366            location: SerializedWorkspaceLocation::Local,
2367            center_group: Default::default(),
2368            window_bounds: Default::default(),
2369            breakpoints: Default::default(),
2370            display: Default::default(),
2371            docks: Default::default(),
2372            centered_layout: false,
2373            session_id: None,
2374            window_id: Some(3),
2375            user_toolchains: Default::default(),
2376        };
2377
2378        db.save_workspace(workspace_3.clone()).await;
2379        assert_eq!(
2380            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2381            workspace_3
2382        );
2383
2384        // Make sure that updating paths differently also works
2385        workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
2386        db.save_workspace(workspace_3.clone()).await;
2387        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2388        assert_eq!(
2389            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2390                .unwrap(),
2391            workspace_3
2392        );
2393    }
2394
2395    #[gpui::test]
2396    async fn test_session_workspaces() {
2397        zlog::init_test();
2398
2399        let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
2400
2401        let workspace_1 = SerializedWorkspace {
2402            id: WorkspaceId(1),
2403            paths: PathList::new(&["/tmp1"]),
2404            location: SerializedWorkspaceLocation::Local,
2405            center_group: Default::default(),
2406            window_bounds: Default::default(),
2407            display: Default::default(),
2408            docks: Default::default(),
2409            centered_layout: false,
2410            breakpoints: Default::default(),
2411            session_id: Some("session-id-1".to_owned()),
2412            window_id: Some(10),
2413            user_toolchains: Default::default(),
2414        };
2415
2416        let workspace_2 = SerializedWorkspace {
2417            id: WorkspaceId(2),
2418            paths: PathList::new(&["/tmp2"]),
2419            location: SerializedWorkspaceLocation::Local,
2420            center_group: Default::default(),
2421            window_bounds: Default::default(),
2422            display: Default::default(),
2423            docks: Default::default(),
2424            centered_layout: false,
2425            breakpoints: Default::default(),
2426            session_id: Some("session-id-1".to_owned()),
2427            window_id: Some(20),
2428            user_toolchains: Default::default(),
2429        };
2430
2431        let workspace_3 = SerializedWorkspace {
2432            id: WorkspaceId(3),
2433            paths: PathList::new(&["/tmp3"]),
2434            location: SerializedWorkspaceLocation::Local,
2435            center_group: Default::default(),
2436            window_bounds: Default::default(),
2437            display: Default::default(),
2438            docks: Default::default(),
2439            centered_layout: false,
2440            breakpoints: Default::default(),
2441            session_id: Some("session-id-2".to_owned()),
2442            window_id: Some(30),
2443            user_toolchains: Default::default(),
2444        };
2445
2446        let workspace_4 = SerializedWorkspace {
2447            id: WorkspaceId(4),
2448            paths: PathList::new(&["/tmp4"]),
2449            location: SerializedWorkspaceLocation::Local,
2450            center_group: Default::default(),
2451            window_bounds: Default::default(),
2452            display: Default::default(),
2453            docks: Default::default(),
2454            centered_layout: false,
2455            breakpoints: Default::default(),
2456            session_id: None,
2457            window_id: None,
2458            user_toolchains: Default::default(),
2459        };
2460
2461        let connection_id = db
2462            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2463                host: "my-host".to_string(),
2464                port: Some(1234),
2465                ..Default::default()
2466            }))
2467            .await
2468            .unwrap();
2469
2470        let workspace_5 = SerializedWorkspace {
2471            id: WorkspaceId(5),
2472            paths: PathList::default(),
2473            location: SerializedWorkspaceLocation::Remote(
2474                db.remote_connection(connection_id).unwrap(),
2475            ),
2476            center_group: Default::default(),
2477            window_bounds: Default::default(),
2478            display: Default::default(),
2479            docks: Default::default(),
2480            centered_layout: false,
2481            breakpoints: Default::default(),
2482            session_id: Some("session-id-2".to_owned()),
2483            window_id: Some(50),
2484            user_toolchains: Default::default(),
2485        };
2486
2487        let workspace_6 = SerializedWorkspace {
2488            id: WorkspaceId(6),
2489            paths: PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]),
2490            location: SerializedWorkspaceLocation::Local,
2491            center_group: Default::default(),
2492            window_bounds: Default::default(),
2493            breakpoints: Default::default(),
2494            display: Default::default(),
2495            docks: Default::default(),
2496            centered_layout: false,
2497            session_id: Some("session-id-3".to_owned()),
2498            window_id: Some(60),
2499            user_toolchains: Default::default(),
2500        };
2501
2502        db.save_workspace(workspace_1.clone()).await;
2503        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2504        db.save_workspace(workspace_2.clone()).await;
2505        db.save_workspace(workspace_3.clone()).await;
2506        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2507        db.save_workspace(workspace_4.clone()).await;
2508        db.save_workspace(workspace_5.clone()).await;
2509        db.save_workspace(workspace_6.clone()).await;
2510
2511        let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2512        assert_eq!(locations.len(), 2);
2513        assert_eq!(locations[0].0, PathList::new(&["/tmp2"]));
2514        assert_eq!(locations[0].1, Some(20));
2515        assert_eq!(locations[1].0, PathList::new(&["/tmp1"]));
2516        assert_eq!(locations[1].1, Some(10));
2517
2518        let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
2519        assert_eq!(locations.len(), 2);
2520        assert_eq!(locations[0].0, PathList::default());
2521        assert_eq!(locations[0].1, Some(50));
2522        assert_eq!(locations[0].2, Some(connection_id));
2523        assert_eq!(locations[1].0, PathList::new(&["/tmp3"]));
2524        assert_eq!(locations[1].1, Some(30));
2525
2526        let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
2527        assert_eq!(locations.len(), 1);
2528        assert_eq!(
2529            locations[0].0,
2530            PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]),
2531        );
2532        assert_eq!(locations[0].1, Some(60));
2533    }
2534
2535    fn default_workspace<P: AsRef<Path>>(
2536        paths: &[P],
2537        center_group: &SerializedPaneGroup,
2538    ) -> SerializedWorkspace {
2539        SerializedWorkspace {
2540            id: WorkspaceId(4),
2541            paths: PathList::new(paths),
2542            location: SerializedWorkspaceLocation::Local,
2543            center_group: center_group.clone(),
2544            window_bounds: Default::default(),
2545            display: Default::default(),
2546            docks: Default::default(),
2547            breakpoints: Default::default(),
2548            centered_layout: false,
2549            session_id: None,
2550            window_id: None,
2551            user_toolchains: Default::default(),
2552        }
2553    }
2554
2555    #[gpui::test]
2556    async fn test_last_session_workspace_locations() {
2557        let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
2558        let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
2559        let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
2560        let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
2561
2562        let db =
2563            WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
2564
2565        let workspaces = [
2566            (1, vec![dir1.path()], 9),
2567            (2, vec![dir2.path()], 5),
2568            (3, vec![dir3.path()], 8),
2569            (4, vec![dir4.path()], 2),
2570            (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
2571            (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
2572        ]
2573        .into_iter()
2574        .map(|(id, paths, window_id)| SerializedWorkspace {
2575            id: WorkspaceId(id),
2576            paths: PathList::new(paths.as_slice()),
2577            location: SerializedWorkspaceLocation::Local,
2578            center_group: Default::default(),
2579            window_bounds: Default::default(),
2580            display: Default::default(),
2581            docks: Default::default(),
2582            centered_layout: false,
2583            session_id: Some("one-session".to_owned()),
2584            breakpoints: Default::default(),
2585            window_id: Some(window_id),
2586            user_toolchains: Default::default(),
2587        })
2588        .collect::<Vec<_>>();
2589
2590        for workspace in workspaces.iter() {
2591            db.save_workspace(workspace.clone()).await;
2592        }
2593
2594        let stack = Some(Vec::from([
2595            WindowId::from(2), // Top
2596            WindowId::from(8),
2597            WindowId::from(5),
2598            WindowId::from(9),
2599            WindowId::from(3),
2600            WindowId::from(4), // Bottom
2601        ]));
2602
2603        let locations = db
2604            .last_session_workspace_locations("one-session", stack)
2605            .unwrap();
2606        assert_eq!(
2607            locations,
2608            [
2609                (
2610                    SerializedWorkspaceLocation::Local,
2611                    PathList::new(&[dir4.path()])
2612                ),
2613                (
2614                    SerializedWorkspaceLocation::Local,
2615                    PathList::new(&[dir3.path()])
2616                ),
2617                (
2618                    SerializedWorkspaceLocation::Local,
2619                    PathList::new(&[dir2.path()])
2620                ),
2621                (
2622                    SerializedWorkspaceLocation::Local,
2623                    PathList::new(&[dir1.path()])
2624                ),
2625                (
2626                    SerializedWorkspaceLocation::Local,
2627                    PathList::new(&[dir1.path(), dir2.path(), dir3.path()])
2628                ),
2629                (
2630                    SerializedWorkspaceLocation::Local,
2631                    PathList::new(&[dir4.path(), dir3.path(), dir2.path()])
2632                ),
2633            ]
2634        );
2635    }
2636
2637    #[gpui::test]
2638    async fn test_last_session_workspace_locations_remote() {
2639        let db =
2640            WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
2641                .await;
2642
2643        let remote_connections = [
2644            ("host-1", "my-user-1"),
2645            ("host-2", "my-user-2"),
2646            ("host-3", "my-user-3"),
2647            ("host-4", "my-user-4"),
2648        ]
2649        .into_iter()
2650        .map(|(host, user)| async {
2651            let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
2652                host: host.to_string(),
2653                username: Some(user.to_string()),
2654                ..Default::default()
2655            });
2656            db.get_or_create_remote_connection(options.clone())
2657                .await
2658                .unwrap();
2659            options
2660        })
2661        .collect::<Vec<_>>();
2662
2663        let remote_connections = futures::future::join_all(remote_connections).await;
2664
2665        let workspaces = [
2666            (1, remote_connections[0].clone(), 9),
2667            (2, remote_connections[1].clone(), 5),
2668            (3, remote_connections[2].clone(), 8),
2669            (4, remote_connections[3].clone(), 2),
2670        ]
2671        .into_iter()
2672        .map(|(id, remote_connection, window_id)| SerializedWorkspace {
2673            id: WorkspaceId(id),
2674            paths: PathList::default(),
2675            location: SerializedWorkspaceLocation::Remote(remote_connection),
2676            center_group: Default::default(),
2677            window_bounds: Default::default(),
2678            display: Default::default(),
2679            docks: Default::default(),
2680            centered_layout: false,
2681            session_id: Some("one-session".to_owned()),
2682            breakpoints: Default::default(),
2683            window_id: Some(window_id),
2684            user_toolchains: Default::default(),
2685        })
2686        .collect::<Vec<_>>();
2687
2688        for workspace in workspaces.iter() {
2689            db.save_workspace(workspace.clone()).await;
2690        }
2691
2692        let stack = Some(Vec::from([
2693            WindowId::from(2), // Top
2694            WindowId::from(8),
2695            WindowId::from(5),
2696            WindowId::from(9), // Bottom
2697        ]));
2698
2699        let have = db
2700            .last_session_workspace_locations("one-session", stack)
2701            .unwrap();
2702        assert_eq!(have.len(), 4);
2703        assert_eq!(
2704            have[0],
2705            (
2706                SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
2707                PathList::default()
2708            )
2709        );
2710        assert_eq!(
2711            have[1],
2712            (
2713                SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
2714                PathList::default()
2715            )
2716        );
2717        assert_eq!(
2718            have[2],
2719            (
2720                SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
2721                PathList::default()
2722            )
2723        );
2724        assert_eq!(
2725            have[3],
2726            (
2727                SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
2728                PathList::default()
2729            )
2730        );
2731    }
2732
2733    #[gpui::test]
2734    async fn test_get_or_create_ssh_project() {
2735        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
2736
2737        let host = "example.com".to_string();
2738        let port = Some(22_u16);
2739        let user = Some("user".to_string());
2740
2741        let connection_id = db
2742            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2743                host: host.clone(),
2744                port,
2745                username: user.clone(),
2746                ..Default::default()
2747            }))
2748            .await
2749            .unwrap();
2750
2751        // Test that calling the function again with the same parameters returns the same project
2752        let same_connection = db
2753            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2754                host: host.clone(),
2755                port,
2756                username: user.clone(),
2757                ..Default::default()
2758            }))
2759            .await
2760            .unwrap();
2761
2762        assert_eq!(connection_id, same_connection);
2763
2764        // Test with different parameters
2765        let host2 = "otherexample.com".to_string();
2766        let port2 = None;
2767        let user2 = Some("otheruser".to_string());
2768
2769        let different_connection = db
2770            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2771                host: host2.clone(),
2772                port: port2,
2773                username: user2.clone(),
2774                ..Default::default()
2775            }))
2776            .await
2777            .unwrap();
2778
2779        assert_ne!(connection_id, different_connection);
2780    }
2781
2782    #[gpui::test]
2783    async fn test_get_or_create_ssh_project_with_null_user() {
2784        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
2785
2786        let (host, port, user) = ("example.com".to_string(), None, None);
2787
2788        let connection_id = db
2789            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2790                host: host.clone(),
2791                port,
2792                username: None,
2793                ..Default::default()
2794            }))
2795            .await
2796            .unwrap();
2797
2798        let same_connection_id = db
2799            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2800                host: host.clone(),
2801                port,
2802                username: user.clone(),
2803                ..Default::default()
2804            }))
2805            .await
2806            .unwrap();
2807
2808        assert_eq!(connection_id, same_connection_id);
2809    }
2810
2811    #[gpui::test]
2812    async fn test_get_remote_connections() {
2813        let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
2814
2815        let connections = [
2816            ("example.com".to_string(), None, None),
2817            (
2818                "anotherexample.com".to_string(),
2819                Some(123_u16),
2820                Some("user2".to_string()),
2821            ),
2822            ("yetanother.com".to_string(), Some(345_u16), None),
2823        ];
2824
2825        let mut ids = Vec::new();
2826        for (host, port, user) in connections.iter() {
2827            ids.push(
2828                db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
2829                    SshConnectionOptions {
2830                        host: host.clone(),
2831                        port: *port,
2832                        username: user.clone(),
2833                        ..Default::default()
2834                    },
2835                ))
2836                .await
2837                .unwrap(),
2838            );
2839        }
2840
2841        let stored_connections = db.remote_connections().unwrap();
2842        assert_eq!(
2843            stored_connections,
2844            [
2845                (
2846                    ids[0],
2847                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
2848                        host: "example.com".into(),
2849                        port: None,
2850                        username: None,
2851                        ..Default::default()
2852                    }),
2853                ),
2854                (
2855                    ids[1],
2856                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
2857                        host: "anotherexample.com".into(),
2858                        port: Some(123),
2859                        username: Some("user2".into()),
2860                        ..Default::default()
2861                    }),
2862                ),
2863                (
2864                    ids[2],
2865                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
2866                        host: "yetanother.com".into(),
2867                        port: Some(345),
2868                        username: None,
2869                        ..Default::default()
2870                    }),
2871                ),
2872            ]
2873            .into_iter()
2874            .collect::<HashMap<_, _>>(),
2875        );
2876    }
2877
2878    #[gpui::test]
2879    async fn test_simple_split() {
2880        zlog::init_test();
2881
2882        let db = WorkspaceDb::open_test_db("simple_split").await;
2883
2884        //  -----------------
2885        //  | 1,2   | 5,6   |
2886        //  | - - - |       |
2887        //  | 3,4   |       |
2888        //  -----------------
2889        let center_pane = group(
2890            Axis::Horizontal,
2891            vec![
2892                group(
2893                    Axis::Vertical,
2894                    vec![
2895                        SerializedPaneGroup::Pane(SerializedPane::new(
2896                            vec![
2897                                SerializedItem::new("Terminal", 1, false, false),
2898                                SerializedItem::new("Terminal", 2, true, false),
2899                            ],
2900                            false,
2901                            0,
2902                        )),
2903                        SerializedPaneGroup::Pane(SerializedPane::new(
2904                            vec![
2905                                SerializedItem::new("Terminal", 4, false, false),
2906                                SerializedItem::new("Terminal", 3, true, false),
2907                            ],
2908                            true,
2909                            0,
2910                        )),
2911                    ],
2912                ),
2913                SerializedPaneGroup::Pane(SerializedPane::new(
2914                    vec![
2915                        SerializedItem::new("Terminal", 5, true, false),
2916                        SerializedItem::new("Terminal", 6, false, false),
2917                    ],
2918                    false,
2919                    0,
2920                )),
2921            ],
2922        );
2923
2924        let workspace = default_workspace(&["/tmp"], &center_pane);
2925
2926        db.save_workspace(workspace.clone()).await;
2927
2928        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
2929
2930        assert_eq!(workspace.center_group, new_workspace.center_group);
2931    }
2932
2933    #[gpui::test]
2934    async fn test_cleanup_panes() {
2935        zlog::init_test();
2936
2937        let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
2938
2939        let center_pane = group(
2940            Axis::Horizontal,
2941            vec![
2942                group(
2943                    Axis::Vertical,
2944                    vec![
2945                        SerializedPaneGroup::Pane(SerializedPane::new(
2946                            vec![
2947                                SerializedItem::new("Terminal", 1, false, false),
2948                                SerializedItem::new("Terminal", 2, true, false),
2949                            ],
2950                            false,
2951                            0,
2952                        )),
2953                        SerializedPaneGroup::Pane(SerializedPane::new(
2954                            vec![
2955                                SerializedItem::new("Terminal", 4, false, false),
2956                                SerializedItem::new("Terminal", 3, true, false),
2957                            ],
2958                            true,
2959                            0,
2960                        )),
2961                    ],
2962                ),
2963                SerializedPaneGroup::Pane(SerializedPane::new(
2964                    vec![
2965                        SerializedItem::new("Terminal", 5, false, false),
2966                        SerializedItem::new("Terminal", 6, true, false),
2967                    ],
2968                    false,
2969                    0,
2970                )),
2971            ],
2972        );
2973
2974        let id = &["/tmp"];
2975
2976        let mut workspace = default_workspace(id, &center_pane);
2977
2978        db.save_workspace(workspace.clone()).await;
2979
2980        workspace.center_group = group(
2981            Axis::Vertical,
2982            vec![
2983                SerializedPaneGroup::Pane(SerializedPane::new(
2984                    vec![
2985                        SerializedItem::new("Terminal", 1, false, false),
2986                        SerializedItem::new("Terminal", 2, true, false),
2987                    ],
2988                    false,
2989                    0,
2990                )),
2991                SerializedPaneGroup::Pane(SerializedPane::new(
2992                    vec![
2993                        SerializedItem::new("Terminal", 4, true, false),
2994                        SerializedItem::new("Terminal", 3, false, false),
2995                    ],
2996                    true,
2997                    0,
2998                )),
2999            ],
3000        );
3001
3002        db.save_workspace(workspace.clone()).await;
3003
3004        let new_workspace = db.workspace_for_roots(id).unwrap();
3005
3006        assert_eq!(workspace.center_group, new_workspace.center_group);
3007    }
3008}