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    sql!(
 370        ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
 371    ),
 372    ];
 373}
 374
 375impl WorkspaceDb {
 376    /// Returns a serialized workspace for the given worktree_roots. If the passed array
 377    /// is empty, the most recent workspace is returned instead. If no workspace for the
 378    /// passed roots is stored, returns none.
 379    pub(crate) fn workspace_for_roots<P: AsRef<Path>>(
 380        &self,
 381        worktree_roots: &[P],
 382    ) -> Option<SerializedWorkspace> {
 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_dev_server_project(
 462        &self,
 463        dev_server_project_id: DevServerProjectId,
 464    ) -> Option<SerializedWorkspace> {
 465        // Note that we re-assign the workspace_id here in case it's empty
 466        // and we've grabbed the most recent workspace
 467        let (
 468            workspace_id,
 469            dev_server_project_id,
 470            window_bounds,
 471            display,
 472            centered_layout,
 473            docks,
 474            window_id,
 475        ): (
 476            WorkspaceId,
 477            Option<u64>,
 478            Option<SerializedWindowBounds>,
 479            Option<Uuid>,
 480            Option<bool>,
 481            DockStructure,
 482            Option<u64>,
 483        ) = self
 484            .select_row_bound(sql! {
 485                SELECT
 486                    workspace_id,
 487                    dev_server_project_id,
 488                    window_state,
 489                    window_x,
 490                    window_y,
 491                    window_width,
 492                    window_height,
 493                    display,
 494                    centered_layout,
 495                    left_dock_visible,
 496                    left_dock_active_panel,
 497                    left_dock_zoom,
 498                    right_dock_visible,
 499                    right_dock_active_panel,
 500                    right_dock_zoom,
 501                    bottom_dock_visible,
 502                    bottom_dock_active_panel,
 503                    bottom_dock_zoom,
 504                    window_id
 505                FROM workspaces
 506                WHERE dev_server_project_id = ?
 507            })
 508            .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id.0))
 509            .context("No workspaces found")
 510            .warn_on_err()
 511            .flatten()?;
 512
 513        let dev_server_project_id = dev_server_project_id?;
 514
 515        let dev_server_project: SerializedDevServerProject = self
 516            .select_row_bound(sql! {
 517                SELECT id, path, dev_server_name
 518                FROM dev_server_projects
 519                WHERE id = ?
 520            })
 521            .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id))
 522            .context("No remote project found")
 523            .warn_on_err()
 524            .flatten()?;
 525
 526        let location = SerializedWorkspaceLocation::DevServer(dev_server_project);
 527
 528        Some(SerializedWorkspace {
 529            id: workspace_id,
 530            location,
 531            center_group: self
 532                .get_center_pane_group(workspace_id)
 533                .context("Getting center group")
 534                .log_err()?,
 535            window_bounds,
 536            centered_layout: centered_layout.unwrap_or(false),
 537            display,
 538            docks,
 539            session_id: None,
 540            window_id,
 541        })
 542    }
 543
 544    pub(crate) fn workspace_for_ssh_project(
 545        &self,
 546        ssh_project: &SerializedSshProject,
 547    ) -> Option<SerializedWorkspace> {
 548        let (workspace_id, window_bounds, display, centered_layout, docks, window_id): (
 549            WorkspaceId,
 550            Option<SerializedWindowBounds>,
 551            Option<Uuid>,
 552            Option<bool>,
 553            DockStructure,
 554            Option<u64>,
 555        ) = self
 556            .select_row_bound(sql! {
 557                SELECT
 558                    workspace_id,
 559                    window_state,
 560                    window_x,
 561                    window_y,
 562                    window_width,
 563                    window_height,
 564                    display,
 565                    centered_layout,
 566                    left_dock_visible,
 567                    left_dock_active_panel,
 568                    left_dock_zoom,
 569                    right_dock_visible,
 570                    right_dock_active_panel,
 571                    right_dock_zoom,
 572                    bottom_dock_visible,
 573                    bottom_dock_active_panel,
 574                    bottom_dock_zoom,
 575                    window_id
 576                FROM workspaces
 577                WHERE ssh_project_id = ?
 578            })
 579            .and_then(|mut prepared_statement| (prepared_statement)(ssh_project.id.0))
 580            .context("No workspaces found")
 581            .warn_on_err()
 582            .flatten()?;
 583
 584        Some(SerializedWorkspace {
 585            id: workspace_id,
 586            location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()),
 587            center_group: self
 588                .get_center_pane_group(workspace_id)
 589                .context("Getting center group")
 590                .log_err()?,
 591            window_bounds,
 592            centered_layout: centered_layout.unwrap_or(false),
 593            display,
 594            docks,
 595            session_id: None,
 596            window_id,
 597        })
 598    }
 599
 600    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
 601    /// that used this workspace previously
 602    pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
 603        self.write(move |conn| {
 604            conn.with_savepoint("update_worktrees", || {
 605                // Clear out panes and pane_groups
 606                conn.exec_bound(sql!(
 607                    DELETE FROM pane_groups WHERE workspace_id = ?1;
 608                    DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
 609                .context("Clearing old panes")?;
 610
 611                match workspace.location {
 612                    SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => {
 613                        conn.exec_bound(sql!(
 614                            DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ?
 615                        ))?((&local_paths, workspace.id))
 616                        .context("clearing out old locations")?;
 617
 618                        // Upsert
 619                        let query = sql!(
 620                            INSERT INTO workspaces(
 621                                workspace_id,
 622                                local_paths,
 623                                local_paths_order,
 624                                left_dock_visible,
 625                                left_dock_active_panel,
 626                                left_dock_zoom,
 627                                right_dock_visible,
 628                                right_dock_active_panel,
 629                                right_dock_zoom,
 630                                bottom_dock_visible,
 631                                bottom_dock_active_panel,
 632                                bottom_dock_zoom,
 633                                session_id,
 634                                window_id,
 635                                timestamp
 636                            )
 637                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, CURRENT_TIMESTAMP)
 638                            ON CONFLICT DO
 639                            UPDATE SET
 640                                local_paths = ?2,
 641                                local_paths_order = ?3,
 642                                left_dock_visible = ?4,
 643                                left_dock_active_panel = ?5,
 644                                left_dock_zoom = ?6,
 645                                right_dock_visible = ?7,
 646                                right_dock_active_panel = ?8,
 647                                right_dock_zoom = ?9,
 648                                bottom_dock_visible = ?10,
 649                                bottom_dock_active_panel = ?11,
 650                                bottom_dock_zoom = ?12,
 651                                session_id = ?13,
 652                                window_id = ?14,
 653                                timestamp = CURRENT_TIMESTAMP
 654                        );
 655                        let mut prepared_query = conn.exec_bound(query)?;
 656                        let args = (workspace.id, &local_paths, &local_paths_order, workspace.docks, workspace.session_id, workspace.window_id);
 657
 658                        prepared_query(args).context("Updating workspace")?;
 659                    }
 660                    SerializedWorkspaceLocation::DevServer(dev_server_project) => {
 661                        conn.exec_bound(sql!(
 662                            DELETE FROM workspaces WHERE dev_server_project_id = ? AND workspace_id != ?
 663                        ))?((dev_server_project.id.0, workspace.id))
 664                        .context("clearing out old locations")?;
 665
 666                        conn.exec_bound(sql!(
 667                            INSERT INTO dev_server_projects(
 668                                id,
 669                                path,
 670                                dev_server_name
 671                            ) VALUES (?1, ?2, ?3)
 672                            ON CONFLICT DO
 673                            UPDATE SET
 674                                path = ?2,
 675                                dev_server_name = ?3
 676                        ))?(&dev_server_project)?;
 677
 678                        // Upsert
 679                        conn.exec_bound(sql!(
 680                            INSERT INTO workspaces(
 681                                workspace_id,
 682                                dev_server_project_id,
 683                                left_dock_visible,
 684                                left_dock_active_panel,
 685                                left_dock_zoom,
 686                                right_dock_visible,
 687                                right_dock_active_panel,
 688                                right_dock_zoom,
 689                                bottom_dock_visible,
 690                                bottom_dock_active_panel,
 691                                bottom_dock_zoom,
 692                                timestamp
 693                            )
 694                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
 695                            ON CONFLICT DO
 696                            UPDATE SET
 697                                dev_server_project_id = ?2,
 698                                left_dock_visible = ?3,
 699                                left_dock_active_panel = ?4,
 700                                left_dock_zoom = ?5,
 701                                right_dock_visible = ?6,
 702                                right_dock_active_panel = ?7,
 703                                right_dock_zoom = ?8,
 704                                bottom_dock_visible = ?9,
 705                                bottom_dock_active_panel = ?10,
 706                                bottom_dock_zoom = ?11,
 707                                timestamp = CURRENT_TIMESTAMP
 708                        ))?((
 709                            workspace.id,
 710                            dev_server_project.id.0,
 711                            workspace.docks,
 712                        ))
 713                        .context("Updating workspace")?;
 714                    },
 715                    SerializedWorkspaceLocation::Ssh(ssh_project) => {
 716                        conn.exec_bound(sql!(
 717                            DELETE FROM workspaces WHERE ssh_project_id = ? AND workspace_id != ?
 718                        ))?((ssh_project.id.0, workspace.id))
 719                        .context("clearing out old locations")?;
 720
 721                        // Upsert
 722                        conn.exec_bound(sql!(
 723                            INSERT INTO workspaces(
 724                                workspace_id,
 725                                ssh_project_id,
 726                                left_dock_visible,
 727                                left_dock_active_panel,
 728                                left_dock_zoom,
 729                                right_dock_visible,
 730                                right_dock_active_panel,
 731                                right_dock_zoom,
 732                                bottom_dock_visible,
 733                                bottom_dock_active_panel,
 734                                bottom_dock_zoom,
 735                                timestamp
 736                            )
 737                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
 738                            ON CONFLICT DO
 739                            UPDATE SET
 740                                ssh_project_id = ?2,
 741                                left_dock_visible = ?3,
 742                                left_dock_active_panel = ?4,
 743                                left_dock_zoom = ?5,
 744                                right_dock_visible = ?6,
 745                                right_dock_active_panel = ?7,
 746                                right_dock_zoom = ?8,
 747                                bottom_dock_visible = ?9,
 748                                bottom_dock_active_panel = ?10,
 749                                bottom_dock_zoom = ?11,
 750                                timestamp = CURRENT_TIMESTAMP
 751                        ))?((
 752                            workspace.id,
 753                            ssh_project.id.0,
 754                            workspace.docks,
 755                        ))
 756                        .context("Updating workspace")?;
 757                    }
 758                }
 759
 760                // Save center pane group
 761                Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
 762                    .context("save pane group in save workspace")?;
 763
 764                Ok(())
 765            })
 766            .log_err();
 767        })
 768        .await;
 769    }
 770
 771    pub(crate) async fn get_or_create_ssh_project(
 772        &self,
 773        host: String,
 774        port: Option<u16>,
 775        paths: Vec<String>,
 776        user: Option<String>,
 777    ) -> Result<SerializedSshProject> {
 778        let paths = serde_json::to_string(&paths)?;
 779        if let Some(project) = self
 780            .get_ssh_project(host.clone(), port, paths.clone(), user.clone())
 781            .await?
 782        {
 783            Ok(project)
 784        } else {
 785            self.insert_ssh_project(host, port, paths, user)
 786                .await?
 787                .ok_or_else(|| anyhow!("failed to insert ssh project"))
 788        }
 789    }
 790
 791    query! {
 792        async fn get_ssh_project(host: String, port: Option<u16>, paths: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
 793            SELECT id, host, port, paths, user
 794            FROM ssh_projects
 795            WHERE host IS ? AND port IS ? AND paths IS ? AND user IS ?
 796            LIMIT 1
 797        }
 798    }
 799
 800    query! {
 801        async fn insert_ssh_project(host: String, port: Option<u16>, paths: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
 802            INSERT INTO ssh_projects(
 803                host,
 804                port,
 805                paths,
 806                user
 807            ) VALUES (?1, ?2, ?3, ?4)
 808            RETURNING id, host, port, paths, user
 809        }
 810    }
 811
 812    query! {
 813        pub async fn next_id() -> Result<WorkspaceId> {
 814            INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
 815        }
 816    }
 817
 818    query! {
 819        fn recent_workspaces() -> Result<Vec<(WorkspaceId, LocalPaths, LocalPathsOrder, Option<u64>, Option<u64>)>> {
 820            SELECT workspace_id, local_paths, local_paths_order, dev_server_project_id, ssh_project_id
 821            FROM workspaces
 822            WHERE local_paths IS NOT NULL
 823                OR dev_server_project_id IS NOT NULL
 824                OR ssh_project_id IS NOT NULL
 825            ORDER BY timestamp DESC
 826        }
 827    }
 828
 829    query! {
 830        fn session_workspaces(session_id: String) -> Result<Vec<(LocalPaths, Option<u64>)>> {
 831            SELECT local_paths, window_id
 832            FROM workspaces
 833            WHERE session_id = ?1 AND dev_server_project_id IS NULL
 834            ORDER BY timestamp DESC
 835        }
 836    }
 837
 838    query! {
 839        fn dev_server_projects() -> Result<Vec<SerializedDevServerProject>> {
 840            SELECT id, path, dev_server_name
 841            FROM dev_server_projects
 842        }
 843    }
 844
 845    query! {
 846        fn ssh_projects() -> Result<Vec<SerializedSshProject>> {
 847            SELECT id, host, port, paths, user
 848            FROM ssh_projects
 849        }
 850    }
 851
 852    pub(crate) fn last_window(
 853        &self,
 854    ) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
 855        let mut prepared_query =
 856            self.select::<(Option<Uuid>, Option<SerializedWindowBounds>)>(sql!(
 857                SELECT
 858                display,
 859                window_state, window_x, window_y, window_width, window_height
 860                FROM workspaces
 861                WHERE local_paths
 862                IS NOT NULL
 863                ORDER BY timestamp DESC
 864                LIMIT 1
 865            ))?;
 866        let result = prepared_query()?;
 867        Ok(result.into_iter().next().unwrap_or((None, None)))
 868    }
 869
 870    query! {
 871        pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
 872            DELETE FROM workspaces
 873            WHERE workspace_id IS ?
 874        }
 875    }
 876
 877    pub async fn delete_workspace_by_dev_server_project_id(
 878        &self,
 879        id: DevServerProjectId,
 880    ) -> Result<()> {
 881        self.write(move |conn| {
 882            conn.exec_bound(sql!(
 883                DELETE FROM dev_server_projects WHERE id = ?
 884            ))?(id.0)?;
 885            conn.exec_bound(sql!(
 886                DELETE FROM workspaces
 887                WHERE dev_server_project_id IS ?
 888            ))?(id.0)
 889        })
 890        .await
 891    }
 892
 893    // Returns the recent locations which are still valid on disk and deletes ones which no longer
 894    // exist.
 895    pub async fn recent_workspaces_on_disk(
 896        &self,
 897    ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation)>> {
 898        let mut result = Vec::new();
 899        let mut delete_tasks = Vec::new();
 900        let dev_server_projects = self.dev_server_projects()?;
 901        let ssh_projects = self.ssh_projects()?;
 902
 903        for (id, location, order, dev_server_project_id, ssh_project_id) in
 904            self.recent_workspaces()?
 905        {
 906            if let Some(dev_server_project_id) = dev_server_project_id.map(DevServerProjectId) {
 907                if let Some(dev_server_project) = dev_server_projects
 908                    .iter()
 909                    .find(|rp| rp.id == dev_server_project_id)
 910                {
 911                    result.push((id, dev_server_project.clone().into()));
 912                } else {
 913                    delete_tasks.push(self.delete_workspace_by_id(id));
 914                }
 915                continue;
 916            }
 917
 918            if let Some(ssh_project_id) = ssh_project_id.map(SshProjectId) {
 919                if let Some(ssh_project) = ssh_projects.iter().find(|rp| rp.id == ssh_project_id) {
 920                    result.push((id, SerializedWorkspaceLocation::Ssh(ssh_project.clone())));
 921                } else {
 922                    delete_tasks.push(self.delete_workspace_by_id(id));
 923                }
 924                continue;
 925            }
 926
 927            if location.paths().iter().all(|path| path.exists())
 928                && location.paths().iter().any(|path| path.is_dir())
 929            {
 930                result.push((id, SerializedWorkspaceLocation::Local(location, order)));
 931            } else {
 932                delete_tasks.push(self.delete_workspace_by_id(id));
 933            }
 934        }
 935
 936        futures::future::join_all(delete_tasks).await;
 937        Ok(result)
 938    }
 939
 940    pub async fn last_workspace(&self) -> Result<Option<LocalPaths>> {
 941        Ok(self
 942            .recent_workspaces_on_disk()
 943            .await?
 944            .into_iter()
 945            .filter_map(|(_, location)| match location {
 946                SerializedWorkspaceLocation::Local(local_paths, _) => Some(local_paths),
 947                // Do not automatically reopen Dev Server and SSH workspaces
 948                SerializedWorkspaceLocation::DevServer(_) => None,
 949                SerializedWorkspaceLocation::Ssh(_) => None,
 950            })
 951            .next())
 952    }
 953
 954    // Returns the locations of the workspaces that were still opened when the last
 955    // session was closed (i.e. when Zed was quit).
 956    // If `last_session_window_order` is provided, the returned locations are ordered
 957    // according to that.
 958    pub fn last_session_workspace_locations(
 959        &self,
 960        last_session_id: &str,
 961        last_session_window_stack: Option<Vec<WindowId>>,
 962    ) -> Result<Vec<LocalPaths>> {
 963        let mut workspaces = Vec::new();
 964
 965        for (location, window_id) in self.session_workspaces(last_session_id.to_owned())? {
 966            if location.paths().iter().all(|path| path.exists())
 967                && location.paths().iter().any(|path| path.is_dir())
 968            {
 969                workspaces.push((location, window_id.map(WindowId::from)));
 970            }
 971        }
 972
 973        if let Some(stack) = last_session_window_stack {
 974            workspaces.sort_by_key(|(_, window_id)| {
 975                window_id
 976                    .and_then(|id| stack.iter().position(|&order_id| order_id == id))
 977                    .unwrap_or(usize::MAX)
 978            });
 979        }
 980
 981        Ok(workspaces
 982            .into_iter()
 983            .map(|(paths, _)| paths)
 984            .collect::<Vec<_>>())
 985    }
 986
 987    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
 988        Ok(self
 989            .get_pane_group(workspace_id, None)?
 990            .into_iter()
 991            .next()
 992            .unwrap_or_else(|| {
 993                SerializedPaneGroup::Pane(SerializedPane {
 994                    active: true,
 995                    children: vec![],
 996                    pinned_count: 0,
 997                })
 998            }))
 999    }
1000
1001    fn get_pane_group(
1002        &self,
1003        workspace_id: WorkspaceId,
1004        group_id: Option<GroupId>,
1005    ) -> Result<Vec<SerializedPaneGroup>> {
1006        type GroupKey = (Option<GroupId>, WorkspaceId);
1007        type GroupOrPane = (
1008            Option<GroupId>,
1009            Option<SerializedAxis>,
1010            Option<PaneId>,
1011            Option<bool>,
1012            Option<usize>,
1013            Option<String>,
1014        );
1015        self.select_bound::<GroupKey, GroupOrPane>(sql!(
1016            SELECT group_id, axis, pane_id, active, pinned_count, flexes
1017                FROM (SELECT
1018                        group_id,
1019                        axis,
1020                        NULL as pane_id,
1021                        NULL as active,
1022                        NULL as pinned_count,
1023                        position,
1024                        parent_group_id,
1025                        workspace_id,
1026                        flexes
1027                      FROM pane_groups
1028                    UNION
1029                      SELECT
1030                        NULL,
1031                        NULL,
1032                        center_panes.pane_id,
1033                        panes.active as active,
1034                        pinned_count,
1035                        position,
1036                        parent_group_id,
1037                        panes.workspace_id as workspace_id,
1038                        NULL
1039                      FROM center_panes
1040                      JOIN panes ON center_panes.pane_id = panes.pane_id)
1041                WHERE parent_group_id IS ? AND workspace_id = ?
1042                ORDER BY position
1043        ))?((group_id, workspace_id))?
1044        .into_iter()
1045        .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1046            let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1047            if let Some((group_id, axis)) = group_id.zip(axis) {
1048                let flexes = flexes
1049                    .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1050                    .transpose()?;
1051
1052                Ok(SerializedPaneGroup::Group {
1053                    axis,
1054                    children: self.get_pane_group(workspace_id, Some(group_id))?,
1055                    flexes,
1056                })
1057            } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
1058                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
1059                    self.get_items(pane_id)?,
1060                    active,
1061                    pinned_count,
1062                )))
1063            } else {
1064                bail!("Pane Group Child was neither a pane group or a pane");
1065            }
1066        })
1067        // Filter out panes and pane groups which don't have any children or items
1068        .filter(|pane_group| match pane_group {
1069            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
1070            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
1071            _ => true,
1072        })
1073        .collect::<Result<_>>()
1074    }
1075
1076    fn save_pane_group(
1077        conn: &Connection,
1078        workspace_id: WorkspaceId,
1079        pane_group: &SerializedPaneGroup,
1080        parent: Option<(GroupId, usize)>,
1081    ) -> Result<()> {
1082        match pane_group {
1083            SerializedPaneGroup::Group {
1084                axis,
1085                children,
1086                flexes,
1087            } => {
1088                let (parent_id, position) = parent.unzip();
1089
1090                let flex_string = flexes
1091                    .as_ref()
1092                    .map(|flexes| serde_json::json!(flexes).to_string());
1093
1094                let group_id = conn.select_row_bound::<_, i64>(sql!(
1095                    INSERT INTO pane_groups(
1096                        workspace_id,
1097                        parent_group_id,
1098                        position,
1099                        axis,
1100                        flexes
1101                    )
1102                    VALUES (?, ?, ?, ?, ?)
1103                    RETURNING group_id
1104                ))?((
1105                    workspace_id,
1106                    parent_id,
1107                    position,
1108                    *axis,
1109                    flex_string,
1110                ))?
1111                .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
1112
1113                for (position, group) in children.iter().enumerate() {
1114                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
1115                }
1116
1117                Ok(())
1118            }
1119            SerializedPaneGroup::Pane(pane) => {
1120                Self::save_pane(conn, workspace_id, pane, parent)?;
1121                Ok(())
1122            }
1123        }
1124    }
1125
1126    fn save_pane(
1127        conn: &Connection,
1128        workspace_id: WorkspaceId,
1129        pane: &SerializedPane,
1130        parent: Option<(GroupId, usize)>,
1131    ) -> Result<PaneId> {
1132        let pane_id = conn.select_row_bound::<_, i64>(sql!(
1133            INSERT INTO panes(workspace_id, active, pinned_count)
1134            VALUES (?, ?, ?)
1135            RETURNING pane_id
1136        ))?((workspace_id, pane.active, pane.pinned_count))?
1137        .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
1138
1139        let (parent_id, order) = parent.unzip();
1140        conn.exec_bound(sql!(
1141            INSERT INTO center_panes(pane_id, parent_group_id, position)
1142            VALUES (?, ?, ?)
1143        ))?((pane_id, parent_id, order))?;
1144
1145        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
1146
1147        Ok(pane_id)
1148    }
1149
1150    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
1151        self.select_bound(sql!(
1152            SELECT kind, item_id, active, preview FROM items
1153            WHERE pane_id = ?
1154                ORDER BY position
1155        ))?(pane_id)
1156    }
1157
1158    fn save_items(
1159        conn: &Connection,
1160        workspace_id: WorkspaceId,
1161        pane_id: PaneId,
1162        items: &[SerializedItem],
1163    ) -> Result<()> {
1164        let mut insert = conn.exec_bound(sql!(
1165            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
1166        )).context("Preparing insertion")?;
1167        for (position, item) in items.iter().enumerate() {
1168            insert((workspace_id, pane_id, position, item))?;
1169        }
1170
1171        Ok(())
1172    }
1173
1174    query! {
1175        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
1176            UPDATE workspaces
1177            SET timestamp = CURRENT_TIMESTAMP
1178            WHERE workspace_id = ?
1179        }
1180    }
1181
1182    query! {
1183        pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
1184            UPDATE workspaces
1185            SET window_state = ?2,
1186                window_x = ?3,
1187                window_y = ?4,
1188                window_width = ?5,
1189                window_height = ?6,
1190                display = ?7
1191            WHERE workspace_id = ?1
1192        }
1193    }
1194
1195    query! {
1196        pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
1197            UPDATE workspaces
1198            SET centered_layout = ?2
1199            WHERE workspace_id = ?1
1200        }
1201    }
1202}
1203
1204#[cfg(test)]
1205mod tests {
1206    use super::*;
1207    use crate::persistence::model::SerializedWorkspace;
1208    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
1209    use db::open_test_db;
1210    use gpui::{self};
1211
1212    #[gpui::test]
1213    async fn test_next_id_stability() {
1214        env_logger::try_init().ok();
1215
1216        let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
1217
1218        db.write(|conn| {
1219            conn.migrate(
1220                "test_table",
1221                &[sql!(
1222                    CREATE TABLE test_table(
1223                        text TEXT,
1224                        workspace_id INTEGER,
1225                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1226                        ON DELETE CASCADE
1227                    ) STRICT;
1228                )],
1229            )
1230            .unwrap();
1231        })
1232        .await;
1233
1234        let id = db.next_id().await.unwrap();
1235        // Assert the empty row got inserted
1236        assert_eq!(
1237            Some(id),
1238            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
1239                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
1240            ))
1241            .unwrap()(id)
1242            .unwrap()
1243        );
1244
1245        db.write(move |conn| {
1246            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1247                .unwrap()(("test-text-1", id))
1248            .unwrap()
1249        })
1250        .await;
1251
1252        let test_text_1 = db
1253            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1254            .unwrap()(1)
1255        .unwrap()
1256        .unwrap();
1257        assert_eq!(test_text_1, "test-text-1");
1258    }
1259
1260    #[gpui::test]
1261    async fn test_workspace_id_stability() {
1262        env_logger::try_init().ok();
1263
1264        let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
1265
1266        db.write(|conn| {
1267            conn.migrate(
1268                "test_table",
1269                &[sql!(
1270                        CREATE TABLE test_table(
1271                            text TEXT,
1272                            workspace_id INTEGER,
1273                            FOREIGN KEY(workspace_id)
1274                                REFERENCES workspaces(workspace_id)
1275                            ON DELETE CASCADE
1276                        ) STRICT;)],
1277            )
1278        })
1279        .await
1280        .unwrap();
1281
1282        let mut workspace_1 = SerializedWorkspace {
1283            id: WorkspaceId(1),
1284            location: SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]),
1285            center_group: Default::default(),
1286            window_bounds: Default::default(),
1287            display: Default::default(),
1288            docks: Default::default(),
1289            centered_layout: false,
1290            session_id: None,
1291            window_id: None,
1292        };
1293
1294        let workspace_2 = SerializedWorkspace {
1295            id: WorkspaceId(2),
1296            location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1297            center_group: Default::default(),
1298            window_bounds: Default::default(),
1299            display: Default::default(),
1300            docks: Default::default(),
1301            centered_layout: false,
1302            session_id: None,
1303            window_id: None,
1304        };
1305
1306        db.save_workspace(workspace_1.clone()).await;
1307
1308        db.write(|conn| {
1309            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1310                .unwrap()(("test-text-1", 1))
1311            .unwrap();
1312        })
1313        .await;
1314
1315        db.save_workspace(workspace_2.clone()).await;
1316
1317        db.write(|conn| {
1318            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1319                .unwrap()(("test-text-2", 2))
1320            .unwrap();
1321        })
1322        .await;
1323
1324        workspace_1.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp3"]);
1325        db.save_workspace(workspace_1.clone()).await;
1326        db.save_workspace(workspace_1).await;
1327        db.save_workspace(workspace_2).await;
1328
1329        let test_text_2 = db
1330            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1331            .unwrap()(2)
1332        .unwrap()
1333        .unwrap();
1334        assert_eq!(test_text_2, "test-text-2");
1335
1336        let test_text_1 = db
1337            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1338            .unwrap()(1)
1339        .unwrap()
1340        .unwrap();
1341        assert_eq!(test_text_1, "test-text-1");
1342    }
1343
1344    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
1345        SerializedPaneGroup::Group {
1346            axis: SerializedAxis(axis),
1347            flexes: None,
1348            children,
1349        }
1350    }
1351
1352    #[gpui::test]
1353    async fn test_full_workspace_serialization() {
1354        env_logger::try_init().ok();
1355
1356        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
1357
1358        //  -----------------
1359        //  | 1,2   | 5,6   |
1360        //  | - - - |       |
1361        //  | 3,4   |       |
1362        //  -----------------
1363        let center_group = group(
1364            Axis::Horizontal,
1365            vec![
1366                group(
1367                    Axis::Vertical,
1368                    vec![
1369                        SerializedPaneGroup::Pane(SerializedPane::new(
1370                            vec![
1371                                SerializedItem::new("Terminal", 5, false, false),
1372                                SerializedItem::new("Terminal", 6, true, false),
1373                            ],
1374                            false,
1375                            0,
1376                        )),
1377                        SerializedPaneGroup::Pane(SerializedPane::new(
1378                            vec![
1379                                SerializedItem::new("Terminal", 7, true, false),
1380                                SerializedItem::new("Terminal", 8, false, false),
1381                            ],
1382                            false,
1383                            0,
1384                        )),
1385                    ],
1386                ),
1387                SerializedPaneGroup::Pane(SerializedPane::new(
1388                    vec![
1389                        SerializedItem::new("Terminal", 9, false, false),
1390                        SerializedItem::new("Terminal", 10, true, false),
1391                    ],
1392                    false,
1393                    0,
1394                )),
1395            ],
1396        );
1397
1398        let workspace = SerializedWorkspace {
1399            id: WorkspaceId(5),
1400            location: SerializedWorkspaceLocation::Local(
1401                LocalPaths::new(["/tmp", "/tmp2"]),
1402                LocalPathsOrder::new([1, 0]),
1403            ),
1404            center_group,
1405            window_bounds: Default::default(),
1406            display: Default::default(),
1407            docks: Default::default(),
1408            centered_layout: false,
1409            session_id: None,
1410            window_id: Some(999),
1411        };
1412
1413        db.save_workspace(workspace.clone()).await;
1414
1415        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
1416        assert_eq!(workspace, round_trip_workspace.unwrap());
1417
1418        // Test guaranteed duplicate IDs
1419        db.save_workspace(workspace.clone()).await;
1420        db.save_workspace(workspace.clone()).await;
1421
1422        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
1423        assert_eq!(workspace, round_trip_workspace.unwrap());
1424    }
1425
1426    #[gpui::test]
1427    async fn test_workspace_assignment() {
1428        env_logger::try_init().ok();
1429
1430        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
1431
1432        let workspace_1 = SerializedWorkspace {
1433            id: WorkspaceId(1),
1434            location: SerializedWorkspaceLocation::Local(
1435                LocalPaths::new(["/tmp", "/tmp2"]),
1436                LocalPathsOrder::new([0, 1]),
1437            ),
1438            center_group: Default::default(),
1439            window_bounds: Default::default(),
1440            display: Default::default(),
1441            docks: Default::default(),
1442            centered_layout: false,
1443            session_id: None,
1444            window_id: Some(1),
1445        };
1446
1447        let mut workspace_2 = SerializedWorkspace {
1448            id: WorkspaceId(2),
1449            location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
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: None,
1456            window_id: Some(2),
1457        };
1458
1459        db.save_workspace(workspace_1.clone()).await;
1460        db.save_workspace(workspace_2.clone()).await;
1461
1462        // Test that paths are treated as a set
1463        assert_eq!(
1464            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1465            workspace_1
1466        );
1467        assert_eq!(
1468            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
1469            workspace_1
1470        );
1471
1472        // Make sure that other keys work
1473        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
1474        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
1475
1476        // Test 'mutate' case of updating a pre-existing id
1477        workspace_2.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]);
1478
1479        db.save_workspace(workspace_2.clone()).await;
1480        assert_eq!(
1481            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1482            workspace_2
1483        );
1484
1485        // Test other mechanism for mutating
1486        let mut workspace_3 = SerializedWorkspace {
1487            id: WorkspaceId(3),
1488            location: SerializedWorkspaceLocation::Local(
1489                LocalPaths::new(["/tmp", "/tmp2"]),
1490                LocalPathsOrder::new([1, 0]),
1491            ),
1492            center_group: Default::default(),
1493            window_bounds: Default::default(),
1494            display: Default::default(),
1495            docks: Default::default(),
1496            centered_layout: false,
1497            session_id: None,
1498            window_id: Some(3),
1499        };
1500
1501        db.save_workspace(workspace_3.clone()).await;
1502        assert_eq!(
1503            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1504            workspace_3
1505        );
1506
1507        // Make sure that updating paths differently also works
1508        workspace_3.location =
1509            SerializedWorkspaceLocation::from_local_paths(["/tmp3", "/tmp4", "/tmp2"]);
1510        db.save_workspace(workspace_3.clone()).await;
1511        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
1512        assert_eq!(
1513            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
1514                .unwrap(),
1515            workspace_3
1516        );
1517    }
1518
1519    #[gpui::test]
1520    async fn test_session_workspaces() {
1521        env_logger::try_init().ok();
1522
1523        let db = WorkspaceDb(open_test_db("test_serializing_workspaces_session_id").await);
1524
1525        let workspace_1 = SerializedWorkspace {
1526            id: WorkspaceId(1),
1527            location: SerializedWorkspaceLocation::from_local_paths(["/tmp1"]),
1528            center_group: Default::default(),
1529            window_bounds: Default::default(),
1530            display: Default::default(),
1531            docks: Default::default(),
1532            centered_layout: false,
1533            session_id: Some("session-id-1".to_owned()),
1534            window_id: Some(10),
1535        };
1536
1537        let workspace_2 = SerializedWorkspace {
1538            id: WorkspaceId(2),
1539            location: SerializedWorkspaceLocation::from_local_paths(["/tmp2"]),
1540            center_group: Default::default(),
1541            window_bounds: Default::default(),
1542            display: Default::default(),
1543            docks: Default::default(),
1544            centered_layout: false,
1545            session_id: Some("session-id-1".to_owned()),
1546            window_id: Some(20),
1547        };
1548
1549        let workspace_3 = SerializedWorkspace {
1550            id: WorkspaceId(3),
1551            location: SerializedWorkspaceLocation::from_local_paths(["/tmp3"]),
1552            center_group: Default::default(),
1553            window_bounds: Default::default(),
1554            display: Default::default(),
1555            docks: Default::default(),
1556            centered_layout: false,
1557            session_id: Some("session-id-2".to_owned()),
1558            window_id: Some(30),
1559        };
1560
1561        let workspace_4 = SerializedWorkspace {
1562            id: WorkspaceId(4),
1563            location: SerializedWorkspaceLocation::from_local_paths(["/tmp4"]),
1564            center_group: Default::default(),
1565            window_bounds: Default::default(),
1566            display: Default::default(),
1567            docks: Default::default(),
1568            centered_layout: false,
1569            session_id: None,
1570            window_id: None,
1571        };
1572
1573        db.save_workspace(workspace_1.clone()).await;
1574        db.save_workspace(workspace_2.clone()).await;
1575        db.save_workspace(workspace_3.clone()).await;
1576        db.save_workspace(workspace_4.clone()).await;
1577
1578        let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
1579        assert_eq!(locations.len(), 2);
1580        assert_eq!(locations[0].0, LocalPaths::new(["/tmp1"]));
1581        assert_eq!(locations[0].1, Some(10));
1582        assert_eq!(locations[1].0, LocalPaths::new(["/tmp2"]));
1583        assert_eq!(locations[1].1, Some(20));
1584
1585        let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
1586        assert_eq!(locations.len(), 1);
1587        assert_eq!(locations[0].0, LocalPaths::new(["/tmp3"]));
1588        assert_eq!(locations[0].1, Some(30));
1589    }
1590
1591    fn default_workspace<P: AsRef<Path>>(
1592        workspace_id: &[P],
1593        center_group: &SerializedPaneGroup,
1594    ) -> SerializedWorkspace {
1595        SerializedWorkspace {
1596            id: WorkspaceId(4),
1597            location: SerializedWorkspaceLocation::from_local_paths(workspace_id),
1598            center_group: center_group.clone(),
1599            window_bounds: Default::default(),
1600            display: Default::default(),
1601            docks: Default::default(),
1602            centered_layout: false,
1603            session_id: None,
1604            window_id: None,
1605        }
1606    }
1607
1608    #[gpui::test]
1609    async fn test_last_session_workspace_locations() {
1610        let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
1611        let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
1612        let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
1613        let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
1614
1615        let db =
1616            WorkspaceDb(open_test_db("test_serializing_workspaces_last_session_workspaces").await);
1617
1618        let workspaces = [
1619            (1, dir1.path().to_str().unwrap(), 9),
1620            (2, dir2.path().to_str().unwrap(), 5),
1621            (3, dir3.path().to_str().unwrap(), 8),
1622            (4, dir4.path().to_str().unwrap(), 2),
1623        ]
1624        .into_iter()
1625        .map(|(id, location, window_id)| SerializedWorkspace {
1626            id: WorkspaceId(id),
1627            location: SerializedWorkspaceLocation::from_local_paths([location]),
1628            center_group: Default::default(),
1629            window_bounds: Default::default(),
1630            display: Default::default(),
1631            docks: Default::default(),
1632            centered_layout: false,
1633            session_id: Some("one-session".to_owned()),
1634            window_id: Some(window_id),
1635        })
1636        .collect::<Vec<_>>();
1637
1638        for workspace in workspaces.iter() {
1639            db.save_workspace(workspace.clone()).await;
1640        }
1641
1642        let stack = Some(Vec::from([
1643            WindowId::from(2), // Top
1644            WindowId::from(8),
1645            WindowId::from(5),
1646            WindowId::from(9), // Bottom
1647        ]));
1648
1649        let have = db
1650            .last_session_workspace_locations("one-session", stack)
1651            .unwrap();
1652        assert_eq!(have.len(), 4);
1653        assert_eq!(have[0], LocalPaths::new([dir4.path().to_str().unwrap()]));
1654        assert_eq!(have[1], LocalPaths::new([dir3.path().to_str().unwrap()]));
1655        assert_eq!(have[2], LocalPaths::new([dir2.path().to_str().unwrap()]));
1656        assert_eq!(have[3], LocalPaths::new([dir1.path().to_str().unwrap()]));
1657    }
1658
1659    #[gpui::test]
1660    async fn test_get_or_create_ssh_project() {
1661        let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project").await);
1662
1663        let (host, port, paths, user) = (
1664            "example.com".to_string(),
1665            Some(22_u16),
1666            vec!["/home/user".to_string(), "/etc/nginx".to_string()],
1667            Some("user".to_string()),
1668        );
1669
1670        let project = db
1671            .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
1672            .await
1673            .unwrap();
1674
1675        assert_eq!(project.host, host);
1676        assert_eq!(project.paths, paths);
1677        assert_eq!(project.user, user);
1678
1679        // Test that calling the function again with the same parameters returns the same project
1680        let same_project = db
1681            .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
1682            .await
1683            .unwrap();
1684
1685        assert_eq!(project.id, same_project.id);
1686
1687        // Test with different parameters
1688        let (host2, paths2, user2) = (
1689            "otherexample.com".to_string(),
1690            vec!["/home/otheruser".to_string()],
1691            Some("otheruser".to_string()),
1692        );
1693
1694        let different_project = db
1695            .get_or_create_ssh_project(host2.clone(), None, paths2.clone(), user2.clone())
1696            .await
1697            .unwrap();
1698
1699        assert_ne!(project.id, different_project.id);
1700        assert_eq!(different_project.host, host2);
1701        assert_eq!(different_project.paths, paths2);
1702        assert_eq!(different_project.user, user2);
1703    }
1704
1705    #[gpui::test]
1706    async fn test_get_or_create_ssh_project_with_null_user() {
1707        let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project_with_null_user").await);
1708
1709        let (host, port, paths, user) = (
1710            "example.com".to_string(),
1711            None,
1712            vec!["/home/user".to_string()],
1713            None,
1714        );
1715
1716        let project = db
1717            .get_or_create_ssh_project(host.clone(), port, paths.clone(), None)
1718            .await
1719            .unwrap();
1720
1721        assert_eq!(project.host, host);
1722        assert_eq!(project.paths, paths);
1723        assert_eq!(project.user, None);
1724
1725        // Test that calling the function again with the same parameters returns the same project
1726        let same_project = db
1727            .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
1728            .await
1729            .unwrap();
1730
1731        assert_eq!(project.id, same_project.id);
1732    }
1733
1734    #[gpui::test]
1735    async fn test_get_ssh_projects() {
1736        let db = WorkspaceDb(open_test_db("test_get_ssh_projects").await);
1737
1738        let projects = vec![
1739            (
1740                "example.com".to_string(),
1741                None,
1742                vec!["/home/user".to_string()],
1743                None,
1744            ),
1745            (
1746                "anotherexample.com".to_string(),
1747                Some(123_u16),
1748                vec!["/home/user2".to_string()],
1749                Some("user2".to_string()),
1750            ),
1751            (
1752                "yetanother.com".to_string(),
1753                Some(345_u16),
1754                vec!["/home/user3".to_string(), "/proc/1234/exe".to_string()],
1755                None,
1756            ),
1757        ];
1758
1759        for (host, port, paths, user) in projects.iter() {
1760            let project = db
1761                .get_or_create_ssh_project(host.clone(), *port, paths.clone(), user.clone())
1762                .await
1763                .unwrap();
1764
1765            assert_eq!(&project.host, host);
1766            assert_eq!(&project.port, port);
1767            assert_eq!(&project.paths, paths);
1768            assert_eq!(&project.user, user);
1769        }
1770
1771        let stored_projects = db.ssh_projects().unwrap();
1772        assert_eq!(stored_projects.len(), projects.len());
1773    }
1774
1775    #[gpui::test]
1776    async fn test_simple_split() {
1777        env_logger::try_init().ok();
1778
1779        let db = WorkspaceDb(open_test_db("simple_split").await);
1780
1781        //  -----------------
1782        //  | 1,2   | 5,6   |
1783        //  | - - - |       |
1784        //  | 3,4   |       |
1785        //  -----------------
1786        let center_pane = group(
1787            Axis::Horizontal,
1788            vec![
1789                group(
1790                    Axis::Vertical,
1791                    vec![
1792                        SerializedPaneGroup::Pane(SerializedPane::new(
1793                            vec![
1794                                SerializedItem::new("Terminal", 1, false, false),
1795                                SerializedItem::new("Terminal", 2, true, false),
1796                            ],
1797                            false,
1798                            0,
1799                        )),
1800                        SerializedPaneGroup::Pane(SerializedPane::new(
1801                            vec![
1802                                SerializedItem::new("Terminal", 4, false, false),
1803                                SerializedItem::new("Terminal", 3, true, false),
1804                            ],
1805                            true,
1806                            0,
1807                        )),
1808                    ],
1809                ),
1810                SerializedPaneGroup::Pane(SerializedPane::new(
1811                    vec![
1812                        SerializedItem::new("Terminal", 5, true, false),
1813                        SerializedItem::new("Terminal", 6, false, false),
1814                    ],
1815                    false,
1816                    0,
1817                )),
1818            ],
1819        );
1820
1821        let workspace = default_workspace(&["/tmp"], &center_pane);
1822
1823        db.save_workspace(workspace.clone()).await;
1824
1825        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
1826
1827        assert_eq!(workspace.center_group, new_workspace.center_group);
1828    }
1829
1830    #[gpui::test]
1831    async fn test_cleanup_panes() {
1832        env_logger::try_init().ok();
1833
1834        let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
1835
1836        let center_pane = group(
1837            Axis::Horizontal,
1838            vec![
1839                group(
1840                    Axis::Vertical,
1841                    vec![
1842                        SerializedPaneGroup::Pane(SerializedPane::new(
1843                            vec![
1844                                SerializedItem::new("Terminal", 1, false, false),
1845                                SerializedItem::new("Terminal", 2, true, false),
1846                            ],
1847                            false,
1848                            0,
1849                        )),
1850                        SerializedPaneGroup::Pane(SerializedPane::new(
1851                            vec![
1852                                SerializedItem::new("Terminal", 4, false, false),
1853                                SerializedItem::new("Terminal", 3, true, false),
1854                            ],
1855                            true,
1856                            0,
1857                        )),
1858                    ],
1859                ),
1860                SerializedPaneGroup::Pane(SerializedPane::new(
1861                    vec![
1862                        SerializedItem::new("Terminal", 5, false, false),
1863                        SerializedItem::new("Terminal", 6, true, false),
1864                    ],
1865                    false,
1866                    0,
1867                )),
1868            ],
1869        );
1870
1871        let id = &["/tmp"];
1872
1873        let mut workspace = default_workspace(id, &center_pane);
1874
1875        db.save_workspace(workspace.clone()).await;
1876
1877        workspace.center_group = group(
1878            Axis::Vertical,
1879            vec![
1880                SerializedPaneGroup::Pane(SerializedPane::new(
1881                    vec![
1882                        SerializedItem::new("Terminal", 1, false, false),
1883                        SerializedItem::new("Terminal", 2, true, false),
1884                    ],
1885                    false,
1886                    0,
1887                )),
1888                SerializedPaneGroup::Pane(SerializedPane::new(
1889                    vec![
1890                        SerializedItem::new("Terminal", 4, true, false),
1891                        SerializedItem::new("Terminal", 3, false, false),
1892                    ],
1893                    true,
1894                    0,
1895                )),
1896            ],
1897        );
1898
1899        db.save_workspace(workspace.clone()).await;
1900
1901        let new_workspace = db.workspace_for_roots(id).unwrap();
1902
1903        assert_eq!(workspace.center_group, new_workspace.center_group);
1904    }
1905}