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