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            if paths.paths().iter().all(|path| path.exists())
1351                && paths.paths().iter().any(|path| path.is_dir())
1352            {
1353                result.push((id, SerializedWorkspaceLocation::Local, paths));
1354            } else {
1355                delete_tasks.push(self.delete_workspace_by_id(id));
1356            }
1357        }
1358
1359        futures::future::join_all(delete_tasks).await;
1360        Ok(result)
1361    }
1362
1363    pub async fn last_workspace(&self) -> Result<Option<(SerializedWorkspaceLocation, PathList)>> {
1364        Ok(self
1365            .recent_workspaces_on_disk()
1366            .await?
1367            .into_iter()
1368            .next()
1369            .map(|(_, location, paths)| (location, paths)))
1370    }
1371
1372    // Returns the locations of the workspaces that were still opened when the last
1373    // session was closed (i.e. when Zed was quit).
1374    // If `last_session_window_order` is provided, the returned locations are ordered
1375    // according to that.
1376    pub fn last_session_workspace_locations(
1377        &self,
1378        last_session_id: &str,
1379        last_session_window_stack: Option<Vec<WindowId>>,
1380    ) -> Result<Vec<(SerializedWorkspaceLocation, PathList)>> {
1381        let mut workspaces = Vec::new();
1382
1383        for (paths, window_id, remote_connection_id) in
1384            self.session_workspaces(last_session_id.to_owned())?
1385        {
1386            if let Some(remote_connection_id) = remote_connection_id {
1387                workspaces.push((
1388                    SerializedWorkspaceLocation::Remote(
1389                        self.remote_connection(remote_connection_id)?,
1390                    ),
1391                    paths,
1392                    window_id.map(WindowId::from),
1393                ));
1394            } else if paths.paths().iter().all(|path| path.exists())
1395                && paths.paths().iter().any(|path| path.is_dir())
1396            {
1397                workspaces.push((
1398                    SerializedWorkspaceLocation::Local,
1399                    paths,
1400                    window_id.map(WindowId::from),
1401                ));
1402            }
1403        }
1404
1405        if let Some(stack) = last_session_window_stack {
1406            workspaces.sort_by_key(|(_, _, window_id)| {
1407                window_id
1408                    .and_then(|id| stack.iter().position(|&order_id| order_id == id))
1409                    .unwrap_or(usize::MAX)
1410            });
1411        }
1412
1413        Ok(workspaces
1414            .into_iter()
1415            .map(|(location, paths, _)| (location, paths))
1416            .collect::<Vec<_>>())
1417    }
1418
1419    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1420        Ok(self
1421            .get_pane_group(workspace_id, None)?
1422            .into_iter()
1423            .next()
1424            .unwrap_or_else(|| {
1425                SerializedPaneGroup::Pane(SerializedPane {
1426                    active: true,
1427                    children: vec![],
1428                    pinned_count: 0,
1429                })
1430            }))
1431    }
1432
1433    fn get_pane_group(
1434        &self,
1435        workspace_id: WorkspaceId,
1436        group_id: Option<GroupId>,
1437    ) -> Result<Vec<SerializedPaneGroup>> {
1438        type GroupKey = (Option<GroupId>, WorkspaceId);
1439        type GroupOrPane = (
1440            Option<GroupId>,
1441            Option<SerializedAxis>,
1442            Option<PaneId>,
1443            Option<bool>,
1444            Option<usize>,
1445            Option<String>,
1446        );
1447        self.select_bound::<GroupKey, GroupOrPane>(sql!(
1448            SELECT group_id, axis, pane_id, active, pinned_count, flexes
1449                FROM (SELECT
1450                        group_id,
1451                        axis,
1452                        NULL as pane_id,
1453                        NULL as active,
1454                        NULL as pinned_count,
1455                        position,
1456                        parent_group_id,
1457                        workspace_id,
1458                        flexes
1459                      FROM pane_groups
1460                    UNION
1461                      SELECT
1462                        NULL,
1463                        NULL,
1464                        center_panes.pane_id,
1465                        panes.active as active,
1466                        pinned_count,
1467                        position,
1468                        parent_group_id,
1469                        panes.workspace_id as workspace_id,
1470                        NULL
1471                      FROM center_panes
1472                      JOIN panes ON center_panes.pane_id = panes.pane_id)
1473                WHERE parent_group_id IS ? AND workspace_id = ?
1474                ORDER BY position
1475        ))?((group_id, workspace_id))?
1476        .into_iter()
1477        .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1478            let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1479            if let Some((group_id, axis)) = group_id.zip(axis) {
1480                let flexes = flexes
1481                    .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1482                    .transpose()?;
1483
1484                Ok(SerializedPaneGroup::Group {
1485                    axis,
1486                    children: self.get_pane_group(workspace_id, Some(group_id))?,
1487                    flexes,
1488                })
1489            } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
1490                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
1491                    self.get_items(pane_id)?,
1492                    active,
1493                    pinned_count,
1494                )))
1495            } else {
1496                bail!("Pane Group Child was neither a pane group or a pane");
1497            }
1498        })
1499        // Filter out panes and pane groups which don't have any children or items
1500        .filter(|pane_group| match pane_group {
1501            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
1502            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
1503            _ => true,
1504        })
1505        .collect::<Result<_>>()
1506    }
1507
1508    fn save_pane_group(
1509        conn: &Connection,
1510        workspace_id: WorkspaceId,
1511        pane_group: &SerializedPaneGroup,
1512        parent: Option<(GroupId, usize)>,
1513    ) -> Result<()> {
1514        if parent.is_none() {
1515            log::debug!("Saving a pane group for workspace {workspace_id:?}");
1516        }
1517        match pane_group {
1518            SerializedPaneGroup::Group {
1519                axis,
1520                children,
1521                flexes,
1522            } => {
1523                let (parent_id, position) = parent.unzip();
1524
1525                let flex_string = flexes
1526                    .as_ref()
1527                    .map(|flexes| serde_json::json!(flexes).to_string());
1528
1529                let group_id = conn.select_row_bound::<_, i64>(sql!(
1530                    INSERT INTO pane_groups(
1531                        workspace_id,
1532                        parent_group_id,
1533                        position,
1534                        axis,
1535                        flexes
1536                    )
1537                    VALUES (?, ?, ?, ?, ?)
1538                    RETURNING group_id
1539                ))?((
1540                    workspace_id,
1541                    parent_id,
1542                    position,
1543                    *axis,
1544                    flex_string,
1545                ))?
1546                .context("Couldn't retrieve group_id from inserted pane_group")?;
1547
1548                for (position, group) in children.iter().enumerate() {
1549                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
1550                }
1551
1552                Ok(())
1553            }
1554            SerializedPaneGroup::Pane(pane) => {
1555                Self::save_pane(conn, workspace_id, pane, parent)?;
1556                Ok(())
1557            }
1558        }
1559    }
1560
1561    fn save_pane(
1562        conn: &Connection,
1563        workspace_id: WorkspaceId,
1564        pane: &SerializedPane,
1565        parent: Option<(GroupId, usize)>,
1566    ) -> Result<PaneId> {
1567        let pane_id = conn.select_row_bound::<_, i64>(sql!(
1568            INSERT INTO panes(workspace_id, active, pinned_count)
1569            VALUES (?, ?, ?)
1570            RETURNING pane_id
1571        ))?((workspace_id, pane.active, pane.pinned_count))?
1572        .context("Could not retrieve inserted pane_id")?;
1573
1574        let (parent_id, order) = parent.unzip();
1575        conn.exec_bound(sql!(
1576            INSERT INTO center_panes(pane_id, parent_group_id, position)
1577            VALUES (?, ?, ?)
1578        ))?((pane_id, parent_id, order))?;
1579
1580        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
1581
1582        Ok(pane_id)
1583    }
1584
1585    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
1586        self.select_bound(sql!(
1587            SELECT kind, item_id, active, preview FROM items
1588            WHERE pane_id = ?
1589                ORDER BY position
1590        ))?(pane_id)
1591    }
1592
1593    fn save_items(
1594        conn: &Connection,
1595        workspace_id: WorkspaceId,
1596        pane_id: PaneId,
1597        items: &[SerializedItem],
1598    ) -> Result<()> {
1599        let mut insert = conn.exec_bound(sql!(
1600            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
1601        )).context("Preparing insertion")?;
1602        for (position, item) in items.iter().enumerate() {
1603            insert((workspace_id, pane_id, position, item))?;
1604        }
1605
1606        Ok(())
1607    }
1608
1609    query! {
1610        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
1611            UPDATE workspaces
1612            SET timestamp = CURRENT_TIMESTAMP
1613            WHERE workspace_id = ?
1614        }
1615    }
1616
1617    query! {
1618        pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
1619            UPDATE workspaces
1620            SET window_state = ?2,
1621                window_x = ?3,
1622                window_y = ?4,
1623                window_width = ?5,
1624                window_height = ?6,
1625                display = ?7
1626            WHERE workspace_id = ?1
1627        }
1628    }
1629
1630    query! {
1631        pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
1632            UPDATE workspaces
1633            SET centered_layout = ?2
1634            WHERE workspace_id = ?1
1635        }
1636    }
1637
1638    query! {
1639        pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option<String>) -> Result<()> {
1640            UPDATE workspaces
1641            SET session_id = ?2
1642            WHERE workspace_id = ?1
1643        }
1644    }
1645
1646    pub async fn toolchain(
1647        &self,
1648        workspace_id: WorkspaceId,
1649        worktree_id: WorktreeId,
1650        relative_worktree_path: Arc<RelPath>,
1651        language_name: LanguageName,
1652    ) -> Result<Option<Toolchain>> {
1653        self.write(move |this| {
1654            let mut select = this
1655                .select_bound(sql!(
1656                    SELECT
1657                        name, path, raw_json
1658                    FROM toolchains
1659                    WHERE
1660                        workspace_id = ? AND
1661                        language_name = ? AND
1662                        worktree_id = ? AND
1663                        relative_worktree_path = ?
1664                ))
1665                .context("select toolchain")?;
1666
1667            let toolchain: Vec<(String, String, String)> = select((
1668                workspace_id,
1669                language_name.as_ref().to_string(),
1670                worktree_id.to_usize(),
1671                relative_worktree_path.as_unix_str().to_string(),
1672            ))?;
1673
1674            Ok(toolchain
1675                .into_iter()
1676                .next()
1677                .and_then(|(name, path, raw_json)| {
1678                    Some(Toolchain {
1679                        name: name.into(),
1680                        path: path.into(),
1681                        language_name,
1682                        as_json: serde_json::Value::from_str(&raw_json).ok()?,
1683                    })
1684                }))
1685        })
1686        .await
1687    }
1688
1689    pub(crate) async fn toolchains(
1690        &self,
1691        workspace_id: WorkspaceId,
1692    ) -> Result<Vec<(Toolchain, WorktreeId, Arc<RelPath>)>> {
1693        self.write(move |this| {
1694            let mut select = this
1695                .select_bound(sql!(
1696                    SELECT
1697                        name, path, worktree_id, relative_worktree_path, language_name, raw_json
1698                    FROM toolchains
1699                    WHERE workspace_id = ?
1700                ))
1701                .context("select toolchains")?;
1702
1703            let toolchain: Vec<(String, String, u64, String, String, String)> =
1704                select(workspace_id)?;
1705
1706            Ok(toolchain
1707                .into_iter()
1708                .filter_map(
1709                    |(name, path, worktree_id, relative_worktree_path, language, json)| {
1710                        Some((
1711                            Toolchain {
1712                                name: name.into(),
1713                                path: path.into(),
1714                                language_name: LanguageName::new(&language),
1715                                as_json: serde_json::Value::from_str(&json).ok()?,
1716                            },
1717                            WorktreeId::from_proto(worktree_id),
1718                            RelPath::from_proto(&relative_worktree_path).log_err()?,
1719                        ))
1720                    },
1721                )
1722                .collect())
1723        })
1724        .await
1725    }
1726
1727    pub async fn set_toolchain(
1728        &self,
1729        workspace_id: WorkspaceId,
1730        worktree_id: WorktreeId,
1731        relative_worktree_path: Arc<RelPath>,
1732        toolchain: Toolchain,
1733    ) -> Result<()> {
1734        log::debug!(
1735            "Setting toolchain for workspace, worktree: {worktree_id:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
1736            toolchain.name
1737        );
1738        self.write(move |conn| {
1739            let mut insert = conn
1740                .exec_bound(sql!(
1741                    INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?,  ?, ?)
1742                    ON CONFLICT DO
1743                    UPDATE SET
1744                        name = ?5,
1745                        path = ?6,
1746                        raw_json = ?7
1747                ))
1748                .context("Preparing insertion")?;
1749
1750            insert((
1751                workspace_id,
1752                worktree_id.to_usize(),
1753                relative_worktree_path.as_unix_str(),
1754                toolchain.language_name.as_ref(),
1755                toolchain.name.as_ref(),
1756                toolchain.path.as_ref(),
1757                toolchain.as_json.to_string(),
1758            ))?;
1759
1760            Ok(())
1761        }).await
1762    }
1763}
1764
1765pub fn delete_unloaded_items(
1766    alive_items: Vec<ItemId>,
1767    workspace_id: WorkspaceId,
1768    table: &'static str,
1769    db: &ThreadSafeConnection,
1770    cx: &mut App,
1771) -> Task<Result<()>> {
1772    let db = db.clone();
1773    cx.spawn(async move |_| {
1774        let placeholders = alive_items
1775            .iter()
1776            .map(|_| "?")
1777            .collect::<Vec<&str>>()
1778            .join(", ");
1779
1780        let query = format!(
1781            "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
1782        );
1783
1784        db.write(move |conn| {
1785            let mut statement = Statement::prepare(conn, query)?;
1786            let mut next_index = statement.bind(&workspace_id, 1)?;
1787            for id in alive_items {
1788                next_index = statement.bind(&id, next_index)?;
1789            }
1790            statement.exec()
1791        })
1792        .await
1793    })
1794}
1795
1796#[cfg(test)]
1797mod tests {
1798    use super::*;
1799    use crate::persistence::model::{
1800        SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
1801    };
1802    use gpui;
1803    use pretty_assertions::assert_eq;
1804    use remote::SshConnectionOptions;
1805    use std::{thread, time::Duration};
1806
1807    #[gpui::test]
1808    async fn test_breakpoints() {
1809        zlog::init_test();
1810
1811        let db = WorkspaceDb::open_test_db("test_breakpoints").await;
1812        let id = db.next_id().await.unwrap();
1813
1814        let path = Path::new("/tmp/test.rs");
1815
1816        let breakpoint = Breakpoint {
1817            position: 123,
1818            message: None,
1819            state: BreakpointState::Enabled,
1820            condition: None,
1821            hit_condition: None,
1822        };
1823
1824        let log_breakpoint = Breakpoint {
1825            position: 456,
1826            message: Some("Test log message".into()),
1827            state: BreakpointState::Enabled,
1828            condition: None,
1829            hit_condition: None,
1830        };
1831
1832        let disable_breakpoint = Breakpoint {
1833            position: 578,
1834            message: None,
1835            state: BreakpointState::Disabled,
1836            condition: None,
1837            hit_condition: None,
1838        };
1839
1840        let condition_breakpoint = Breakpoint {
1841            position: 789,
1842            message: None,
1843            state: BreakpointState::Enabled,
1844            condition: Some("x > 5".into()),
1845            hit_condition: None,
1846        };
1847
1848        let hit_condition_breakpoint = Breakpoint {
1849            position: 999,
1850            message: None,
1851            state: BreakpointState::Enabled,
1852            condition: None,
1853            hit_condition: Some(">= 3".into()),
1854        };
1855
1856        let workspace = SerializedWorkspace {
1857            id,
1858            paths: PathList::new(&["/tmp"]),
1859            location: SerializedWorkspaceLocation::Local,
1860            center_group: Default::default(),
1861            window_bounds: Default::default(),
1862            display: Default::default(),
1863            docks: Default::default(),
1864            centered_layout: false,
1865            breakpoints: {
1866                let mut map = collections::BTreeMap::default();
1867                map.insert(
1868                    Arc::from(path),
1869                    vec![
1870                        SourceBreakpoint {
1871                            row: breakpoint.position,
1872                            path: Arc::from(path),
1873                            message: breakpoint.message.clone(),
1874                            state: breakpoint.state,
1875                            condition: breakpoint.condition.clone(),
1876                            hit_condition: breakpoint.hit_condition.clone(),
1877                        },
1878                        SourceBreakpoint {
1879                            row: log_breakpoint.position,
1880                            path: Arc::from(path),
1881                            message: log_breakpoint.message.clone(),
1882                            state: log_breakpoint.state,
1883                            condition: log_breakpoint.condition.clone(),
1884                            hit_condition: log_breakpoint.hit_condition.clone(),
1885                        },
1886                        SourceBreakpoint {
1887                            row: disable_breakpoint.position,
1888                            path: Arc::from(path),
1889                            message: disable_breakpoint.message.clone(),
1890                            state: disable_breakpoint.state,
1891                            condition: disable_breakpoint.condition.clone(),
1892                            hit_condition: disable_breakpoint.hit_condition.clone(),
1893                        },
1894                        SourceBreakpoint {
1895                            row: condition_breakpoint.position,
1896                            path: Arc::from(path),
1897                            message: condition_breakpoint.message.clone(),
1898                            state: condition_breakpoint.state,
1899                            condition: condition_breakpoint.condition.clone(),
1900                            hit_condition: condition_breakpoint.hit_condition.clone(),
1901                        },
1902                        SourceBreakpoint {
1903                            row: hit_condition_breakpoint.position,
1904                            path: Arc::from(path),
1905                            message: hit_condition_breakpoint.message.clone(),
1906                            state: hit_condition_breakpoint.state,
1907                            condition: hit_condition_breakpoint.condition.clone(),
1908                            hit_condition: hit_condition_breakpoint.hit_condition.clone(),
1909                        },
1910                    ],
1911                );
1912                map
1913            },
1914            session_id: None,
1915            window_id: None,
1916            user_toolchains: Default::default(),
1917        };
1918
1919        db.save_workspace(workspace.clone()).await;
1920
1921        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
1922        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
1923
1924        assert_eq!(loaded_breakpoints.len(), 5);
1925
1926        // normal breakpoint
1927        assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
1928        assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
1929        assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
1930        assert_eq!(
1931            loaded_breakpoints[0].hit_condition,
1932            breakpoint.hit_condition
1933        );
1934        assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
1935        assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
1936
1937        // enabled breakpoint
1938        assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
1939        assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
1940        assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
1941        assert_eq!(
1942            loaded_breakpoints[1].hit_condition,
1943            log_breakpoint.hit_condition
1944        );
1945        assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
1946        assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
1947
1948        // disable breakpoint
1949        assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
1950        assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
1951        assert_eq!(
1952            loaded_breakpoints[2].condition,
1953            disable_breakpoint.condition
1954        );
1955        assert_eq!(
1956            loaded_breakpoints[2].hit_condition,
1957            disable_breakpoint.hit_condition
1958        );
1959        assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
1960        assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
1961
1962        // condition breakpoint
1963        assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
1964        assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
1965        assert_eq!(
1966            loaded_breakpoints[3].condition,
1967            condition_breakpoint.condition
1968        );
1969        assert_eq!(
1970            loaded_breakpoints[3].hit_condition,
1971            condition_breakpoint.hit_condition
1972        );
1973        assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
1974        assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
1975
1976        // hit condition breakpoint
1977        assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
1978        assert_eq!(
1979            loaded_breakpoints[4].message,
1980            hit_condition_breakpoint.message
1981        );
1982        assert_eq!(
1983            loaded_breakpoints[4].condition,
1984            hit_condition_breakpoint.condition
1985        );
1986        assert_eq!(
1987            loaded_breakpoints[4].hit_condition,
1988            hit_condition_breakpoint.hit_condition
1989        );
1990        assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
1991        assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
1992    }
1993
1994    #[gpui::test]
1995    async fn test_remove_last_breakpoint() {
1996        zlog::init_test();
1997
1998        let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
1999        let id = db.next_id().await.unwrap();
2000
2001        let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
2002
2003        let breakpoint_to_remove = Breakpoint {
2004            position: 100,
2005            message: None,
2006            state: BreakpointState::Enabled,
2007            condition: None,
2008            hit_condition: None,
2009        };
2010
2011        let workspace = SerializedWorkspace {
2012            id,
2013            paths: PathList::new(&["/tmp"]),
2014            location: SerializedWorkspaceLocation::Local,
2015            center_group: Default::default(),
2016            window_bounds: Default::default(),
2017            display: Default::default(),
2018            docks: Default::default(),
2019            centered_layout: false,
2020            breakpoints: {
2021                let mut map = collections::BTreeMap::default();
2022                map.insert(
2023                    Arc::from(singular_path),
2024                    vec![SourceBreakpoint {
2025                        row: breakpoint_to_remove.position,
2026                        path: Arc::from(singular_path),
2027                        message: None,
2028                        state: BreakpointState::Enabled,
2029                        condition: None,
2030                        hit_condition: None,
2031                    }],
2032                );
2033                map
2034            },
2035            session_id: None,
2036            window_id: None,
2037            user_toolchains: Default::default(),
2038        };
2039
2040        db.save_workspace(workspace.clone()).await;
2041
2042        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2043        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
2044
2045        assert_eq!(loaded_breakpoints.len(), 1);
2046        assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
2047        assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
2048        assert_eq!(
2049            loaded_breakpoints[0].condition,
2050            breakpoint_to_remove.condition
2051        );
2052        assert_eq!(
2053            loaded_breakpoints[0].hit_condition,
2054            breakpoint_to_remove.hit_condition
2055        );
2056        assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
2057        assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
2058
2059        let workspace_without_breakpoint = SerializedWorkspace {
2060            id,
2061            paths: PathList::new(&["/tmp"]),
2062            location: SerializedWorkspaceLocation::Local,
2063            center_group: Default::default(),
2064            window_bounds: Default::default(),
2065            display: Default::default(),
2066            docks: Default::default(),
2067            centered_layout: false,
2068            breakpoints: collections::BTreeMap::default(),
2069            session_id: None,
2070            window_id: None,
2071            user_toolchains: Default::default(),
2072        };
2073
2074        db.save_workspace(workspace_without_breakpoint.clone())
2075            .await;
2076
2077        let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
2078        let empty_breakpoints = loaded_after_remove
2079            .breakpoints
2080            .get(&Arc::from(singular_path));
2081
2082        assert!(empty_breakpoints.is_none());
2083    }
2084
2085    #[gpui::test]
2086    async fn test_next_id_stability() {
2087        zlog::init_test();
2088
2089        let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
2090
2091        db.write(|conn| {
2092            conn.migrate(
2093                "test_table",
2094                &[sql!(
2095                    CREATE TABLE test_table(
2096                        text TEXT,
2097                        workspace_id INTEGER,
2098                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2099                        ON DELETE CASCADE
2100                    ) STRICT;
2101                )],
2102                |_, _, _| false,
2103            )
2104            .unwrap();
2105        })
2106        .await;
2107
2108        let id = db.next_id().await.unwrap();
2109        // Assert the empty row got inserted
2110        assert_eq!(
2111            Some(id),
2112            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
2113                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
2114            ))
2115            .unwrap()(id)
2116            .unwrap()
2117        );
2118
2119        db.write(move |conn| {
2120            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2121                .unwrap()(("test-text-1", id))
2122            .unwrap()
2123        })
2124        .await;
2125
2126        let test_text_1 = db
2127            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2128            .unwrap()(1)
2129        .unwrap()
2130        .unwrap();
2131        assert_eq!(test_text_1, "test-text-1");
2132    }
2133
2134    #[gpui::test]
2135    async fn test_workspace_id_stability() {
2136        zlog::init_test();
2137
2138        let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
2139
2140        db.write(|conn| {
2141            conn.migrate(
2142                "test_table",
2143                &[sql!(
2144                        CREATE TABLE test_table(
2145                            text TEXT,
2146                            workspace_id INTEGER,
2147                            FOREIGN KEY(workspace_id)
2148                                REFERENCES workspaces(workspace_id)
2149                            ON DELETE CASCADE
2150                        ) STRICT;)],
2151                |_, _, _| false,
2152            )
2153        })
2154        .await
2155        .unwrap();
2156
2157        let mut workspace_1 = SerializedWorkspace {
2158            id: WorkspaceId(1),
2159            paths: PathList::new(&["/tmp", "/tmp2"]),
2160            location: SerializedWorkspaceLocation::Local,
2161            center_group: Default::default(),
2162            window_bounds: Default::default(),
2163            display: Default::default(),
2164            docks: Default::default(),
2165            centered_layout: false,
2166            breakpoints: Default::default(),
2167            session_id: None,
2168            window_id: None,
2169            user_toolchains: Default::default(),
2170        };
2171
2172        let workspace_2 = SerializedWorkspace {
2173            id: WorkspaceId(2),
2174            paths: PathList::new(&["/tmp"]),
2175            location: SerializedWorkspaceLocation::Local,
2176            center_group: Default::default(),
2177            window_bounds: Default::default(),
2178            display: Default::default(),
2179            docks: Default::default(),
2180            centered_layout: false,
2181            breakpoints: Default::default(),
2182            session_id: None,
2183            window_id: None,
2184            user_toolchains: Default::default(),
2185        };
2186
2187        db.save_workspace(workspace_1.clone()).await;
2188
2189        db.write(|conn| {
2190            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2191                .unwrap()(("test-text-1", 1))
2192            .unwrap();
2193        })
2194        .await;
2195
2196        db.save_workspace(workspace_2.clone()).await;
2197
2198        db.write(|conn| {
2199            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2200                .unwrap()(("test-text-2", 2))
2201            .unwrap();
2202        })
2203        .await;
2204
2205        workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
2206        db.save_workspace(workspace_1.clone()).await;
2207        db.save_workspace(workspace_1).await;
2208        db.save_workspace(workspace_2).await;
2209
2210        let test_text_2 = db
2211            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2212            .unwrap()(2)
2213        .unwrap()
2214        .unwrap();
2215        assert_eq!(test_text_2, "test-text-2");
2216
2217        let test_text_1 = db
2218            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2219            .unwrap()(1)
2220        .unwrap()
2221        .unwrap();
2222        assert_eq!(test_text_1, "test-text-1");
2223    }
2224
2225    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
2226        SerializedPaneGroup::Group {
2227            axis: SerializedAxis(axis),
2228            flexes: None,
2229            children,
2230        }
2231    }
2232
2233    #[gpui::test]
2234    async fn test_full_workspace_serialization() {
2235        zlog::init_test();
2236
2237        let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
2238
2239        //  -----------------
2240        //  | 1,2   | 5,6   |
2241        //  | - - - |       |
2242        //  | 3,4   |       |
2243        //  -----------------
2244        let center_group = group(
2245            Axis::Horizontal,
2246            vec![
2247                group(
2248                    Axis::Vertical,
2249                    vec![
2250                        SerializedPaneGroup::Pane(SerializedPane::new(
2251                            vec![
2252                                SerializedItem::new("Terminal", 5, false, false),
2253                                SerializedItem::new("Terminal", 6, true, false),
2254                            ],
2255                            false,
2256                            0,
2257                        )),
2258                        SerializedPaneGroup::Pane(SerializedPane::new(
2259                            vec![
2260                                SerializedItem::new("Terminal", 7, true, false),
2261                                SerializedItem::new("Terminal", 8, false, false),
2262                            ],
2263                            false,
2264                            0,
2265                        )),
2266                    ],
2267                ),
2268                SerializedPaneGroup::Pane(SerializedPane::new(
2269                    vec![
2270                        SerializedItem::new("Terminal", 9, false, false),
2271                        SerializedItem::new("Terminal", 10, true, false),
2272                    ],
2273                    false,
2274                    0,
2275                )),
2276            ],
2277        );
2278
2279        let workspace = SerializedWorkspace {
2280            id: WorkspaceId(5),
2281            paths: PathList::new(&["/tmp", "/tmp2"]),
2282            location: SerializedWorkspaceLocation::Local,
2283            center_group,
2284            window_bounds: Default::default(),
2285            breakpoints: Default::default(),
2286            display: Default::default(),
2287            docks: Default::default(),
2288            centered_layout: false,
2289            session_id: None,
2290            window_id: Some(999),
2291            user_toolchains: Default::default(),
2292        };
2293
2294        db.save_workspace(workspace.clone()).await;
2295
2296        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2297        assert_eq!(workspace, round_trip_workspace.unwrap());
2298
2299        // Test guaranteed duplicate IDs
2300        db.save_workspace(workspace.clone()).await;
2301        db.save_workspace(workspace.clone()).await;
2302
2303        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2304        assert_eq!(workspace, round_trip_workspace.unwrap());
2305    }
2306
2307    #[gpui::test]
2308    async fn test_workspace_assignment() {
2309        zlog::init_test();
2310
2311        let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2312
2313        let workspace_1 = SerializedWorkspace {
2314            id: WorkspaceId(1),
2315            paths: PathList::new(&["/tmp", "/tmp2"]),
2316            location: SerializedWorkspaceLocation::Local,
2317            center_group: Default::default(),
2318            window_bounds: Default::default(),
2319            breakpoints: Default::default(),
2320            display: Default::default(),
2321            docks: Default::default(),
2322            centered_layout: false,
2323            session_id: None,
2324            window_id: Some(1),
2325            user_toolchains: Default::default(),
2326        };
2327
2328        let mut workspace_2 = SerializedWorkspace {
2329            id: WorkspaceId(2),
2330            paths: PathList::new(&["/tmp"]),
2331            location: SerializedWorkspaceLocation::Local,
2332            center_group: Default::default(),
2333            window_bounds: Default::default(),
2334            display: Default::default(),
2335            docks: Default::default(),
2336            centered_layout: false,
2337            breakpoints: Default::default(),
2338            session_id: None,
2339            window_id: Some(2),
2340            user_toolchains: Default::default(),
2341        };
2342
2343        db.save_workspace(workspace_1.clone()).await;
2344        db.save_workspace(workspace_2.clone()).await;
2345
2346        // Test that paths are treated as a set
2347        assert_eq!(
2348            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2349            workspace_1
2350        );
2351        assert_eq!(
2352            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
2353            workspace_1
2354        );
2355
2356        // Make sure that other keys work
2357        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
2358        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
2359
2360        // Test 'mutate' case of updating a pre-existing id
2361        workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
2362
2363        db.save_workspace(workspace_2.clone()).await;
2364        assert_eq!(
2365            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2366            workspace_2
2367        );
2368
2369        // Test other mechanism for mutating
2370        let mut workspace_3 = SerializedWorkspace {
2371            id: WorkspaceId(3),
2372            paths: PathList::new(&["/tmp2", "/tmp"]),
2373            location: SerializedWorkspaceLocation::Local,
2374            center_group: Default::default(),
2375            window_bounds: Default::default(),
2376            breakpoints: Default::default(),
2377            display: Default::default(),
2378            docks: Default::default(),
2379            centered_layout: false,
2380            session_id: None,
2381            window_id: Some(3),
2382            user_toolchains: Default::default(),
2383        };
2384
2385        db.save_workspace(workspace_3.clone()).await;
2386        assert_eq!(
2387            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2388            workspace_3
2389        );
2390
2391        // Make sure that updating paths differently also works
2392        workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
2393        db.save_workspace(workspace_3.clone()).await;
2394        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2395        assert_eq!(
2396            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2397                .unwrap(),
2398            workspace_3
2399        );
2400    }
2401
2402    #[gpui::test]
2403    async fn test_session_workspaces() {
2404        zlog::init_test();
2405
2406        let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
2407
2408        let workspace_1 = SerializedWorkspace {
2409            id: WorkspaceId(1),
2410            paths: PathList::new(&["/tmp1"]),
2411            location: SerializedWorkspaceLocation::Local,
2412            center_group: Default::default(),
2413            window_bounds: Default::default(),
2414            display: Default::default(),
2415            docks: Default::default(),
2416            centered_layout: false,
2417            breakpoints: Default::default(),
2418            session_id: Some("session-id-1".to_owned()),
2419            window_id: Some(10),
2420            user_toolchains: Default::default(),
2421        };
2422
2423        let workspace_2 = SerializedWorkspace {
2424            id: WorkspaceId(2),
2425            paths: PathList::new(&["/tmp2"]),
2426            location: SerializedWorkspaceLocation::Local,
2427            center_group: Default::default(),
2428            window_bounds: Default::default(),
2429            display: Default::default(),
2430            docks: Default::default(),
2431            centered_layout: false,
2432            breakpoints: Default::default(),
2433            session_id: Some("session-id-1".to_owned()),
2434            window_id: Some(20),
2435            user_toolchains: Default::default(),
2436        };
2437
2438        let workspace_3 = SerializedWorkspace {
2439            id: WorkspaceId(3),
2440            paths: PathList::new(&["/tmp3"]),
2441            location: SerializedWorkspaceLocation::Local,
2442            center_group: Default::default(),
2443            window_bounds: Default::default(),
2444            display: Default::default(),
2445            docks: Default::default(),
2446            centered_layout: false,
2447            breakpoints: Default::default(),
2448            session_id: Some("session-id-2".to_owned()),
2449            window_id: Some(30),
2450            user_toolchains: Default::default(),
2451        };
2452
2453        let workspace_4 = SerializedWorkspace {
2454            id: WorkspaceId(4),
2455            paths: PathList::new(&["/tmp4"]),
2456            location: SerializedWorkspaceLocation::Local,
2457            center_group: Default::default(),
2458            window_bounds: Default::default(),
2459            display: Default::default(),
2460            docks: Default::default(),
2461            centered_layout: false,
2462            breakpoints: Default::default(),
2463            session_id: None,
2464            window_id: None,
2465            user_toolchains: Default::default(),
2466        };
2467
2468        let connection_id = db
2469            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2470                host: "my-host".to_string(),
2471                port: Some(1234),
2472                ..Default::default()
2473            }))
2474            .await
2475            .unwrap();
2476
2477        let workspace_5 = SerializedWorkspace {
2478            id: WorkspaceId(5),
2479            paths: PathList::default(),
2480            location: SerializedWorkspaceLocation::Remote(
2481                db.remote_connection(connection_id).unwrap(),
2482            ),
2483            center_group: Default::default(),
2484            window_bounds: Default::default(),
2485            display: Default::default(),
2486            docks: Default::default(),
2487            centered_layout: false,
2488            breakpoints: Default::default(),
2489            session_id: Some("session-id-2".to_owned()),
2490            window_id: Some(50),
2491            user_toolchains: Default::default(),
2492        };
2493
2494        let workspace_6 = SerializedWorkspace {
2495            id: WorkspaceId(6),
2496            paths: PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]),
2497            location: SerializedWorkspaceLocation::Local,
2498            center_group: Default::default(),
2499            window_bounds: Default::default(),
2500            breakpoints: Default::default(),
2501            display: Default::default(),
2502            docks: Default::default(),
2503            centered_layout: false,
2504            session_id: Some("session-id-3".to_owned()),
2505            window_id: Some(60),
2506            user_toolchains: Default::default(),
2507        };
2508
2509        db.save_workspace(workspace_1.clone()).await;
2510        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2511        db.save_workspace(workspace_2.clone()).await;
2512        db.save_workspace(workspace_3.clone()).await;
2513        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2514        db.save_workspace(workspace_4.clone()).await;
2515        db.save_workspace(workspace_5.clone()).await;
2516        db.save_workspace(workspace_6.clone()).await;
2517
2518        let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2519        assert_eq!(locations.len(), 2);
2520        assert_eq!(locations[0].0, PathList::new(&["/tmp2"]));
2521        assert_eq!(locations[0].1, Some(20));
2522        assert_eq!(locations[1].0, PathList::new(&["/tmp1"]));
2523        assert_eq!(locations[1].1, Some(10));
2524
2525        let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
2526        assert_eq!(locations.len(), 2);
2527        assert_eq!(locations[0].0, PathList::default());
2528        assert_eq!(locations[0].1, Some(50));
2529        assert_eq!(locations[0].2, Some(connection_id));
2530        assert_eq!(locations[1].0, PathList::new(&["/tmp3"]));
2531        assert_eq!(locations[1].1, Some(30));
2532
2533        let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
2534        assert_eq!(locations.len(), 1);
2535        assert_eq!(
2536            locations[0].0,
2537            PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]),
2538        );
2539        assert_eq!(locations[0].1, Some(60));
2540    }
2541
2542    fn default_workspace<P: AsRef<Path>>(
2543        paths: &[P],
2544        center_group: &SerializedPaneGroup,
2545    ) -> SerializedWorkspace {
2546        SerializedWorkspace {
2547            id: WorkspaceId(4),
2548            paths: PathList::new(paths),
2549            location: SerializedWorkspaceLocation::Local,
2550            center_group: center_group.clone(),
2551            window_bounds: Default::default(),
2552            display: Default::default(),
2553            docks: Default::default(),
2554            breakpoints: Default::default(),
2555            centered_layout: false,
2556            session_id: None,
2557            window_id: None,
2558            user_toolchains: Default::default(),
2559        }
2560    }
2561
2562    #[gpui::test]
2563    async fn test_last_session_workspace_locations() {
2564        let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
2565        let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
2566        let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
2567        let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
2568
2569        let db =
2570            WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
2571
2572        let workspaces = [
2573            (1, vec![dir1.path()], 9),
2574            (2, vec![dir2.path()], 5),
2575            (3, vec![dir3.path()], 8),
2576            (4, vec![dir4.path()], 2),
2577            (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
2578            (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
2579        ]
2580        .into_iter()
2581        .map(|(id, paths, window_id)| SerializedWorkspace {
2582            id: WorkspaceId(id),
2583            paths: PathList::new(paths.as_slice()),
2584            location: SerializedWorkspaceLocation::Local,
2585            center_group: Default::default(),
2586            window_bounds: Default::default(),
2587            display: Default::default(),
2588            docks: Default::default(),
2589            centered_layout: false,
2590            session_id: Some("one-session".to_owned()),
2591            breakpoints: Default::default(),
2592            window_id: Some(window_id),
2593            user_toolchains: Default::default(),
2594        })
2595        .collect::<Vec<_>>();
2596
2597        for workspace in workspaces.iter() {
2598            db.save_workspace(workspace.clone()).await;
2599        }
2600
2601        let stack = Some(Vec::from([
2602            WindowId::from(2), // Top
2603            WindowId::from(8),
2604            WindowId::from(5),
2605            WindowId::from(9),
2606            WindowId::from(3),
2607            WindowId::from(4), // Bottom
2608        ]));
2609
2610        let locations = db
2611            .last_session_workspace_locations("one-session", stack)
2612            .unwrap();
2613        assert_eq!(
2614            locations,
2615            [
2616                (
2617                    SerializedWorkspaceLocation::Local,
2618                    PathList::new(&[dir4.path()])
2619                ),
2620                (
2621                    SerializedWorkspaceLocation::Local,
2622                    PathList::new(&[dir3.path()])
2623                ),
2624                (
2625                    SerializedWorkspaceLocation::Local,
2626                    PathList::new(&[dir2.path()])
2627                ),
2628                (
2629                    SerializedWorkspaceLocation::Local,
2630                    PathList::new(&[dir1.path()])
2631                ),
2632                (
2633                    SerializedWorkspaceLocation::Local,
2634                    PathList::new(&[dir1.path(), dir2.path(), dir3.path()])
2635                ),
2636                (
2637                    SerializedWorkspaceLocation::Local,
2638                    PathList::new(&[dir4.path(), dir3.path(), dir2.path()])
2639                ),
2640            ]
2641        );
2642    }
2643
2644    #[gpui::test]
2645    async fn test_last_session_workspace_locations_remote() {
2646        let db =
2647            WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
2648                .await;
2649
2650        let remote_connections = [
2651            ("host-1", "my-user-1"),
2652            ("host-2", "my-user-2"),
2653            ("host-3", "my-user-3"),
2654            ("host-4", "my-user-4"),
2655        ]
2656        .into_iter()
2657        .map(|(host, user)| async {
2658            let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
2659                host: host.to_string(),
2660                username: Some(user.to_string()),
2661                ..Default::default()
2662            });
2663            db.get_or_create_remote_connection(options.clone())
2664                .await
2665                .unwrap();
2666            options
2667        })
2668        .collect::<Vec<_>>();
2669
2670        let remote_connections = futures::future::join_all(remote_connections).await;
2671
2672        let workspaces = [
2673            (1, remote_connections[0].clone(), 9),
2674            (2, remote_connections[1].clone(), 5),
2675            (3, remote_connections[2].clone(), 8),
2676            (4, remote_connections[3].clone(), 2),
2677        ]
2678        .into_iter()
2679        .map(|(id, remote_connection, window_id)| SerializedWorkspace {
2680            id: WorkspaceId(id),
2681            paths: PathList::default(),
2682            location: SerializedWorkspaceLocation::Remote(remote_connection),
2683            center_group: Default::default(),
2684            window_bounds: Default::default(),
2685            display: Default::default(),
2686            docks: Default::default(),
2687            centered_layout: false,
2688            session_id: Some("one-session".to_owned()),
2689            breakpoints: Default::default(),
2690            window_id: Some(window_id),
2691            user_toolchains: Default::default(),
2692        })
2693        .collect::<Vec<_>>();
2694
2695        for workspace in workspaces.iter() {
2696            db.save_workspace(workspace.clone()).await;
2697        }
2698
2699        let stack = Some(Vec::from([
2700            WindowId::from(2), // Top
2701            WindowId::from(8),
2702            WindowId::from(5),
2703            WindowId::from(9), // Bottom
2704        ]));
2705
2706        let have = db
2707            .last_session_workspace_locations("one-session", stack)
2708            .unwrap();
2709        assert_eq!(have.len(), 4);
2710        assert_eq!(
2711            have[0],
2712            (
2713                SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
2714                PathList::default()
2715            )
2716        );
2717        assert_eq!(
2718            have[1],
2719            (
2720                SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
2721                PathList::default()
2722            )
2723        );
2724        assert_eq!(
2725            have[2],
2726            (
2727                SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
2728                PathList::default()
2729            )
2730        );
2731        assert_eq!(
2732            have[3],
2733            (
2734                SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
2735                PathList::default()
2736            )
2737        );
2738    }
2739
2740    #[gpui::test]
2741    async fn test_get_or_create_ssh_project() {
2742        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
2743
2744        let host = "example.com".to_string();
2745        let port = Some(22_u16);
2746        let user = Some("user".to_string());
2747
2748        let connection_id = db
2749            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2750                host: host.clone(),
2751                port,
2752                username: user.clone(),
2753                ..Default::default()
2754            }))
2755            .await
2756            .unwrap();
2757
2758        // Test that calling the function again with the same parameters returns the same project
2759        let same_connection = db
2760            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2761                host: host.clone(),
2762                port,
2763                username: user.clone(),
2764                ..Default::default()
2765            }))
2766            .await
2767            .unwrap();
2768
2769        assert_eq!(connection_id, same_connection);
2770
2771        // Test with different parameters
2772        let host2 = "otherexample.com".to_string();
2773        let port2 = None;
2774        let user2 = Some("otheruser".to_string());
2775
2776        let different_connection = db
2777            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2778                host: host2.clone(),
2779                port: port2,
2780                username: user2.clone(),
2781                ..Default::default()
2782            }))
2783            .await
2784            .unwrap();
2785
2786        assert_ne!(connection_id, different_connection);
2787    }
2788
2789    #[gpui::test]
2790    async fn test_get_or_create_ssh_project_with_null_user() {
2791        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
2792
2793        let (host, port, user) = ("example.com".to_string(), None, None);
2794
2795        let connection_id = db
2796            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2797                host: host.clone(),
2798                port,
2799                username: None,
2800                ..Default::default()
2801            }))
2802            .await
2803            .unwrap();
2804
2805        let same_connection_id = db
2806            .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2807                host: host.clone(),
2808                port,
2809                username: user.clone(),
2810                ..Default::default()
2811            }))
2812            .await
2813            .unwrap();
2814
2815        assert_eq!(connection_id, same_connection_id);
2816    }
2817
2818    #[gpui::test]
2819    async fn test_get_remote_connections() {
2820        let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
2821
2822        let connections = [
2823            ("example.com".to_string(), None, None),
2824            (
2825                "anotherexample.com".to_string(),
2826                Some(123_u16),
2827                Some("user2".to_string()),
2828            ),
2829            ("yetanother.com".to_string(), Some(345_u16), None),
2830        ];
2831
2832        let mut ids = Vec::new();
2833        for (host, port, user) in connections.iter() {
2834            ids.push(
2835                db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
2836                    SshConnectionOptions {
2837                        host: host.clone(),
2838                        port: *port,
2839                        username: user.clone(),
2840                        ..Default::default()
2841                    },
2842                ))
2843                .await
2844                .unwrap(),
2845            );
2846        }
2847
2848        let stored_connections = db.remote_connections().unwrap();
2849        assert_eq!(
2850            stored_connections,
2851            [
2852                (
2853                    ids[0],
2854                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
2855                        host: "example.com".into(),
2856                        port: None,
2857                        username: None,
2858                        ..Default::default()
2859                    }),
2860                ),
2861                (
2862                    ids[1],
2863                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
2864                        host: "anotherexample.com".into(),
2865                        port: Some(123),
2866                        username: Some("user2".into()),
2867                        ..Default::default()
2868                    }),
2869                ),
2870                (
2871                    ids[2],
2872                    RemoteConnectionOptions::Ssh(SshConnectionOptions {
2873                        host: "yetanother.com".into(),
2874                        port: Some(345),
2875                        username: None,
2876                        ..Default::default()
2877                    }),
2878                ),
2879            ]
2880            .into_iter()
2881            .collect::<HashMap<_, _>>(),
2882        );
2883    }
2884
2885    #[gpui::test]
2886    async fn test_simple_split() {
2887        zlog::init_test();
2888
2889        let db = WorkspaceDb::open_test_db("simple_split").await;
2890
2891        //  -----------------
2892        //  | 1,2   | 5,6   |
2893        //  | - - - |       |
2894        //  | 3,4   |       |
2895        //  -----------------
2896        let center_pane = group(
2897            Axis::Horizontal,
2898            vec![
2899                group(
2900                    Axis::Vertical,
2901                    vec![
2902                        SerializedPaneGroup::Pane(SerializedPane::new(
2903                            vec![
2904                                SerializedItem::new("Terminal", 1, false, false),
2905                                SerializedItem::new("Terminal", 2, true, false),
2906                            ],
2907                            false,
2908                            0,
2909                        )),
2910                        SerializedPaneGroup::Pane(SerializedPane::new(
2911                            vec![
2912                                SerializedItem::new("Terminal", 4, false, false),
2913                                SerializedItem::new("Terminal", 3, true, false),
2914                            ],
2915                            true,
2916                            0,
2917                        )),
2918                    ],
2919                ),
2920                SerializedPaneGroup::Pane(SerializedPane::new(
2921                    vec![
2922                        SerializedItem::new("Terminal", 5, true, false),
2923                        SerializedItem::new("Terminal", 6, false, false),
2924                    ],
2925                    false,
2926                    0,
2927                )),
2928            ],
2929        );
2930
2931        let workspace = default_workspace(&["/tmp"], &center_pane);
2932
2933        db.save_workspace(workspace.clone()).await;
2934
2935        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
2936
2937        assert_eq!(workspace.center_group, new_workspace.center_group);
2938    }
2939
2940    #[gpui::test]
2941    async fn test_cleanup_panes() {
2942        zlog::init_test();
2943
2944        let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
2945
2946        let center_pane = group(
2947            Axis::Horizontal,
2948            vec![
2949                group(
2950                    Axis::Vertical,
2951                    vec![
2952                        SerializedPaneGroup::Pane(SerializedPane::new(
2953                            vec![
2954                                SerializedItem::new("Terminal", 1, false, false),
2955                                SerializedItem::new("Terminal", 2, true, false),
2956                            ],
2957                            false,
2958                            0,
2959                        )),
2960                        SerializedPaneGroup::Pane(SerializedPane::new(
2961                            vec![
2962                                SerializedItem::new("Terminal", 4, false, false),
2963                                SerializedItem::new("Terminal", 3, true, false),
2964                            ],
2965                            true,
2966                            0,
2967                        )),
2968                    ],
2969                ),
2970                SerializedPaneGroup::Pane(SerializedPane::new(
2971                    vec![
2972                        SerializedItem::new("Terminal", 5, false, false),
2973                        SerializedItem::new("Terminal", 6, true, false),
2974                    ],
2975                    false,
2976                    0,
2977                )),
2978            ],
2979        );
2980
2981        let id = &["/tmp"];
2982
2983        let mut workspace = default_workspace(id, &center_pane);
2984
2985        db.save_workspace(workspace.clone()).await;
2986
2987        workspace.center_group = group(
2988            Axis::Vertical,
2989            vec![
2990                SerializedPaneGroup::Pane(SerializedPane::new(
2991                    vec![
2992                        SerializedItem::new("Terminal", 1, false, false),
2993                        SerializedItem::new("Terminal", 2, true, false),
2994                    ],
2995                    false,
2996                    0,
2997                )),
2998                SerializedPaneGroup::Pane(SerializedPane::new(
2999                    vec![
3000                        SerializedItem::new("Terminal", 4, true, false),
3001                        SerializedItem::new("Terminal", 3, false, false),
3002                    ],
3003                    true,
3004                    0,
3005                )),
3006            ],
3007        );
3008
3009        db.save_workspace(workspace.clone()).await;
3010
3011        let new_workspace = db.workspace_for_roots(id).unwrap();
3012
3013        assert_eq!(workspace.center_group, new_workspace.center_group);
3014    }
3015}