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