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