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