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