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