persistence.rs

   1pub mod model;
   2
   3use std::{
   4    borrow::Cow,
   5    collections::BTreeMap,
   6    path::{Path, PathBuf},
   7    str::FromStr,
   8    sync::Arc,
   9};
  10
  11use anyhow::{Context as _, Result, bail};
  12use collections::HashMap;
  13use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
  14use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size};
  15use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint};
  16
  17use language::{LanguageName, Toolchain};
  18use project::WorktreeId;
  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, SshConnectionId,
  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: SshConnectionId,
 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<SshConnectionId>,
 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        log::debug!("Saving workspace at location: {:?}", workspace.location);
 766        self.write(move |conn| {
 767            conn.with_savepoint("update_worktrees", || {
 768                let ssh_connection_id = match &workspace.location {
 769                    SerializedWorkspaceLocation::Local => None,
 770                    SerializedWorkspaceLocation::Ssh(connection) => {
 771                        Some(Self::get_or_create_ssh_connection_query(
 772                            conn,
 773                            connection.host.clone(),
 774                            connection.port,
 775                            connection.user.clone(),
 776                        )?.0)
 777                    }
 778                };
 779
 780                // Clear out panes and pane_groups
 781                conn.exec_bound(sql!(
 782                    DELETE FROM pane_groups WHERE workspace_id = ?1;
 783                    DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
 784                    .context("Clearing old panes")?;
 785
 786                conn.exec_bound(
 787                    sql!(
 788                        DELETE FROM breakpoints WHERE workspace_id = ?1;
 789                        DELETE FROM toolchains WHERE workspace_id = ?1;
 790                    )
 791                )?(workspace.id).context("Clearing old breakpoints")?;
 792
 793                for (path, breakpoints) in workspace.breakpoints {
 794                    for bp in breakpoints {
 795                        let state = BreakpointStateWrapper::from(bp.state);
 796                        match conn.exec_bound(sql!(
 797                            INSERT INTO breakpoints (workspace_id, path, breakpoint_location,  log_message, condition, hit_condition, state)
 798                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);))?
 799
 800                        ((
 801                            workspace.id,
 802                            path.as_ref(),
 803                            bp.row,
 804                            bp.message,
 805                            bp.condition,
 806                            bp.hit_condition,
 807                            state,
 808                        )) {
 809                            Ok(_) => {
 810                                log::debug!("Stored breakpoint at row: {} in path: {}", bp.row, path.to_string_lossy())
 811                            }
 812                            Err(err) => {
 813                                log::error!("{err}");
 814                                continue;
 815                            }
 816                        }
 817                    }
 818                }
 819
 820                conn.exec_bound(sql!(
 821                    DELETE
 822                    FROM workspaces
 823                    WHERE
 824                        workspace_id != ?1 AND
 825                        paths IS ?2 AND
 826                        ssh_connection_id IS ?3
 827                ))?((
 828                    workspace.id,
 829                    paths.paths.clone(),
 830                    ssh_connection_id,
 831                ))
 832                .context("clearing out old locations")?;
 833
 834                // Upsert
 835                let query = sql!(
 836                    INSERT INTO workspaces(
 837                        workspace_id,
 838                        paths,
 839                        paths_order,
 840                        ssh_connection_id,
 841                        left_dock_visible,
 842                        left_dock_active_panel,
 843                        left_dock_zoom,
 844                        right_dock_visible,
 845                        right_dock_active_panel,
 846                        right_dock_zoom,
 847                        bottom_dock_visible,
 848                        bottom_dock_active_panel,
 849                        bottom_dock_zoom,
 850                        session_id,
 851                        window_id,
 852                        timestamp
 853                    )
 854                    VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, CURRENT_TIMESTAMP)
 855                    ON CONFLICT DO
 856                    UPDATE SET
 857                        paths = ?2,
 858                        paths_order = ?3,
 859                        ssh_connection_id = ?4,
 860                        left_dock_visible = ?5,
 861                        left_dock_active_panel = ?6,
 862                        left_dock_zoom = ?7,
 863                        right_dock_visible = ?8,
 864                        right_dock_active_panel = ?9,
 865                        right_dock_zoom = ?10,
 866                        bottom_dock_visible = ?11,
 867                        bottom_dock_active_panel = ?12,
 868                        bottom_dock_zoom = ?13,
 869                        session_id = ?14,
 870                        window_id = ?15,
 871                        timestamp = CURRENT_TIMESTAMP
 872                );
 873                let mut prepared_query = conn.exec_bound(query)?;
 874                let args = (
 875                    workspace.id,
 876                    paths.paths.clone(),
 877                    paths.order.clone(),
 878                    ssh_connection_id,
 879                    workspace.docks,
 880                    workspace.session_id,
 881                    workspace.window_id,
 882                );
 883
 884                prepared_query(args).context("Updating workspace")?;
 885
 886                // Save center pane group
 887                Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
 888                    .context("save pane group in save workspace")?;
 889
 890                Ok(())
 891            })
 892            .log_err();
 893        })
 894        .await;
 895    }
 896
 897    pub(crate) async fn get_or_create_ssh_connection(
 898        &self,
 899        host: String,
 900        port: Option<u16>,
 901        user: Option<String>,
 902    ) -> Result<SshConnectionId> {
 903        self.write(move |conn| Self::get_or_create_ssh_connection_query(conn, host, port, user))
 904            .await
 905    }
 906
 907    fn get_or_create_ssh_connection_query(
 908        this: &Connection,
 909        host: String,
 910        port: Option<u16>,
 911        user: Option<String>,
 912    ) -> Result<SshConnectionId> {
 913        if let Some(id) = this.select_row_bound(sql!(
 914            SELECT id FROM ssh_connections WHERE host IS ? AND port IS ? AND user IS ? LIMIT 1
 915        ))?((host.clone(), port, user.clone()))?
 916        {
 917            Ok(SshConnectionId(id))
 918        } else {
 919            log::debug!("Inserting SSH project at host {host}");
 920            let id = this.select_row_bound(sql!(
 921                INSERT INTO ssh_connections (
 922                    host,
 923                    port,
 924                    user
 925                ) VALUES (?1, ?2, ?3)
 926                RETURNING id
 927            ))?((host, port, user))?
 928            .context("failed to insert ssh project")?;
 929            Ok(SshConnectionId(id))
 930        }
 931    }
 932
 933    query! {
 934        pub async fn next_id() -> Result<WorkspaceId> {
 935            INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
 936        }
 937    }
 938
 939    fn recent_workspaces(&self) -> Result<Vec<(WorkspaceId, PathList, Option<u64>)>> {
 940        Ok(self
 941            .recent_workspaces_query()?
 942            .into_iter()
 943            .map(|(id, paths, order, ssh_connection_id)| {
 944                (
 945                    id,
 946                    PathList::deserialize(&SerializedPathList { paths, order }),
 947                    ssh_connection_id,
 948                )
 949            })
 950            .collect())
 951    }
 952
 953    query! {
 954        fn recent_workspaces_query() -> Result<Vec<(WorkspaceId, String, String, Option<u64>)>> {
 955            SELECT workspace_id, paths, paths_order, ssh_connection_id
 956            FROM workspaces
 957            WHERE
 958                paths IS NOT NULL OR
 959                ssh_connection_id IS NOT NULL
 960            ORDER BY timestamp DESC
 961        }
 962    }
 963
 964    fn session_workspaces(
 965        &self,
 966        session_id: String,
 967    ) -> Result<Vec<(PathList, Option<u64>, Option<SshConnectionId>)>> {
 968        Ok(self
 969            .session_workspaces_query(session_id)?
 970            .into_iter()
 971            .map(|(paths, order, window_id, ssh_connection_id)| {
 972                (
 973                    PathList::deserialize(&SerializedPathList { paths, order }),
 974                    window_id,
 975                    ssh_connection_id.map(SshConnectionId),
 976                )
 977            })
 978            .collect())
 979    }
 980
 981    query! {
 982        fn session_workspaces_query(session_id: String) -> Result<Vec<(String, String, Option<u64>, Option<u64>)>> {
 983            SELECT paths, paths_order, window_id, ssh_connection_id
 984            FROM workspaces
 985            WHERE session_id = ?1
 986            ORDER BY timestamp DESC
 987        }
 988    }
 989
 990    query! {
 991        pub fn breakpoints_for_file(workspace_id: WorkspaceId, file_path: &Path) -> Result<Vec<Breakpoint>> {
 992            SELECT breakpoint_location
 993            FROM breakpoints
 994            WHERE  workspace_id= ?1 AND path = ?2
 995        }
 996    }
 997
 998    query! {
 999        pub fn clear_breakpoints(file_path: &Path) -> Result<()> {
1000            DELETE FROM breakpoints
1001            WHERE file_path = ?2
1002        }
1003    }
1004
1005    fn ssh_connections(&self) -> Result<HashMap<SshConnectionId, SerializedSshConnection>> {
1006        Ok(self
1007            .ssh_connections_query()?
1008            .into_iter()
1009            .map(|(id, host, port, user)| {
1010                (
1011                    SshConnectionId(id),
1012                    SerializedSshConnection { host, port, user },
1013                )
1014            })
1015            .collect())
1016    }
1017
1018    query! {
1019        pub fn ssh_connections_query() -> Result<Vec<(u64, String, Option<u16>, Option<String>)>> {
1020            SELECT id, host, port, user
1021            FROM ssh_connections
1022        }
1023    }
1024
1025    pub(crate) fn ssh_connection(&self, id: SshConnectionId) -> Result<SerializedSshConnection> {
1026        let row = self.ssh_connection_query(id.0)?;
1027        Ok(SerializedSshConnection {
1028            host: row.0,
1029            port: row.1,
1030            user: row.2,
1031        })
1032    }
1033
1034    query! {
1035        fn ssh_connection_query(id: u64) -> Result<(String, Option<u16>, Option<String>)> {
1036            SELECT 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(SshConnectionId) {
1079                if let Some(ssh_connection) = ssh_connections.get(&ssh_connection_id) {
1080                    result.push((
1081                        id,
1082                        SerializedWorkspaceLocation::Ssh(ssh_connection.clone()),
1083                        paths,
1084                    ));
1085                } else {
1086                    delete_tasks.push(self.delete_workspace_by_id(id));
1087                }
1088                continue;
1089            }
1090
1091            if paths.paths().iter().all(|path| path.exists())
1092                && paths.paths().iter().any(|path| path.is_dir())
1093            {
1094                result.push((id, SerializedWorkspaceLocation::Local, paths));
1095            } else {
1096                delete_tasks.push(self.delete_workspace_by_id(id));
1097            }
1098        }
1099
1100        futures::future::join_all(delete_tasks).await;
1101        Ok(result)
1102    }
1103
1104    pub async fn last_workspace(&self) -> Result<Option<(SerializedWorkspaceLocation, PathList)>> {
1105        Ok(self
1106            .recent_workspaces_on_disk()
1107            .await?
1108            .into_iter()
1109            .next()
1110            .map(|(_, location, paths)| (location, paths)))
1111    }
1112
1113    // Returns the locations of the workspaces that were still opened when the last
1114    // session was closed (i.e. when Zed was quit).
1115    // If `last_session_window_order` is provided, the returned locations are ordered
1116    // according to that.
1117    pub fn last_session_workspace_locations(
1118        &self,
1119        last_session_id: &str,
1120        last_session_window_stack: Option<Vec<WindowId>>,
1121    ) -> Result<Vec<(SerializedWorkspaceLocation, PathList)>> {
1122        let mut workspaces = Vec::new();
1123
1124        for (paths, window_id, ssh_connection_id) in
1125            self.session_workspaces(last_session_id.to_owned())?
1126        {
1127            if let Some(ssh_connection_id) = ssh_connection_id {
1128                workspaces.push((
1129                    SerializedWorkspaceLocation::Ssh(self.ssh_connection(ssh_connection_id)?),
1130                    paths,
1131                    window_id.map(WindowId::from),
1132                ));
1133            } else if paths.paths().iter().all(|path| path.exists())
1134                && paths.paths().iter().any(|path| path.is_dir())
1135            {
1136                workspaces.push((
1137                    SerializedWorkspaceLocation::Local,
1138                    paths,
1139                    window_id.map(WindowId::from),
1140                ));
1141            }
1142        }
1143
1144        if let Some(stack) = last_session_window_stack {
1145            workspaces.sort_by_key(|(_, _, window_id)| {
1146                window_id
1147                    .and_then(|id| stack.iter().position(|&order_id| order_id == id))
1148                    .unwrap_or(usize::MAX)
1149            });
1150        }
1151
1152        Ok(workspaces
1153            .into_iter()
1154            .map(|(location, paths, _)| (location, paths))
1155            .collect::<Vec<_>>())
1156    }
1157
1158    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1159        Ok(self
1160            .get_pane_group(workspace_id, None)?
1161            .into_iter()
1162            .next()
1163            .unwrap_or_else(|| {
1164                SerializedPaneGroup::Pane(SerializedPane {
1165                    active: true,
1166                    children: vec![],
1167                    pinned_count: 0,
1168                })
1169            }))
1170    }
1171
1172    fn get_pane_group(
1173        &self,
1174        workspace_id: WorkspaceId,
1175        group_id: Option<GroupId>,
1176    ) -> Result<Vec<SerializedPaneGroup>> {
1177        type GroupKey = (Option<GroupId>, WorkspaceId);
1178        type GroupOrPane = (
1179            Option<GroupId>,
1180            Option<SerializedAxis>,
1181            Option<PaneId>,
1182            Option<bool>,
1183            Option<usize>,
1184            Option<String>,
1185        );
1186        self.select_bound::<GroupKey, GroupOrPane>(sql!(
1187            SELECT group_id, axis, pane_id, active, pinned_count, flexes
1188                FROM (SELECT
1189                        group_id,
1190                        axis,
1191                        NULL as pane_id,
1192                        NULL as active,
1193                        NULL as pinned_count,
1194                        position,
1195                        parent_group_id,
1196                        workspace_id,
1197                        flexes
1198                      FROM pane_groups
1199                    UNION
1200                      SELECT
1201                        NULL,
1202                        NULL,
1203                        center_panes.pane_id,
1204                        panes.active as active,
1205                        pinned_count,
1206                        position,
1207                        parent_group_id,
1208                        panes.workspace_id as workspace_id,
1209                        NULL
1210                      FROM center_panes
1211                      JOIN panes ON center_panes.pane_id = panes.pane_id)
1212                WHERE parent_group_id IS ? AND workspace_id = ?
1213                ORDER BY position
1214        ))?((group_id, workspace_id))?
1215        .into_iter()
1216        .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1217            let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1218            if let Some((group_id, axis)) = group_id.zip(axis) {
1219                let flexes = flexes
1220                    .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1221                    .transpose()?;
1222
1223                Ok(SerializedPaneGroup::Group {
1224                    axis,
1225                    children: self.get_pane_group(workspace_id, Some(group_id))?,
1226                    flexes,
1227                })
1228            } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
1229                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
1230                    self.get_items(pane_id)?,
1231                    active,
1232                    pinned_count,
1233                )))
1234            } else {
1235                bail!("Pane Group Child was neither a pane group or a pane");
1236            }
1237        })
1238        // Filter out panes and pane groups which don't have any children or items
1239        .filter(|pane_group| match pane_group {
1240            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
1241            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
1242            _ => true,
1243        })
1244        .collect::<Result<_>>()
1245    }
1246
1247    fn save_pane_group(
1248        conn: &Connection,
1249        workspace_id: WorkspaceId,
1250        pane_group: &SerializedPaneGroup,
1251        parent: Option<(GroupId, usize)>,
1252    ) -> Result<()> {
1253        if parent.is_none() {
1254            log::debug!("Saving a pane group for workspace {workspace_id:?}");
1255        }
1256        match pane_group {
1257            SerializedPaneGroup::Group {
1258                axis,
1259                children,
1260                flexes,
1261            } => {
1262                let (parent_id, position) = parent.unzip();
1263
1264                let flex_string = flexes
1265                    .as_ref()
1266                    .map(|flexes| serde_json::json!(flexes).to_string());
1267
1268                let group_id = conn.select_row_bound::<_, i64>(sql!(
1269                    INSERT INTO pane_groups(
1270                        workspace_id,
1271                        parent_group_id,
1272                        position,
1273                        axis,
1274                        flexes
1275                    )
1276                    VALUES (?, ?, ?, ?, ?)
1277                    RETURNING group_id
1278                ))?((
1279                    workspace_id,
1280                    parent_id,
1281                    position,
1282                    *axis,
1283                    flex_string,
1284                ))?
1285                .context("Couldn't retrieve group_id from inserted pane_group")?;
1286
1287                for (position, group) in children.iter().enumerate() {
1288                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
1289                }
1290
1291                Ok(())
1292            }
1293            SerializedPaneGroup::Pane(pane) => {
1294                Self::save_pane(conn, workspace_id, pane, parent)?;
1295                Ok(())
1296            }
1297        }
1298    }
1299
1300    fn save_pane(
1301        conn: &Connection,
1302        workspace_id: WorkspaceId,
1303        pane: &SerializedPane,
1304        parent: Option<(GroupId, usize)>,
1305    ) -> Result<PaneId> {
1306        let pane_id = conn.select_row_bound::<_, i64>(sql!(
1307            INSERT INTO panes(workspace_id, active, pinned_count)
1308            VALUES (?, ?, ?)
1309            RETURNING pane_id
1310        ))?((workspace_id, pane.active, pane.pinned_count))?
1311        .context("Could not retrieve inserted pane_id")?;
1312
1313        let (parent_id, order) = parent.unzip();
1314        conn.exec_bound(sql!(
1315            INSERT INTO center_panes(pane_id, parent_group_id, position)
1316            VALUES (?, ?, ?)
1317        ))?((pane_id, parent_id, order))?;
1318
1319        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
1320
1321        Ok(pane_id)
1322    }
1323
1324    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
1325        self.select_bound(sql!(
1326            SELECT kind, item_id, active, preview FROM items
1327            WHERE pane_id = ?
1328                ORDER BY position
1329        ))?(pane_id)
1330    }
1331
1332    fn save_items(
1333        conn: &Connection,
1334        workspace_id: WorkspaceId,
1335        pane_id: PaneId,
1336        items: &[SerializedItem],
1337    ) -> Result<()> {
1338        let mut insert = conn.exec_bound(sql!(
1339            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
1340        )).context("Preparing insertion")?;
1341        for (position, item) in items.iter().enumerate() {
1342            insert((workspace_id, pane_id, position, item))?;
1343        }
1344
1345        Ok(())
1346    }
1347
1348    query! {
1349        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
1350            UPDATE workspaces
1351            SET timestamp = CURRENT_TIMESTAMP
1352            WHERE workspace_id = ?
1353        }
1354    }
1355
1356    query! {
1357        pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
1358            UPDATE workspaces
1359            SET window_state = ?2,
1360                window_x = ?3,
1361                window_y = ?4,
1362                window_width = ?5,
1363                window_height = ?6,
1364                display = ?7
1365            WHERE workspace_id = ?1
1366        }
1367    }
1368
1369    query! {
1370        pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
1371            UPDATE workspaces
1372            SET centered_layout = ?2
1373            WHERE workspace_id = ?1
1374        }
1375    }
1376
1377    query! {
1378        pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option<String>) -> Result<()> {
1379            UPDATE workspaces
1380            SET session_id = ?2
1381            WHERE workspace_id = ?1
1382        }
1383    }
1384
1385    pub async fn toolchain(
1386        &self,
1387        workspace_id: WorkspaceId,
1388        worktree_id: WorktreeId,
1389        relative_path: String,
1390        language_name: LanguageName,
1391    ) -> Result<Option<Toolchain>> {
1392        self.write(move |this| {
1393            let mut select = this
1394                .select_bound(sql!(
1395                    SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? AND relative_path = ?
1396                ))
1397                .context("Preparing insertion")?;
1398
1399            let toolchain: Vec<(String, String, String)> =
1400                select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize(), relative_path))?;
1401
1402            Ok(toolchain.into_iter().next().and_then(|(name, path, raw_json)| Some(Toolchain {
1403                name: name.into(),
1404                path: path.into(),
1405                language_name,
1406                as_json: serde_json::Value::from_str(&raw_json).ok()?
1407            })))
1408        })
1409        .await
1410    }
1411
1412    pub(crate) async fn toolchains(
1413        &self,
1414        workspace_id: WorkspaceId,
1415    ) -> Result<Vec<(Toolchain, WorktreeId, Arc<Path>)>> {
1416        self.write(move |this| {
1417            let mut select = this
1418                .select_bound(sql!(
1419                    SELECT name, path, worktree_id, relative_worktree_path, language_name, raw_json FROM toolchains WHERE workspace_id = ?
1420                ))
1421                .context("Preparing insertion")?;
1422
1423            let toolchain: Vec<(String, String, u64, String, String, String)> =
1424                select(workspace_id)?;
1425
1426            Ok(toolchain.into_iter().filter_map(|(name, path, worktree_id, relative_worktree_path, language_name, raw_json)| Some((Toolchain {
1427                name: name.into(),
1428                path: path.into(),
1429                language_name: LanguageName::new(&language_name),
1430                as_json: serde_json::Value::from_str(&raw_json).ok()?
1431            }, WorktreeId::from_proto(worktree_id), Arc::from(relative_worktree_path.as_ref())))).collect())
1432        })
1433        .await
1434    }
1435    pub async fn set_toolchain(
1436        &self,
1437        workspace_id: WorkspaceId,
1438        worktree_id: WorktreeId,
1439        relative_worktree_path: String,
1440        toolchain: Toolchain,
1441    ) -> Result<()> {
1442        log::debug!(
1443            "Setting toolchain for workspace, worktree: {worktree_id:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
1444            toolchain.name
1445        );
1446        self.write(move |conn| {
1447            let mut insert = conn
1448                .exec_bound(sql!(
1449                    INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?,  ?, ?)
1450                    ON CONFLICT DO
1451                    UPDATE SET
1452                        name = ?5,
1453                        path = ?6,
1454                        raw_json = ?7
1455                ))
1456                .context("Preparing insertion")?;
1457
1458            insert((
1459                workspace_id,
1460                worktree_id.to_usize(),
1461                relative_worktree_path,
1462                toolchain.language_name.as_ref(),
1463                toolchain.name.as_ref(),
1464                toolchain.path.as_ref(),
1465                toolchain.as_json.to_string(),
1466            ))?;
1467
1468            Ok(())
1469        }).await
1470    }
1471}
1472
1473pub fn delete_unloaded_items(
1474    alive_items: Vec<ItemId>,
1475    workspace_id: WorkspaceId,
1476    table: &'static str,
1477    db: &ThreadSafeConnection,
1478    cx: &mut App,
1479) -> Task<Result<()>> {
1480    let db = db.clone();
1481    cx.spawn(async move |_| {
1482        let placeholders = alive_items
1483            .iter()
1484            .map(|_| "?")
1485            .collect::<Vec<&str>>()
1486            .join(", ");
1487
1488        let query = format!(
1489            "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
1490        );
1491
1492        db.write(move |conn| {
1493            let mut statement = Statement::prepare(conn, query)?;
1494            let mut next_index = statement.bind(&workspace_id, 1)?;
1495            for id in alive_items {
1496                next_index = statement.bind(&id, next_index)?;
1497            }
1498            statement.exec()
1499        })
1500        .await
1501    })
1502}
1503
1504#[cfg(test)]
1505mod tests {
1506    use super::*;
1507    use crate::persistence::model::{
1508        SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
1509    };
1510    use gpui;
1511    use pretty_assertions::assert_eq;
1512    use std::{thread, time::Duration};
1513
1514    #[gpui::test]
1515    async fn test_breakpoints() {
1516        zlog::init_test();
1517
1518        let db = WorkspaceDb::open_test_db("test_breakpoints").await;
1519        let id = db.next_id().await.unwrap();
1520
1521        let path = Path::new("/tmp/test.rs");
1522
1523        let breakpoint = Breakpoint {
1524            position: 123,
1525            message: None,
1526            state: BreakpointState::Enabled,
1527            condition: None,
1528            hit_condition: None,
1529        };
1530
1531        let log_breakpoint = Breakpoint {
1532            position: 456,
1533            message: Some("Test log message".into()),
1534            state: BreakpointState::Enabled,
1535            condition: None,
1536            hit_condition: None,
1537        };
1538
1539        let disable_breakpoint = Breakpoint {
1540            position: 578,
1541            message: None,
1542            state: BreakpointState::Disabled,
1543            condition: None,
1544            hit_condition: None,
1545        };
1546
1547        let condition_breakpoint = Breakpoint {
1548            position: 789,
1549            message: None,
1550            state: BreakpointState::Enabled,
1551            condition: Some("x > 5".into()),
1552            hit_condition: None,
1553        };
1554
1555        let hit_condition_breakpoint = Breakpoint {
1556            position: 999,
1557            message: None,
1558            state: BreakpointState::Enabled,
1559            condition: None,
1560            hit_condition: Some(">= 3".into()),
1561        };
1562
1563        let workspace = SerializedWorkspace {
1564            id,
1565            paths: PathList::new(&["/tmp"]),
1566            location: SerializedWorkspaceLocation::Local,
1567            center_group: Default::default(),
1568            window_bounds: Default::default(),
1569            display: Default::default(),
1570            docks: Default::default(),
1571            centered_layout: false,
1572            breakpoints: {
1573                let mut map = collections::BTreeMap::default();
1574                map.insert(
1575                    Arc::from(path),
1576                    vec![
1577                        SourceBreakpoint {
1578                            row: breakpoint.position,
1579                            path: Arc::from(path),
1580                            message: breakpoint.message.clone(),
1581                            state: breakpoint.state,
1582                            condition: breakpoint.condition.clone(),
1583                            hit_condition: breakpoint.hit_condition.clone(),
1584                        },
1585                        SourceBreakpoint {
1586                            row: log_breakpoint.position,
1587                            path: Arc::from(path),
1588                            message: log_breakpoint.message.clone(),
1589                            state: log_breakpoint.state,
1590                            condition: log_breakpoint.condition.clone(),
1591                            hit_condition: log_breakpoint.hit_condition.clone(),
1592                        },
1593                        SourceBreakpoint {
1594                            row: disable_breakpoint.position,
1595                            path: Arc::from(path),
1596                            message: disable_breakpoint.message.clone(),
1597                            state: disable_breakpoint.state,
1598                            condition: disable_breakpoint.condition.clone(),
1599                            hit_condition: disable_breakpoint.hit_condition.clone(),
1600                        },
1601                        SourceBreakpoint {
1602                            row: condition_breakpoint.position,
1603                            path: Arc::from(path),
1604                            message: condition_breakpoint.message.clone(),
1605                            state: condition_breakpoint.state,
1606                            condition: condition_breakpoint.condition.clone(),
1607                            hit_condition: condition_breakpoint.hit_condition.clone(),
1608                        },
1609                        SourceBreakpoint {
1610                            row: hit_condition_breakpoint.position,
1611                            path: Arc::from(path),
1612                            message: hit_condition_breakpoint.message.clone(),
1613                            state: hit_condition_breakpoint.state,
1614                            condition: hit_condition_breakpoint.condition.clone(),
1615                            hit_condition: hit_condition_breakpoint.hit_condition.clone(),
1616                        },
1617                    ],
1618                );
1619                map
1620            },
1621            session_id: None,
1622            window_id: None,
1623        };
1624
1625        db.save_workspace(workspace.clone()).await;
1626
1627        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
1628        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
1629
1630        assert_eq!(loaded_breakpoints.len(), 5);
1631
1632        // normal breakpoint
1633        assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
1634        assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
1635        assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
1636        assert_eq!(
1637            loaded_breakpoints[0].hit_condition,
1638            breakpoint.hit_condition
1639        );
1640        assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
1641        assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
1642
1643        // enabled breakpoint
1644        assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
1645        assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
1646        assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
1647        assert_eq!(
1648            loaded_breakpoints[1].hit_condition,
1649            log_breakpoint.hit_condition
1650        );
1651        assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
1652        assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
1653
1654        // disable breakpoint
1655        assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
1656        assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
1657        assert_eq!(
1658            loaded_breakpoints[2].condition,
1659            disable_breakpoint.condition
1660        );
1661        assert_eq!(
1662            loaded_breakpoints[2].hit_condition,
1663            disable_breakpoint.hit_condition
1664        );
1665        assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
1666        assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
1667
1668        // condition breakpoint
1669        assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
1670        assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
1671        assert_eq!(
1672            loaded_breakpoints[3].condition,
1673            condition_breakpoint.condition
1674        );
1675        assert_eq!(
1676            loaded_breakpoints[3].hit_condition,
1677            condition_breakpoint.hit_condition
1678        );
1679        assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
1680        assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
1681
1682        // hit condition breakpoint
1683        assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
1684        assert_eq!(
1685            loaded_breakpoints[4].message,
1686            hit_condition_breakpoint.message
1687        );
1688        assert_eq!(
1689            loaded_breakpoints[4].condition,
1690            hit_condition_breakpoint.condition
1691        );
1692        assert_eq!(
1693            loaded_breakpoints[4].hit_condition,
1694            hit_condition_breakpoint.hit_condition
1695        );
1696        assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
1697        assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
1698    }
1699
1700    #[gpui::test]
1701    async fn test_remove_last_breakpoint() {
1702        zlog::init_test();
1703
1704        let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
1705        let id = db.next_id().await.unwrap();
1706
1707        let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
1708
1709        let breakpoint_to_remove = Breakpoint {
1710            position: 100,
1711            message: None,
1712            state: BreakpointState::Enabled,
1713            condition: None,
1714            hit_condition: None,
1715        };
1716
1717        let workspace = SerializedWorkspace {
1718            id,
1719            paths: PathList::new(&["/tmp"]),
1720            location: SerializedWorkspaceLocation::Local,
1721            center_group: Default::default(),
1722            window_bounds: Default::default(),
1723            display: Default::default(),
1724            docks: Default::default(),
1725            centered_layout: false,
1726            breakpoints: {
1727                let mut map = collections::BTreeMap::default();
1728                map.insert(
1729                    Arc::from(singular_path),
1730                    vec![SourceBreakpoint {
1731                        row: breakpoint_to_remove.position,
1732                        path: Arc::from(singular_path),
1733                        message: None,
1734                        state: BreakpointState::Enabled,
1735                        condition: None,
1736                        hit_condition: None,
1737                    }],
1738                );
1739                map
1740            },
1741            session_id: None,
1742            window_id: None,
1743        };
1744
1745        db.save_workspace(workspace.clone()).await;
1746
1747        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
1748        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
1749
1750        assert_eq!(loaded_breakpoints.len(), 1);
1751        assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
1752        assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
1753        assert_eq!(
1754            loaded_breakpoints[0].condition,
1755            breakpoint_to_remove.condition
1756        );
1757        assert_eq!(
1758            loaded_breakpoints[0].hit_condition,
1759            breakpoint_to_remove.hit_condition
1760        );
1761        assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
1762        assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
1763
1764        let workspace_without_breakpoint = SerializedWorkspace {
1765            id,
1766            paths: PathList::new(&["/tmp"]),
1767            location: SerializedWorkspaceLocation::Local,
1768            center_group: Default::default(),
1769            window_bounds: Default::default(),
1770            display: Default::default(),
1771            docks: Default::default(),
1772            centered_layout: false,
1773            breakpoints: collections::BTreeMap::default(),
1774            session_id: None,
1775            window_id: None,
1776        };
1777
1778        db.save_workspace(workspace_without_breakpoint.clone())
1779            .await;
1780
1781        let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
1782        let empty_breakpoints = loaded_after_remove
1783            .breakpoints
1784            .get(&Arc::from(singular_path));
1785
1786        assert!(empty_breakpoints.is_none());
1787    }
1788
1789    #[gpui::test]
1790    async fn test_next_id_stability() {
1791        zlog::init_test();
1792
1793        let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
1794
1795        db.write(|conn| {
1796            conn.migrate(
1797                "test_table",
1798                &[sql!(
1799                    CREATE TABLE test_table(
1800                        text TEXT,
1801                        workspace_id INTEGER,
1802                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1803                        ON DELETE CASCADE
1804                    ) STRICT;
1805                )],
1806            )
1807            .unwrap();
1808        })
1809        .await;
1810
1811        let id = db.next_id().await.unwrap();
1812        // Assert the empty row got inserted
1813        assert_eq!(
1814            Some(id),
1815            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
1816                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
1817            ))
1818            .unwrap()(id)
1819            .unwrap()
1820        );
1821
1822        db.write(move |conn| {
1823            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1824                .unwrap()(("test-text-1", id))
1825            .unwrap()
1826        })
1827        .await;
1828
1829        let test_text_1 = db
1830            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1831            .unwrap()(1)
1832        .unwrap()
1833        .unwrap();
1834        assert_eq!(test_text_1, "test-text-1");
1835    }
1836
1837    #[gpui::test]
1838    async fn test_workspace_id_stability() {
1839        zlog::init_test();
1840
1841        let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
1842
1843        db.write(|conn| {
1844            conn.migrate(
1845                "test_table",
1846                &[sql!(
1847                        CREATE TABLE test_table(
1848                            text TEXT,
1849                            workspace_id INTEGER,
1850                            FOREIGN KEY(workspace_id)
1851                                REFERENCES workspaces(workspace_id)
1852                            ON DELETE CASCADE
1853                        ) STRICT;)],
1854            )
1855        })
1856        .await
1857        .unwrap();
1858
1859        let mut workspace_1 = SerializedWorkspace {
1860            id: WorkspaceId(1),
1861            paths: PathList::new(&["/tmp", "/tmp2"]),
1862            location: SerializedWorkspaceLocation::Local,
1863            center_group: Default::default(),
1864            window_bounds: Default::default(),
1865            display: Default::default(),
1866            docks: Default::default(),
1867            centered_layout: false,
1868            breakpoints: Default::default(),
1869            session_id: None,
1870            window_id: None,
1871        };
1872
1873        let workspace_2 = SerializedWorkspace {
1874            id: WorkspaceId(2),
1875            paths: PathList::new(&["/tmp"]),
1876            location: SerializedWorkspaceLocation::Local,
1877            center_group: Default::default(),
1878            window_bounds: Default::default(),
1879            display: Default::default(),
1880            docks: Default::default(),
1881            centered_layout: false,
1882            breakpoints: Default::default(),
1883            session_id: None,
1884            window_id: None,
1885        };
1886
1887        db.save_workspace(workspace_1.clone()).await;
1888
1889        db.write(|conn| {
1890            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1891                .unwrap()(("test-text-1", 1))
1892            .unwrap();
1893        })
1894        .await;
1895
1896        db.save_workspace(workspace_2.clone()).await;
1897
1898        db.write(|conn| {
1899            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1900                .unwrap()(("test-text-2", 2))
1901            .unwrap();
1902        })
1903        .await;
1904
1905        workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
1906        db.save_workspace(workspace_1.clone()).await;
1907        db.save_workspace(workspace_1).await;
1908        db.save_workspace(workspace_2).await;
1909
1910        let test_text_2 = db
1911            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1912            .unwrap()(2)
1913        .unwrap()
1914        .unwrap();
1915        assert_eq!(test_text_2, "test-text-2");
1916
1917        let test_text_1 = db
1918            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1919            .unwrap()(1)
1920        .unwrap()
1921        .unwrap();
1922        assert_eq!(test_text_1, "test-text-1");
1923    }
1924
1925    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
1926        SerializedPaneGroup::Group {
1927            axis: SerializedAxis(axis),
1928            flexes: None,
1929            children,
1930        }
1931    }
1932
1933    #[gpui::test]
1934    async fn test_full_workspace_serialization() {
1935        zlog::init_test();
1936
1937        let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
1938
1939        //  -----------------
1940        //  | 1,2   | 5,6   |
1941        //  | - - - |       |
1942        //  | 3,4   |       |
1943        //  -----------------
1944        let center_group = group(
1945            Axis::Horizontal,
1946            vec![
1947                group(
1948                    Axis::Vertical,
1949                    vec![
1950                        SerializedPaneGroup::Pane(SerializedPane::new(
1951                            vec![
1952                                SerializedItem::new("Terminal", 5, false, false),
1953                                SerializedItem::new("Terminal", 6, true, false),
1954                            ],
1955                            false,
1956                            0,
1957                        )),
1958                        SerializedPaneGroup::Pane(SerializedPane::new(
1959                            vec![
1960                                SerializedItem::new("Terminal", 7, true, false),
1961                                SerializedItem::new("Terminal", 8, false, false),
1962                            ],
1963                            false,
1964                            0,
1965                        )),
1966                    ],
1967                ),
1968                SerializedPaneGroup::Pane(SerializedPane::new(
1969                    vec![
1970                        SerializedItem::new("Terminal", 9, false, false),
1971                        SerializedItem::new("Terminal", 10, true, false),
1972                    ],
1973                    false,
1974                    0,
1975                )),
1976            ],
1977        );
1978
1979        let workspace = SerializedWorkspace {
1980            id: WorkspaceId(5),
1981            paths: PathList::new(&["/tmp", "/tmp2"]),
1982            location: SerializedWorkspaceLocation::Local,
1983            center_group,
1984            window_bounds: Default::default(),
1985            breakpoints: Default::default(),
1986            display: Default::default(),
1987            docks: Default::default(),
1988            centered_layout: false,
1989            session_id: None,
1990            window_id: Some(999),
1991        };
1992
1993        db.save_workspace(workspace.clone()).await;
1994
1995        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
1996        assert_eq!(workspace, round_trip_workspace.unwrap());
1997
1998        // Test guaranteed duplicate IDs
1999        db.save_workspace(workspace.clone()).await;
2000        db.save_workspace(workspace.clone()).await;
2001
2002        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2003        assert_eq!(workspace, round_trip_workspace.unwrap());
2004    }
2005
2006    #[gpui::test]
2007    async fn test_workspace_assignment() {
2008        zlog::init_test();
2009
2010        let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2011
2012        let workspace_1 = SerializedWorkspace {
2013            id: WorkspaceId(1),
2014            paths: PathList::new(&["/tmp", "/tmp2"]),
2015            location: SerializedWorkspaceLocation::Local,
2016            center_group: Default::default(),
2017            window_bounds: Default::default(),
2018            breakpoints: Default::default(),
2019            display: Default::default(),
2020            docks: Default::default(),
2021            centered_layout: false,
2022            session_id: None,
2023            window_id: Some(1),
2024        };
2025
2026        let mut workspace_2 = SerializedWorkspace {
2027            id: WorkspaceId(2),
2028            paths: PathList::new(&["/tmp"]),
2029            location: SerializedWorkspaceLocation::Local,
2030            center_group: Default::default(),
2031            window_bounds: Default::default(),
2032            display: Default::default(),
2033            docks: Default::default(),
2034            centered_layout: false,
2035            breakpoints: Default::default(),
2036            session_id: None,
2037            window_id: Some(2),
2038        };
2039
2040        db.save_workspace(workspace_1.clone()).await;
2041        db.save_workspace(workspace_2.clone()).await;
2042
2043        // Test that paths are treated as a set
2044        assert_eq!(
2045            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2046            workspace_1
2047        );
2048        assert_eq!(
2049            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
2050            workspace_1
2051        );
2052
2053        // Make sure that other keys work
2054        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
2055        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
2056
2057        // Test 'mutate' case of updating a pre-existing id
2058        workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
2059
2060        db.save_workspace(workspace_2.clone()).await;
2061        assert_eq!(
2062            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2063            workspace_2
2064        );
2065
2066        // Test other mechanism for mutating
2067        let mut workspace_3 = SerializedWorkspace {
2068            id: WorkspaceId(3),
2069            paths: PathList::new(&["/tmp2", "/tmp"]),
2070            location: SerializedWorkspaceLocation::Local,
2071            center_group: Default::default(),
2072            window_bounds: Default::default(),
2073            breakpoints: Default::default(),
2074            display: Default::default(),
2075            docks: Default::default(),
2076            centered_layout: false,
2077            session_id: None,
2078            window_id: Some(3),
2079        };
2080
2081        db.save_workspace(workspace_3.clone()).await;
2082        assert_eq!(
2083            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2084            workspace_3
2085        );
2086
2087        // Make sure that updating paths differently also works
2088        workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
2089        db.save_workspace(workspace_3.clone()).await;
2090        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2091        assert_eq!(
2092            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2093                .unwrap(),
2094            workspace_3
2095        );
2096    }
2097
2098    #[gpui::test]
2099    async fn test_session_workspaces() {
2100        zlog::init_test();
2101
2102        let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
2103
2104        let workspace_1 = SerializedWorkspace {
2105            id: WorkspaceId(1),
2106            paths: PathList::new(&["/tmp1"]),
2107            location: SerializedWorkspaceLocation::Local,
2108            center_group: Default::default(),
2109            window_bounds: Default::default(),
2110            display: Default::default(),
2111            docks: Default::default(),
2112            centered_layout: false,
2113            breakpoints: Default::default(),
2114            session_id: Some("session-id-1".to_owned()),
2115            window_id: Some(10),
2116        };
2117
2118        let workspace_2 = SerializedWorkspace {
2119            id: WorkspaceId(2),
2120            paths: PathList::new(&["/tmp2"]),
2121            location: SerializedWorkspaceLocation::Local,
2122            center_group: Default::default(),
2123            window_bounds: Default::default(),
2124            display: Default::default(),
2125            docks: Default::default(),
2126            centered_layout: false,
2127            breakpoints: Default::default(),
2128            session_id: Some("session-id-1".to_owned()),
2129            window_id: Some(20),
2130        };
2131
2132        let workspace_3 = SerializedWorkspace {
2133            id: WorkspaceId(3),
2134            paths: PathList::new(&["/tmp3"]),
2135            location: SerializedWorkspaceLocation::Local,
2136            center_group: Default::default(),
2137            window_bounds: Default::default(),
2138            display: Default::default(),
2139            docks: Default::default(),
2140            centered_layout: false,
2141            breakpoints: Default::default(),
2142            session_id: Some("session-id-2".to_owned()),
2143            window_id: Some(30),
2144        };
2145
2146        let workspace_4 = SerializedWorkspace {
2147            id: WorkspaceId(4),
2148            paths: PathList::new(&["/tmp4"]),
2149            location: SerializedWorkspaceLocation::Local,
2150            center_group: Default::default(),
2151            window_bounds: Default::default(),
2152            display: Default::default(),
2153            docks: Default::default(),
2154            centered_layout: false,
2155            breakpoints: Default::default(),
2156            session_id: None,
2157            window_id: None,
2158        };
2159
2160        let connection_id = db
2161            .get_or_create_ssh_connection("my-host".to_string(), Some(1234), None)
2162            .await
2163            .unwrap();
2164
2165        let workspace_5 = SerializedWorkspace {
2166            id: WorkspaceId(5),
2167            paths: PathList::default(),
2168            location: SerializedWorkspaceLocation::Ssh(db.ssh_connection(connection_id).unwrap()),
2169            center_group: Default::default(),
2170            window_bounds: Default::default(),
2171            display: Default::default(),
2172            docks: Default::default(),
2173            centered_layout: false,
2174            breakpoints: Default::default(),
2175            session_id: Some("session-id-2".to_owned()),
2176            window_id: Some(50),
2177        };
2178
2179        let workspace_6 = SerializedWorkspace {
2180            id: WorkspaceId(6),
2181            paths: PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]),
2182            location: SerializedWorkspaceLocation::Local,
2183            center_group: Default::default(),
2184            window_bounds: Default::default(),
2185            breakpoints: Default::default(),
2186            display: Default::default(),
2187            docks: Default::default(),
2188            centered_layout: false,
2189            session_id: Some("session-id-3".to_owned()),
2190            window_id: Some(60),
2191        };
2192
2193        db.save_workspace(workspace_1.clone()).await;
2194        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2195        db.save_workspace(workspace_2.clone()).await;
2196        db.save_workspace(workspace_3.clone()).await;
2197        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2198        db.save_workspace(workspace_4.clone()).await;
2199        db.save_workspace(workspace_5.clone()).await;
2200        db.save_workspace(workspace_6.clone()).await;
2201
2202        let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2203        assert_eq!(locations.len(), 2);
2204        assert_eq!(locations[0].0, PathList::new(&["/tmp2"]));
2205        assert_eq!(locations[0].1, Some(20));
2206        assert_eq!(locations[1].0, PathList::new(&["/tmp1"]));
2207        assert_eq!(locations[1].1, Some(10));
2208
2209        let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
2210        assert_eq!(locations.len(), 2);
2211        assert_eq!(locations[0].0, PathList::default());
2212        assert_eq!(locations[0].1, Some(50));
2213        assert_eq!(locations[0].2, Some(connection_id));
2214        assert_eq!(locations[1].0, PathList::new(&["/tmp3"]));
2215        assert_eq!(locations[1].1, Some(30));
2216
2217        let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
2218        assert_eq!(locations.len(), 1);
2219        assert_eq!(
2220            locations[0].0,
2221            PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]),
2222        );
2223        assert_eq!(locations[0].1, Some(60));
2224    }
2225
2226    fn default_workspace<P: AsRef<Path>>(
2227        paths: &[P],
2228        center_group: &SerializedPaneGroup,
2229    ) -> SerializedWorkspace {
2230        SerializedWorkspace {
2231            id: WorkspaceId(4),
2232            paths: PathList::new(paths),
2233            location: SerializedWorkspaceLocation::Local,
2234            center_group: center_group.clone(),
2235            window_bounds: Default::default(),
2236            display: Default::default(),
2237            docks: Default::default(),
2238            breakpoints: Default::default(),
2239            centered_layout: false,
2240            session_id: None,
2241            window_id: None,
2242        }
2243    }
2244
2245    #[gpui::test]
2246    async fn test_last_session_workspace_locations() {
2247        let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
2248        let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
2249        let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
2250        let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
2251
2252        let db =
2253            WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
2254
2255        let workspaces = [
2256            (1, vec![dir1.path()], 9),
2257            (2, vec![dir2.path()], 5),
2258            (3, vec![dir3.path()], 8),
2259            (4, vec![dir4.path()], 2),
2260            (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
2261            (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
2262        ]
2263        .into_iter()
2264        .map(|(id, paths, window_id)| SerializedWorkspace {
2265            id: WorkspaceId(id),
2266            paths: PathList::new(paths.as_slice()),
2267            location: SerializedWorkspaceLocation::Local,
2268            center_group: Default::default(),
2269            window_bounds: Default::default(),
2270            display: Default::default(),
2271            docks: Default::default(),
2272            centered_layout: false,
2273            session_id: Some("one-session".to_owned()),
2274            breakpoints: Default::default(),
2275            window_id: Some(window_id),
2276        })
2277        .collect::<Vec<_>>();
2278
2279        for workspace in workspaces.iter() {
2280            db.save_workspace(workspace.clone()).await;
2281        }
2282
2283        let stack = Some(Vec::from([
2284            WindowId::from(2), // Top
2285            WindowId::from(8),
2286            WindowId::from(5),
2287            WindowId::from(9),
2288            WindowId::from(3),
2289            WindowId::from(4), // Bottom
2290        ]));
2291
2292        let locations = db
2293            .last_session_workspace_locations("one-session", stack)
2294            .unwrap();
2295        assert_eq!(
2296            locations,
2297            [
2298                (
2299                    SerializedWorkspaceLocation::Local,
2300                    PathList::new(&[dir4.path()])
2301                ),
2302                (
2303                    SerializedWorkspaceLocation::Local,
2304                    PathList::new(&[dir3.path()])
2305                ),
2306                (
2307                    SerializedWorkspaceLocation::Local,
2308                    PathList::new(&[dir2.path()])
2309                ),
2310                (
2311                    SerializedWorkspaceLocation::Local,
2312                    PathList::new(&[dir1.path()])
2313                ),
2314                (
2315                    SerializedWorkspaceLocation::Local,
2316                    PathList::new(&[dir1.path(), dir2.path(), dir3.path()])
2317                ),
2318                (
2319                    SerializedWorkspaceLocation::Local,
2320                    PathList::new(&[dir4.path(), dir3.path(), dir2.path()])
2321                ),
2322            ]
2323        );
2324    }
2325
2326    #[gpui::test]
2327    async fn test_last_session_workspace_locations_ssh_projects() {
2328        let db = WorkspaceDb::open_test_db(
2329            "test_serializing_workspaces_last_session_workspaces_ssh_projects",
2330        )
2331        .await;
2332
2333        let ssh_connections = [
2334            ("host-1", "my-user-1"),
2335            ("host-2", "my-user-2"),
2336            ("host-3", "my-user-3"),
2337            ("host-4", "my-user-4"),
2338        ]
2339        .into_iter()
2340        .map(|(host, user)| async {
2341            db.get_or_create_ssh_connection(host.to_string(), None, Some(user.to_string()))
2342                .await
2343                .unwrap();
2344            SerializedSshConnection {
2345                host: host.into(),
2346                port: None,
2347                user: Some(user.into()),
2348            }
2349        })
2350        .collect::<Vec<_>>();
2351
2352        let ssh_connections = futures::future::join_all(ssh_connections).await;
2353
2354        let workspaces = [
2355            (1, ssh_connections[0].clone(), 9),
2356            (2, ssh_connections[1].clone(), 5),
2357            (3, ssh_connections[2].clone(), 8),
2358            (4, ssh_connections[3].clone(), 2),
2359        ]
2360        .into_iter()
2361        .map(|(id, ssh_connection, window_id)| SerializedWorkspace {
2362            id: WorkspaceId(id),
2363            paths: PathList::default(),
2364            location: SerializedWorkspaceLocation::Ssh(ssh_connection),
2365            center_group: Default::default(),
2366            window_bounds: Default::default(),
2367            display: Default::default(),
2368            docks: Default::default(),
2369            centered_layout: false,
2370            session_id: Some("one-session".to_owned()),
2371            breakpoints: Default::default(),
2372            window_id: Some(window_id),
2373        })
2374        .collect::<Vec<_>>();
2375
2376        for workspace in workspaces.iter() {
2377            db.save_workspace(workspace.clone()).await;
2378        }
2379
2380        let stack = Some(Vec::from([
2381            WindowId::from(2), // Top
2382            WindowId::from(8),
2383            WindowId::from(5),
2384            WindowId::from(9), // Bottom
2385        ]));
2386
2387        let have = db
2388            .last_session_workspace_locations("one-session", stack)
2389            .unwrap();
2390        assert_eq!(have.len(), 4);
2391        assert_eq!(
2392            have[0],
2393            (
2394                SerializedWorkspaceLocation::Ssh(ssh_connections[3].clone()),
2395                PathList::default()
2396            )
2397        );
2398        assert_eq!(
2399            have[1],
2400            (
2401                SerializedWorkspaceLocation::Ssh(ssh_connections[2].clone()),
2402                PathList::default()
2403            )
2404        );
2405        assert_eq!(
2406            have[2],
2407            (
2408                SerializedWorkspaceLocation::Ssh(ssh_connections[1].clone()),
2409                PathList::default()
2410            )
2411        );
2412        assert_eq!(
2413            have[3],
2414            (
2415                SerializedWorkspaceLocation::Ssh(ssh_connections[0].clone()),
2416                PathList::default()
2417            )
2418        );
2419    }
2420
2421    #[gpui::test]
2422    async fn test_get_or_create_ssh_project() {
2423        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
2424
2425        let host = "example.com".to_string();
2426        let port = Some(22_u16);
2427        let user = Some("user".to_string());
2428
2429        let connection_id = db
2430            .get_or_create_ssh_connection(host.clone(), port, user.clone())
2431            .await
2432            .unwrap();
2433
2434        // Test that calling the function again with the same parameters returns the same project
2435        let same_connection = db
2436            .get_or_create_ssh_connection(host.clone(), port, user.clone())
2437            .await
2438            .unwrap();
2439
2440        assert_eq!(connection_id, same_connection);
2441
2442        // Test with different parameters
2443        let host2 = "otherexample.com".to_string();
2444        let port2 = None;
2445        let user2 = Some("otheruser".to_string());
2446
2447        let different_connection = db
2448            .get_or_create_ssh_connection(host2.clone(), port2, user2.clone())
2449            .await
2450            .unwrap();
2451
2452        assert_ne!(connection_id, different_connection);
2453    }
2454
2455    #[gpui::test]
2456    async fn test_get_or_create_ssh_project_with_null_user() {
2457        let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
2458
2459        let (host, port, user) = ("example.com".to_string(), None, None);
2460
2461        let connection_id = db
2462            .get_or_create_ssh_connection(host.clone(), port, None)
2463            .await
2464            .unwrap();
2465
2466        let same_connection_id = db
2467            .get_or_create_ssh_connection(host.clone(), port, user.clone())
2468            .await
2469            .unwrap();
2470
2471        assert_eq!(connection_id, same_connection_id);
2472    }
2473
2474    #[gpui::test]
2475    async fn test_get_ssh_connections() {
2476        let db = WorkspaceDb::open_test_db("test_get_ssh_connections").await;
2477
2478        let connections = [
2479            ("example.com".to_string(), None, None),
2480            (
2481                "anotherexample.com".to_string(),
2482                Some(123_u16),
2483                Some("user2".to_string()),
2484            ),
2485            ("yetanother.com".to_string(), Some(345_u16), None),
2486        ];
2487
2488        let mut ids = Vec::new();
2489        for (host, port, user) in connections.iter() {
2490            ids.push(
2491                db.get_or_create_ssh_connection(host.clone(), *port, user.clone())
2492                    .await
2493                    .unwrap(),
2494            );
2495        }
2496
2497        let stored_projects = db.ssh_connections().unwrap();
2498        assert_eq!(
2499            stored_projects,
2500            [
2501                (
2502                    ids[0],
2503                    SerializedSshConnection {
2504                        host: "example.com".into(),
2505                        port: None,
2506                        user: None,
2507                    }
2508                ),
2509                (
2510                    ids[1],
2511                    SerializedSshConnection {
2512                        host: "anotherexample.com".into(),
2513                        port: Some(123),
2514                        user: Some("user2".into()),
2515                    }
2516                ),
2517                (
2518                    ids[2],
2519                    SerializedSshConnection {
2520                        host: "yetanother.com".into(),
2521                        port: Some(345),
2522                        user: None,
2523                    }
2524                ),
2525            ]
2526            .into_iter()
2527            .collect::<HashMap<_, _>>(),
2528        );
2529    }
2530
2531    #[gpui::test]
2532    async fn test_simple_split() {
2533        zlog::init_test();
2534
2535        let db = WorkspaceDb::open_test_db("simple_split").await;
2536
2537        //  -----------------
2538        //  | 1,2   | 5,6   |
2539        //  | - - - |       |
2540        //  | 3,4   |       |
2541        //  -----------------
2542        let center_pane = group(
2543            Axis::Horizontal,
2544            vec![
2545                group(
2546                    Axis::Vertical,
2547                    vec![
2548                        SerializedPaneGroup::Pane(SerializedPane::new(
2549                            vec![
2550                                SerializedItem::new("Terminal", 1, false, false),
2551                                SerializedItem::new("Terminal", 2, true, false),
2552                            ],
2553                            false,
2554                            0,
2555                        )),
2556                        SerializedPaneGroup::Pane(SerializedPane::new(
2557                            vec![
2558                                SerializedItem::new("Terminal", 4, false, false),
2559                                SerializedItem::new("Terminal", 3, true, false),
2560                            ],
2561                            true,
2562                            0,
2563                        )),
2564                    ],
2565                ),
2566                SerializedPaneGroup::Pane(SerializedPane::new(
2567                    vec![
2568                        SerializedItem::new("Terminal", 5, true, false),
2569                        SerializedItem::new("Terminal", 6, false, false),
2570                    ],
2571                    false,
2572                    0,
2573                )),
2574            ],
2575        );
2576
2577        let workspace = default_workspace(&["/tmp"], &center_pane);
2578
2579        db.save_workspace(workspace.clone()).await;
2580
2581        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
2582
2583        assert_eq!(workspace.center_group, new_workspace.center_group);
2584    }
2585
2586    #[gpui::test]
2587    async fn test_cleanup_panes() {
2588        zlog::init_test();
2589
2590        let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
2591
2592        let center_pane = group(
2593            Axis::Horizontal,
2594            vec![
2595                group(
2596                    Axis::Vertical,
2597                    vec![
2598                        SerializedPaneGroup::Pane(SerializedPane::new(
2599                            vec![
2600                                SerializedItem::new("Terminal", 1, false, false),
2601                                SerializedItem::new("Terminal", 2, true, false),
2602                            ],
2603                            false,
2604                            0,
2605                        )),
2606                        SerializedPaneGroup::Pane(SerializedPane::new(
2607                            vec![
2608                                SerializedItem::new("Terminal", 4, false, false),
2609                                SerializedItem::new("Terminal", 3, true, false),
2610                            ],
2611                            true,
2612                            0,
2613                        )),
2614                    ],
2615                ),
2616                SerializedPaneGroup::Pane(SerializedPane::new(
2617                    vec![
2618                        SerializedItem::new("Terminal", 5, false, false),
2619                        SerializedItem::new("Terminal", 6, true, false),
2620                    ],
2621                    false,
2622                    0,
2623                )),
2624            ],
2625        );
2626
2627        let id = &["/tmp"];
2628
2629        let mut workspace = default_workspace(id, &center_pane);
2630
2631        db.save_workspace(workspace.clone()).await;
2632
2633        workspace.center_group = group(
2634            Axis::Vertical,
2635            vec![
2636                SerializedPaneGroup::Pane(SerializedPane::new(
2637                    vec![
2638                        SerializedItem::new("Terminal", 1, false, false),
2639                        SerializedItem::new("Terminal", 2, true, false),
2640                    ],
2641                    false,
2642                    0,
2643                )),
2644                SerializedPaneGroup::Pane(SerializedPane::new(
2645                    vec![
2646                        SerializedItem::new("Terminal", 4, true, false),
2647                        SerializedItem::new("Terminal", 3, false, false),
2648                    ],
2649                    true,
2650                    0,
2651                )),
2652            ],
2653        );
2654
2655        db.save_workspace(workspace.clone()).await;
2656
2657        let new_workspace = db.workspace_for_roots(id).unwrap();
2658
2659        assert_eq!(workspace.center_group, new_workspace.center_group);
2660    }
2661}