persistence.rs

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