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