persistence.rs

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