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