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