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