persistence.rs

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