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