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