persistence.rs

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