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