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