persistence.rs

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