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