persistence.rs

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