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