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        language_name: LanguageName,
1338    ) -> Result<Option<Toolchain>> {
1339        self.write(move |this| {
1340            let mut select = this
1341                .select_bound(sql!(
1342                    SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ?
1343                ))
1344                .context("Preparing insertion")?;
1345
1346            let toolchain: Vec<(String, String, String)> =
1347                select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize()))?;
1348
1349            Ok(toolchain.into_iter().next().and_then(|(name, path, raw_json)| Some(Toolchain {
1350                name: name.into(),
1351                path: path.into(),
1352                language_name,
1353                as_json: serde_json::Value::from_str(&raw_json).ok()?
1354            })))
1355        })
1356        .await
1357    }
1358
1359    pub(crate) async fn toolchains(
1360        &self,
1361        workspace_id: WorkspaceId,
1362    ) -> Result<Vec<(Toolchain, WorktreeId, Arc<Path>)>> {
1363        self.write(move |this| {
1364            let mut select = this
1365                .select_bound(sql!(
1366                    SELECT name, path, worktree_id, relative_worktree_path, language_name, raw_json FROM toolchains WHERE workspace_id = ?
1367                ))
1368                .context("Preparing insertion")?;
1369
1370            let toolchain: Vec<(String, String, u64, String, String, String)> =
1371                select(workspace_id)?;
1372
1373            Ok(toolchain.into_iter().filter_map(|(name, path, worktree_id, relative_worktree_path, language_name, raw_json)| Some((Toolchain {
1374                name: name.into(),
1375                path: path.into(),
1376                language_name: LanguageName::new(&language_name),
1377                as_json: serde_json::Value::from_str(&raw_json).ok()?
1378            }, WorktreeId::from_proto(worktree_id), Arc::from(relative_worktree_path.as_ref())))).collect())
1379        })
1380        .await
1381    }
1382    pub async fn set_toolchain(
1383        &self,
1384        workspace_id: WorkspaceId,
1385        worktree_id: WorktreeId,
1386        relative_worktree_path: String,
1387        toolchain: Toolchain,
1388    ) -> Result<()> {
1389        self.write(move |conn| {
1390            let mut insert = conn
1391                .exec_bound(sql!(
1392                    INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path) VALUES (?, ?, ?, ?, ?,  ?)
1393                    ON CONFLICT DO
1394                    UPDATE SET
1395                        name = ?4,
1396                        path = ?5
1397
1398                ))
1399                .context("Preparing insertion")?;
1400
1401            insert((
1402                workspace_id,
1403                worktree_id.to_usize(),
1404                relative_worktree_path,
1405                toolchain.language_name.as_ref(),
1406                toolchain.name.as_ref(),
1407                toolchain.path.as_ref(),
1408            ))?;
1409
1410            Ok(())
1411        }).await
1412    }
1413}
1414
1415#[cfg(test)]
1416mod tests {
1417    use std::thread;
1418    use std::time::Duration;
1419
1420    use super::*;
1421    use crate::persistence::model::SerializedWorkspace;
1422    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
1423    use db::open_test_db;
1424    use gpui;
1425
1426    #[gpui::test]
1427    async fn test_breakpoints() {
1428        env_logger::try_init().ok();
1429
1430        let db = WorkspaceDb(open_test_db("test_breakpoints").await);
1431        let id = db.next_id().await.unwrap();
1432
1433        let path = Path::new("/tmp/test.rs");
1434
1435        let breakpoint = Breakpoint {
1436            position: 123,
1437            message: None,
1438            state: BreakpointState::Enabled,
1439            condition: None,
1440            hit_condition: None,
1441        };
1442
1443        let log_breakpoint = Breakpoint {
1444            position: 456,
1445            message: Some("Test log message".into()),
1446            state: BreakpointState::Enabled,
1447            condition: None,
1448            hit_condition: None,
1449        };
1450
1451        let disable_breakpoint = Breakpoint {
1452            position: 578,
1453            message: None,
1454            state: BreakpointState::Disabled,
1455            condition: None,
1456            hit_condition: None,
1457        };
1458
1459        let condition_breakpoint = Breakpoint {
1460            position: 789,
1461            message: None,
1462            state: BreakpointState::Enabled,
1463            condition: Some("x > 5".into()),
1464            hit_condition: None,
1465        };
1466
1467        let hit_condition_breakpoint = Breakpoint {
1468            position: 999,
1469            message: None,
1470            state: BreakpointState::Enabled,
1471            condition: None,
1472            hit_condition: Some(">= 3".into()),
1473        };
1474
1475        let workspace = SerializedWorkspace {
1476            id,
1477            location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1478            center_group: Default::default(),
1479            window_bounds: Default::default(),
1480            display: Default::default(),
1481            docks: Default::default(),
1482            centered_layout: false,
1483            breakpoints: {
1484                let mut map = collections::BTreeMap::default();
1485                map.insert(
1486                    Arc::from(path),
1487                    vec![
1488                        SourceBreakpoint {
1489                            row: breakpoint.position,
1490                            path: Arc::from(path),
1491                            message: breakpoint.message.clone(),
1492                            state: breakpoint.state,
1493                            condition: breakpoint.condition.clone(),
1494                            hit_condition: breakpoint.hit_condition.clone(),
1495                        },
1496                        SourceBreakpoint {
1497                            row: log_breakpoint.position,
1498                            path: Arc::from(path),
1499                            message: log_breakpoint.message.clone(),
1500                            state: log_breakpoint.state,
1501                            condition: log_breakpoint.condition.clone(),
1502                            hit_condition: log_breakpoint.hit_condition.clone(),
1503                        },
1504                        SourceBreakpoint {
1505                            row: disable_breakpoint.position,
1506                            path: Arc::from(path),
1507                            message: disable_breakpoint.message.clone(),
1508                            state: disable_breakpoint.state,
1509                            condition: disable_breakpoint.condition.clone(),
1510                            hit_condition: disable_breakpoint.hit_condition.clone(),
1511                        },
1512                        SourceBreakpoint {
1513                            row: condition_breakpoint.position,
1514                            path: Arc::from(path),
1515                            message: condition_breakpoint.message.clone(),
1516                            state: condition_breakpoint.state,
1517                            condition: condition_breakpoint.condition.clone(),
1518                            hit_condition: condition_breakpoint.hit_condition.clone(),
1519                        },
1520                        SourceBreakpoint {
1521                            row: hit_condition_breakpoint.position,
1522                            path: Arc::from(path),
1523                            message: hit_condition_breakpoint.message.clone(),
1524                            state: hit_condition_breakpoint.state,
1525                            condition: hit_condition_breakpoint.condition.clone(),
1526                            hit_condition: hit_condition_breakpoint.hit_condition.clone(),
1527                        },
1528                    ],
1529                );
1530                map
1531            },
1532            session_id: None,
1533            window_id: None,
1534        };
1535
1536        db.save_workspace(workspace.clone()).await;
1537
1538        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
1539        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
1540
1541        assert_eq!(loaded_breakpoints.len(), 5);
1542
1543        // normal breakpoint
1544        assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
1545        assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
1546        assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
1547        assert_eq!(
1548            loaded_breakpoints[0].hit_condition,
1549            breakpoint.hit_condition
1550        );
1551        assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
1552        assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
1553
1554        // enabled breakpoint
1555        assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
1556        assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
1557        assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
1558        assert_eq!(
1559            loaded_breakpoints[1].hit_condition,
1560            log_breakpoint.hit_condition
1561        );
1562        assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
1563        assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
1564
1565        // disable breakpoint
1566        assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
1567        assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
1568        assert_eq!(
1569            loaded_breakpoints[2].condition,
1570            disable_breakpoint.condition
1571        );
1572        assert_eq!(
1573            loaded_breakpoints[2].hit_condition,
1574            disable_breakpoint.hit_condition
1575        );
1576        assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
1577        assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
1578
1579        // condition breakpoint
1580        assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
1581        assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
1582        assert_eq!(
1583            loaded_breakpoints[3].condition,
1584            condition_breakpoint.condition
1585        );
1586        assert_eq!(
1587            loaded_breakpoints[3].hit_condition,
1588            condition_breakpoint.hit_condition
1589        );
1590        assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
1591        assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
1592
1593        // hit condition breakpoint
1594        assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
1595        assert_eq!(
1596            loaded_breakpoints[4].message,
1597            hit_condition_breakpoint.message
1598        );
1599        assert_eq!(
1600            loaded_breakpoints[4].condition,
1601            hit_condition_breakpoint.condition
1602        );
1603        assert_eq!(
1604            loaded_breakpoints[4].hit_condition,
1605            hit_condition_breakpoint.hit_condition
1606        );
1607        assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
1608        assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
1609    }
1610
1611    #[gpui::test]
1612    async fn test_remove_last_breakpoint() {
1613        env_logger::try_init().ok();
1614
1615        let db = WorkspaceDb(open_test_db("test_remove_last_breakpoint").await);
1616        let id = db.next_id().await.unwrap();
1617
1618        let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
1619
1620        let breakpoint_to_remove = Breakpoint {
1621            position: 100,
1622            message: None,
1623            state: BreakpointState::Enabled,
1624            condition: None,
1625            hit_condition: None,
1626        };
1627
1628        let workspace = SerializedWorkspace {
1629            id,
1630            location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1631            center_group: Default::default(),
1632            window_bounds: Default::default(),
1633            display: Default::default(),
1634            docks: Default::default(),
1635            centered_layout: false,
1636            breakpoints: {
1637                let mut map = collections::BTreeMap::default();
1638                map.insert(
1639                    Arc::from(singular_path),
1640                    vec![SourceBreakpoint {
1641                        row: breakpoint_to_remove.position,
1642                        path: Arc::from(singular_path),
1643                        message: None,
1644                        state: BreakpointState::Enabled,
1645                        condition: None,
1646                        hit_condition: None,
1647                    }],
1648                );
1649                map
1650            },
1651            session_id: None,
1652            window_id: None,
1653        };
1654
1655        db.save_workspace(workspace.clone()).await;
1656
1657        let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
1658        let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
1659
1660        assert_eq!(loaded_breakpoints.len(), 1);
1661        assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
1662        assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
1663        assert_eq!(
1664            loaded_breakpoints[0].condition,
1665            breakpoint_to_remove.condition
1666        );
1667        assert_eq!(
1668            loaded_breakpoints[0].hit_condition,
1669            breakpoint_to_remove.hit_condition
1670        );
1671        assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
1672        assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
1673
1674        let workspace_without_breakpoint = SerializedWorkspace {
1675            id,
1676            location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1677            center_group: Default::default(),
1678            window_bounds: Default::default(),
1679            display: Default::default(),
1680            docks: Default::default(),
1681            centered_layout: false,
1682            breakpoints: collections::BTreeMap::default(),
1683            session_id: None,
1684            window_id: None,
1685        };
1686
1687        db.save_workspace(workspace_without_breakpoint.clone())
1688            .await;
1689
1690        let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
1691        let empty_breakpoints = loaded_after_remove
1692            .breakpoints
1693            .get(&Arc::from(singular_path));
1694
1695        assert!(empty_breakpoints.is_none());
1696    }
1697
1698    #[gpui::test]
1699    async fn test_next_id_stability() {
1700        env_logger::try_init().ok();
1701
1702        let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
1703
1704        db.write(|conn| {
1705            conn.migrate(
1706                "test_table",
1707                &[sql!(
1708                    CREATE TABLE test_table(
1709                        text TEXT,
1710                        workspace_id INTEGER,
1711                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1712                        ON DELETE CASCADE
1713                    ) STRICT;
1714                )],
1715            )
1716            .unwrap();
1717        })
1718        .await;
1719
1720        let id = db.next_id().await.unwrap();
1721        // Assert the empty row got inserted
1722        assert_eq!(
1723            Some(id),
1724            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
1725                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
1726            ))
1727            .unwrap()(id)
1728            .unwrap()
1729        );
1730
1731        db.write(move |conn| {
1732            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1733                .unwrap()(("test-text-1", id))
1734            .unwrap()
1735        })
1736        .await;
1737
1738        let test_text_1 = db
1739            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1740            .unwrap()(1)
1741        .unwrap()
1742        .unwrap();
1743        assert_eq!(test_text_1, "test-text-1");
1744    }
1745
1746    #[gpui::test]
1747    async fn test_workspace_id_stability() {
1748        env_logger::try_init().ok();
1749
1750        let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
1751
1752        db.write(|conn| {
1753            conn.migrate(
1754                "test_table",
1755                &[sql!(
1756                        CREATE TABLE test_table(
1757                            text TEXT,
1758                            workspace_id INTEGER,
1759                            FOREIGN KEY(workspace_id)
1760                                REFERENCES workspaces(workspace_id)
1761                            ON DELETE CASCADE
1762                        ) STRICT;)],
1763            )
1764        })
1765        .await
1766        .unwrap();
1767
1768        let mut workspace_1 = SerializedWorkspace {
1769            id: WorkspaceId(1),
1770            location: SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]),
1771            center_group: Default::default(),
1772            window_bounds: Default::default(),
1773            display: Default::default(),
1774            docks: Default::default(),
1775            centered_layout: false,
1776            breakpoints: Default::default(),
1777            session_id: None,
1778            window_id: None,
1779        };
1780
1781        let workspace_2 = SerializedWorkspace {
1782            id: WorkspaceId(2),
1783            location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1784            center_group: Default::default(),
1785            window_bounds: Default::default(),
1786            display: Default::default(),
1787            docks: Default::default(),
1788            centered_layout: false,
1789            breakpoints: Default::default(),
1790            session_id: None,
1791            window_id: None,
1792        };
1793
1794        db.save_workspace(workspace_1.clone()).await;
1795
1796        db.write(|conn| {
1797            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1798                .unwrap()(("test-text-1", 1))
1799            .unwrap();
1800        })
1801        .await;
1802
1803        db.save_workspace(workspace_2.clone()).await;
1804
1805        db.write(|conn| {
1806            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1807                .unwrap()(("test-text-2", 2))
1808            .unwrap();
1809        })
1810        .await;
1811
1812        workspace_1.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp3"]);
1813        db.save_workspace(workspace_1.clone()).await;
1814        db.save_workspace(workspace_1).await;
1815        db.save_workspace(workspace_2).await;
1816
1817        let test_text_2 = db
1818            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1819            .unwrap()(2)
1820        .unwrap()
1821        .unwrap();
1822        assert_eq!(test_text_2, "test-text-2");
1823
1824        let test_text_1 = db
1825            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1826            .unwrap()(1)
1827        .unwrap()
1828        .unwrap();
1829        assert_eq!(test_text_1, "test-text-1");
1830    }
1831
1832    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
1833        SerializedPaneGroup::Group {
1834            axis: SerializedAxis(axis),
1835            flexes: None,
1836            children,
1837        }
1838    }
1839
1840    #[gpui::test]
1841    async fn test_full_workspace_serialization() {
1842        env_logger::try_init().ok();
1843
1844        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
1845
1846        //  -----------------
1847        //  | 1,2   | 5,6   |
1848        //  | - - - |       |
1849        //  | 3,4   |       |
1850        //  -----------------
1851        let center_group = group(
1852            Axis::Horizontal,
1853            vec![
1854                group(
1855                    Axis::Vertical,
1856                    vec![
1857                        SerializedPaneGroup::Pane(SerializedPane::new(
1858                            vec![
1859                                SerializedItem::new("Terminal", 5, false, false),
1860                                SerializedItem::new("Terminal", 6, true, false),
1861                            ],
1862                            false,
1863                            0,
1864                        )),
1865                        SerializedPaneGroup::Pane(SerializedPane::new(
1866                            vec![
1867                                SerializedItem::new("Terminal", 7, true, false),
1868                                SerializedItem::new("Terminal", 8, false, false),
1869                            ],
1870                            false,
1871                            0,
1872                        )),
1873                    ],
1874                ),
1875                SerializedPaneGroup::Pane(SerializedPane::new(
1876                    vec![
1877                        SerializedItem::new("Terminal", 9, false, false),
1878                        SerializedItem::new("Terminal", 10, true, false),
1879                    ],
1880                    false,
1881                    0,
1882                )),
1883            ],
1884        );
1885
1886        let workspace = SerializedWorkspace {
1887            id: WorkspaceId(5),
1888            location: SerializedWorkspaceLocation::Local(
1889                LocalPaths::new(["/tmp", "/tmp2"]),
1890                LocalPathsOrder::new([1, 0]),
1891            ),
1892            center_group,
1893            window_bounds: Default::default(),
1894            breakpoints: Default::default(),
1895            display: Default::default(),
1896            docks: Default::default(),
1897            centered_layout: false,
1898            session_id: None,
1899            window_id: Some(999),
1900        };
1901
1902        db.save_workspace(workspace.clone()).await;
1903
1904        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
1905        assert_eq!(workspace, round_trip_workspace.unwrap());
1906
1907        // Test guaranteed duplicate IDs
1908        db.save_workspace(workspace.clone()).await;
1909        db.save_workspace(workspace.clone()).await;
1910
1911        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
1912        assert_eq!(workspace, round_trip_workspace.unwrap());
1913    }
1914
1915    #[gpui::test]
1916    async fn test_workspace_assignment() {
1917        env_logger::try_init().ok();
1918
1919        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
1920
1921        let workspace_1 = SerializedWorkspace {
1922            id: WorkspaceId(1),
1923            location: SerializedWorkspaceLocation::Local(
1924                LocalPaths::new(["/tmp", "/tmp2"]),
1925                LocalPathsOrder::new([0, 1]),
1926            ),
1927            center_group: Default::default(),
1928            window_bounds: Default::default(),
1929            breakpoints: Default::default(),
1930            display: Default::default(),
1931            docks: Default::default(),
1932            centered_layout: false,
1933            session_id: None,
1934            window_id: Some(1),
1935        };
1936
1937        let mut workspace_2 = SerializedWorkspace {
1938            id: WorkspaceId(2),
1939            location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1940            center_group: Default::default(),
1941            window_bounds: Default::default(),
1942            display: Default::default(),
1943            docks: Default::default(),
1944            centered_layout: false,
1945            breakpoints: Default::default(),
1946            session_id: None,
1947            window_id: Some(2),
1948        };
1949
1950        db.save_workspace(workspace_1.clone()).await;
1951        db.save_workspace(workspace_2.clone()).await;
1952
1953        // Test that paths are treated as a set
1954        assert_eq!(
1955            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1956            workspace_1
1957        );
1958        assert_eq!(
1959            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
1960            workspace_1
1961        );
1962
1963        // Make sure that other keys work
1964        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
1965        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
1966
1967        // Test 'mutate' case of updating a pre-existing id
1968        workspace_2.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]);
1969
1970        db.save_workspace(workspace_2.clone()).await;
1971        assert_eq!(
1972            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1973            workspace_2
1974        );
1975
1976        // Test other mechanism for mutating
1977        let mut workspace_3 = SerializedWorkspace {
1978            id: WorkspaceId(3),
1979            location: SerializedWorkspaceLocation::Local(
1980                LocalPaths::new(["/tmp", "/tmp2"]),
1981                LocalPathsOrder::new([1, 0]),
1982            ),
1983            center_group: Default::default(),
1984            window_bounds: Default::default(),
1985            breakpoints: Default::default(),
1986            display: Default::default(),
1987            docks: Default::default(),
1988            centered_layout: false,
1989            session_id: None,
1990            window_id: Some(3),
1991        };
1992
1993        db.save_workspace(workspace_3.clone()).await;
1994        assert_eq!(
1995            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1996            workspace_3
1997        );
1998
1999        // Make sure that updating paths differently also works
2000        workspace_3.location =
2001            SerializedWorkspaceLocation::from_local_paths(["/tmp3", "/tmp4", "/tmp2"]);
2002        db.save_workspace(workspace_3.clone()).await;
2003        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2004        assert_eq!(
2005            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2006                .unwrap(),
2007            workspace_3
2008        );
2009    }
2010
2011    #[gpui::test]
2012    async fn test_session_workspaces() {
2013        env_logger::try_init().ok();
2014
2015        let db = WorkspaceDb(open_test_db("test_serializing_workspaces_session_id").await);
2016
2017        let workspace_1 = SerializedWorkspace {
2018            id: WorkspaceId(1),
2019            location: SerializedWorkspaceLocation::from_local_paths(["/tmp1"]),
2020            center_group: Default::default(),
2021            window_bounds: Default::default(),
2022            display: Default::default(),
2023            docks: Default::default(),
2024            centered_layout: false,
2025            breakpoints: Default::default(),
2026            session_id: Some("session-id-1".to_owned()),
2027            window_id: Some(10),
2028        };
2029
2030        let workspace_2 = SerializedWorkspace {
2031            id: WorkspaceId(2),
2032            location: SerializedWorkspaceLocation::from_local_paths(["/tmp2"]),
2033            center_group: Default::default(),
2034            window_bounds: Default::default(),
2035            display: Default::default(),
2036            docks: Default::default(),
2037            centered_layout: false,
2038            breakpoints: Default::default(),
2039            session_id: Some("session-id-1".to_owned()),
2040            window_id: Some(20),
2041        };
2042
2043        let workspace_3 = SerializedWorkspace {
2044            id: WorkspaceId(3),
2045            location: SerializedWorkspaceLocation::from_local_paths(["/tmp3"]),
2046            center_group: Default::default(),
2047            window_bounds: Default::default(),
2048            display: Default::default(),
2049            docks: Default::default(),
2050            centered_layout: false,
2051            breakpoints: Default::default(),
2052            session_id: Some("session-id-2".to_owned()),
2053            window_id: Some(30),
2054        };
2055
2056        let workspace_4 = SerializedWorkspace {
2057            id: WorkspaceId(4),
2058            location: SerializedWorkspaceLocation::from_local_paths(["/tmp4"]),
2059            center_group: Default::default(),
2060            window_bounds: Default::default(),
2061            display: Default::default(),
2062            docks: Default::default(),
2063            centered_layout: false,
2064            breakpoints: Default::default(),
2065            session_id: None,
2066            window_id: None,
2067        };
2068
2069        let ssh_project = db
2070            .get_or_create_ssh_project("my-host".to_string(), Some(1234), vec![], None)
2071            .await
2072            .unwrap();
2073
2074        let workspace_5 = SerializedWorkspace {
2075            id: WorkspaceId(5),
2076            location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()),
2077            center_group: Default::default(),
2078            window_bounds: Default::default(),
2079            display: Default::default(),
2080            docks: Default::default(),
2081            centered_layout: false,
2082            breakpoints: Default::default(),
2083            session_id: Some("session-id-2".to_owned()),
2084            window_id: Some(50),
2085        };
2086
2087        let workspace_6 = SerializedWorkspace {
2088            id: WorkspaceId(6),
2089            location: SerializedWorkspaceLocation::Local(
2090                LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
2091                LocalPathsOrder::new([2, 1, 0]),
2092            ),
2093            center_group: Default::default(),
2094            window_bounds: Default::default(),
2095            breakpoints: Default::default(),
2096            display: Default::default(),
2097            docks: Default::default(),
2098            centered_layout: false,
2099            session_id: Some("session-id-3".to_owned()),
2100            window_id: Some(60),
2101        };
2102
2103        db.save_workspace(workspace_1.clone()).await;
2104        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2105        db.save_workspace(workspace_2.clone()).await;
2106        db.save_workspace(workspace_3.clone()).await;
2107        thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2108        db.save_workspace(workspace_4.clone()).await;
2109        db.save_workspace(workspace_5.clone()).await;
2110        db.save_workspace(workspace_6.clone()).await;
2111
2112        let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2113        assert_eq!(locations.len(), 2);
2114        assert_eq!(locations[0].0, LocalPaths::new(["/tmp2"]));
2115        assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
2116        assert_eq!(locations[0].2, Some(20));
2117        assert_eq!(locations[1].0, LocalPaths::new(["/tmp1"]));
2118        assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
2119        assert_eq!(locations[1].2, Some(10));
2120
2121        let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
2122        assert_eq!(locations.len(), 2);
2123        let empty_paths: Vec<&str> = Vec::new();
2124        assert_eq!(locations[0].0, LocalPaths::new(empty_paths.iter()));
2125        assert_eq!(locations[0].1, LocalPathsOrder::new([]));
2126        assert_eq!(locations[0].2, Some(50));
2127        assert_eq!(locations[0].3, Some(ssh_project.id.0));
2128        assert_eq!(locations[1].0, LocalPaths::new(["/tmp3"]));
2129        assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
2130        assert_eq!(locations[1].2, Some(30));
2131
2132        let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
2133        assert_eq!(locations.len(), 1);
2134        assert_eq!(
2135            locations[0].0,
2136            LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
2137        );
2138        assert_eq!(locations[0].1, LocalPathsOrder::new([2, 1, 0]));
2139        assert_eq!(locations[0].2, Some(60));
2140    }
2141
2142    fn default_workspace<P: AsRef<Path>>(
2143        workspace_id: &[P],
2144        center_group: &SerializedPaneGroup,
2145    ) -> SerializedWorkspace {
2146        SerializedWorkspace {
2147            id: WorkspaceId(4),
2148            location: SerializedWorkspaceLocation::from_local_paths(workspace_id),
2149            center_group: center_group.clone(),
2150            window_bounds: Default::default(),
2151            display: Default::default(),
2152            docks: Default::default(),
2153            breakpoints: Default::default(),
2154            centered_layout: false,
2155            session_id: None,
2156            window_id: None,
2157        }
2158    }
2159
2160    #[gpui::test]
2161    async fn test_last_session_workspace_locations() {
2162        let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
2163        let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
2164        let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
2165        let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
2166
2167        let db =
2168            WorkspaceDb(open_test_db("test_serializing_workspaces_last_session_workspaces").await);
2169
2170        let workspaces = [
2171            (1, vec![dir1.path()], vec![0], 9),
2172            (2, vec![dir2.path()], vec![0], 5),
2173            (3, vec![dir3.path()], vec![0], 8),
2174            (4, vec![dir4.path()], vec![0], 2),
2175            (
2176                5,
2177                vec![dir1.path(), dir2.path(), dir3.path()],
2178                vec![0, 1, 2],
2179                3,
2180            ),
2181            (
2182                6,
2183                vec![dir2.path(), dir3.path(), dir4.path()],
2184                vec![2, 1, 0],
2185                4,
2186            ),
2187        ]
2188        .into_iter()
2189        .map(|(id, locations, order, window_id)| SerializedWorkspace {
2190            id: WorkspaceId(id),
2191            location: SerializedWorkspaceLocation::Local(
2192                LocalPaths::new(locations),
2193                LocalPathsOrder::new(order),
2194            ),
2195            center_group: Default::default(),
2196            window_bounds: Default::default(),
2197            display: Default::default(),
2198            docks: Default::default(),
2199            centered_layout: false,
2200            session_id: Some("one-session".to_owned()),
2201            breakpoints: Default::default(),
2202            window_id: Some(window_id),
2203        })
2204        .collect::<Vec<_>>();
2205
2206        for workspace in workspaces.iter() {
2207            db.save_workspace(workspace.clone()).await;
2208        }
2209
2210        let stack = Some(Vec::from([
2211            WindowId::from(2), // Top
2212            WindowId::from(8),
2213            WindowId::from(5),
2214            WindowId::from(9),
2215            WindowId::from(3),
2216            WindowId::from(4), // Bottom
2217        ]));
2218
2219        let have = db
2220            .last_session_workspace_locations("one-session", stack)
2221            .unwrap();
2222        assert_eq!(have.len(), 6);
2223        assert_eq!(
2224            have[0],
2225            SerializedWorkspaceLocation::from_local_paths(&[dir4.path()])
2226        );
2227        assert_eq!(
2228            have[1],
2229            SerializedWorkspaceLocation::from_local_paths([dir3.path()])
2230        );
2231        assert_eq!(
2232            have[2],
2233            SerializedWorkspaceLocation::from_local_paths([dir2.path()])
2234        );
2235        assert_eq!(
2236            have[3],
2237            SerializedWorkspaceLocation::from_local_paths([dir1.path()])
2238        );
2239        assert_eq!(
2240            have[4],
2241            SerializedWorkspaceLocation::Local(
2242                LocalPaths::new([dir1.path(), dir2.path(), dir3.path()]),
2243                LocalPathsOrder::new([0, 1, 2]),
2244            ),
2245        );
2246        assert_eq!(
2247            have[5],
2248            SerializedWorkspaceLocation::Local(
2249                LocalPaths::new([dir2.path(), dir3.path(), dir4.path()]),
2250                LocalPathsOrder::new([2, 1, 0]),
2251            ),
2252        );
2253    }
2254
2255    #[gpui::test]
2256    async fn test_last_session_workspace_locations_ssh_projects() {
2257        let db = WorkspaceDb(
2258            open_test_db("test_serializing_workspaces_last_session_workspaces_ssh_projects").await,
2259        );
2260
2261        let ssh_projects = [
2262            ("host-1", "my-user-1"),
2263            ("host-2", "my-user-2"),
2264            ("host-3", "my-user-3"),
2265            ("host-4", "my-user-4"),
2266        ]
2267        .into_iter()
2268        .map(|(host, user)| async {
2269            db.get_or_create_ssh_project(host.to_string(), None, vec![], Some(user.to_string()))
2270                .await
2271                .unwrap()
2272        })
2273        .collect::<Vec<_>>();
2274
2275        let ssh_projects = futures::future::join_all(ssh_projects).await;
2276
2277        let workspaces = [
2278            (1, ssh_projects[0].clone(), 9),
2279            (2, ssh_projects[1].clone(), 5),
2280            (3, ssh_projects[2].clone(), 8),
2281            (4, ssh_projects[3].clone(), 2),
2282        ]
2283        .into_iter()
2284        .map(|(id, ssh_project, window_id)| SerializedWorkspace {
2285            id: WorkspaceId(id),
2286            location: SerializedWorkspaceLocation::Ssh(ssh_project),
2287            center_group: Default::default(),
2288            window_bounds: Default::default(),
2289            display: Default::default(),
2290            docks: Default::default(),
2291            centered_layout: false,
2292            session_id: Some("one-session".to_owned()),
2293            breakpoints: Default::default(),
2294            window_id: Some(window_id),
2295        })
2296        .collect::<Vec<_>>();
2297
2298        for workspace in workspaces.iter() {
2299            db.save_workspace(workspace.clone()).await;
2300        }
2301
2302        let stack = Some(Vec::from([
2303            WindowId::from(2), // Top
2304            WindowId::from(8),
2305            WindowId::from(5),
2306            WindowId::from(9), // Bottom
2307        ]));
2308
2309        let have = db
2310            .last_session_workspace_locations("one-session", stack)
2311            .unwrap();
2312        assert_eq!(have.len(), 4);
2313        assert_eq!(
2314            have[0],
2315            SerializedWorkspaceLocation::Ssh(ssh_projects[3].clone())
2316        );
2317        assert_eq!(
2318            have[1],
2319            SerializedWorkspaceLocation::Ssh(ssh_projects[2].clone())
2320        );
2321        assert_eq!(
2322            have[2],
2323            SerializedWorkspaceLocation::Ssh(ssh_projects[1].clone())
2324        );
2325        assert_eq!(
2326            have[3],
2327            SerializedWorkspaceLocation::Ssh(ssh_projects[0].clone())
2328        );
2329    }
2330
2331    #[gpui::test]
2332    async fn test_get_or_create_ssh_project() {
2333        let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project").await);
2334
2335        let (host, port, paths, user) = (
2336            "example.com".to_string(),
2337            Some(22_u16),
2338            vec!["/home/user".to_string(), "/etc/nginx".to_string()],
2339            Some("user".to_string()),
2340        );
2341
2342        let project = db
2343            .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
2344            .await
2345            .unwrap();
2346
2347        assert_eq!(project.host, host);
2348        assert_eq!(project.paths, paths);
2349        assert_eq!(project.user, user);
2350
2351        // Test that calling the function again with the same parameters returns the same project
2352        let same_project = db
2353            .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
2354            .await
2355            .unwrap();
2356
2357        assert_eq!(project.id, same_project.id);
2358
2359        // Test with different parameters
2360        let (host2, paths2, user2) = (
2361            "otherexample.com".to_string(),
2362            vec!["/home/otheruser".to_string()],
2363            Some("otheruser".to_string()),
2364        );
2365
2366        let different_project = db
2367            .get_or_create_ssh_project(host2.clone(), None, paths2.clone(), user2.clone())
2368            .await
2369            .unwrap();
2370
2371        assert_ne!(project.id, different_project.id);
2372        assert_eq!(different_project.host, host2);
2373        assert_eq!(different_project.paths, paths2);
2374        assert_eq!(different_project.user, user2);
2375    }
2376
2377    #[gpui::test]
2378    async fn test_get_or_create_ssh_project_with_null_user() {
2379        let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project_with_null_user").await);
2380
2381        let (host, port, paths, user) = (
2382            "example.com".to_string(),
2383            None,
2384            vec!["/home/user".to_string()],
2385            None,
2386        );
2387
2388        let project = db
2389            .get_or_create_ssh_project(host.clone(), port, paths.clone(), None)
2390            .await
2391            .unwrap();
2392
2393        assert_eq!(project.host, host);
2394        assert_eq!(project.paths, paths);
2395        assert_eq!(project.user, None);
2396
2397        // Test that calling the function again with the same parameters returns the same project
2398        let same_project = db
2399            .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
2400            .await
2401            .unwrap();
2402
2403        assert_eq!(project.id, same_project.id);
2404    }
2405
2406    #[gpui::test]
2407    async fn test_get_ssh_projects() {
2408        let db = WorkspaceDb(open_test_db("test_get_ssh_projects").await);
2409
2410        let projects = vec![
2411            (
2412                "example.com".to_string(),
2413                None,
2414                vec!["/home/user".to_string()],
2415                None,
2416            ),
2417            (
2418                "anotherexample.com".to_string(),
2419                Some(123_u16),
2420                vec!["/home/user2".to_string()],
2421                Some("user2".to_string()),
2422            ),
2423            (
2424                "yetanother.com".to_string(),
2425                Some(345_u16),
2426                vec!["/home/user3".to_string(), "/proc/1234/exe".to_string()],
2427                None,
2428            ),
2429        ];
2430
2431        for (host, port, paths, user) in projects.iter() {
2432            let project = db
2433                .get_or_create_ssh_project(host.clone(), *port, paths.clone(), user.clone())
2434                .await
2435                .unwrap();
2436
2437            assert_eq!(&project.host, host);
2438            assert_eq!(&project.port, port);
2439            assert_eq!(&project.paths, paths);
2440            assert_eq!(&project.user, user);
2441        }
2442
2443        let stored_projects = db.ssh_projects().unwrap();
2444        assert_eq!(stored_projects.len(), projects.len());
2445    }
2446
2447    #[gpui::test]
2448    async fn test_simple_split() {
2449        env_logger::try_init().ok();
2450
2451        let db = WorkspaceDb(open_test_db("simple_split").await);
2452
2453        //  -----------------
2454        //  | 1,2   | 5,6   |
2455        //  | - - - |       |
2456        //  | 3,4   |       |
2457        //  -----------------
2458        let center_pane = group(
2459            Axis::Horizontal,
2460            vec![
2461                group(
2462                    Axis::Vertical,
2463                    vec![
2464                        SerializedPaneGroup::Pane(SerializedPane::new(
2465                            vec![
2466                                SerializedItem::new("Terminal", 1, false, false),
2467                                SerializedItem::new("Terminal", 2, true, false),
2468                            ],
2469                            false,
2470                            0,
2471                        )),
2472                        SerializedPaneGroup::Pane(SerializedPane::new(
2473                            vec![
2474                                SerializedItem::new("Terminal", 4, false, false),
2475                                SerializedItem::new("Terminal", 3, true, false),
2476                            ],
2477                            true,
2478                            0,
2479                        )),
2480                    ],
2481                ),
2482                SerializedPaneGroup::Pane(SerializedPane::new(
2483                    vec![
2484                        SerializedItem::new("Terminal", 5, true, false),
2485                        SerializedItem::new("Terminal", 6, false, false),
2486                    ],
2487                    false,
2488                    0,
2489                )),
2490            ],
2491        );
2492
2493        let workspace = default_workspace(&["/tmp"], &center_pane);
2494
2495        db.save_workspace(workspace.clone()).await;
2496
2497        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
2498
2499        assert_eq!(workspace.center_group, new_workspace.center_group);
2500    }
2501
2502    #[gpui::test]
2503    async fn test_cleanup_panes() {
2504        env_logger::try_init().ok();
2505
2506        let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
2507
2508        let center_pane = group(
2509            Axis::Horizontal,
2510            vec![
2511                group(
2512                    Axis::Vertical,
2513                    vec![
2514                        SerializedPaneGroup::Pane(SerializedPane::new(
2515                            vec![
2516                                SerializedItem::new("Terminal", 1, false, false),
2517                                SerializedItem::new("Terminal", 2, true, false),
2518                            ],
2519                            false,
2520                            0,
2521                        )),
2522                        SerializedPaneGroup::Pane(SerializedPane::new(
2523                            vec![
2524                                SerializedItem::new("Terminal", 4, false, false),
2525                                SerializedItem::new("Terminal", 3, true, false),
2526                            ],
2527                            true,
2528                            0,
2529                        )),
2530                    ],
2531                ),
2532                SerializedPaneGroup::Pane(SerializedPane::new(
2533                    vec![
2534                        SerializedItem::new("Terminal", 5, false, false),
2535                        SerializedItem::new("Terminal", 6, true, false),
2536                    ],
2537                    false,
2538                    0,
2539                )),
2540            ],
2541        );
2542
2543        let id = &["/tmp"];
2544
2545        let mut workspace = default_workspace(id, &center_pane);
2546
2547        db.save_workspace(workspace.clone()).await;
2548
2549        workspace.center_group = group(
2550            Axis::Vertical,
2551            vec![
2552                SerializedPaneGroup::Pane(SerializedPane::new(
2553                    vec![
2554                        SerializedItem::new("Terminal", 1, false, false),
2555                        SerializedItem::new("Terminal", 2, true, false),
2556                    ],
2557                    false,
2558                    0,
2559                )),
2560                SerializedPaneGroup::Pane(SerializedPane::new(
2561                    vec![
2562                        SerializedItem::new("Terminal", 4, true, false),
2563                        SerializedItem::new("Terminal", 3, false, false),
2564                    ],
2565                    true,
2566                    0,
2567                )),
2568            ],
2569        );
2570
2571        db.save_workspace(workspace.clone()).await;
2572
2573        let new_workspace = db.workspace_for_roots(id).unwrap();
2574
2575        assert_eq!(workspace.center_group, new_workspace.center_group);
2576    }
2577}