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