persistence.rs

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