persistence.rs

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