persistence.rs

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