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