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