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};
   9
  10use sqlez::{
  11    bindable::{Bind, Column, StaticColumnCount},
  12    statement::Statement,
  13};
  14
  15use util::ResultExt;
  16use uuid::Uuid;
  17
  18use crate::WorkspaceId;
  19
  20use model::{
  21    GroupId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
  22    SerializedWorkspace,
  23};
  24
  25use self::model::{
  26    DockStructure, LocalPathsOrder, SerializedDevServerProject, SerializedWorkspaceLocation,
  27};
  28
  29#[derive(Copy, Clone, Debug, PartialEq)]
  30pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
  31impl sqlez::bindable::StaticColumnCount for SerializedAxis {}
  32impl sqlez::bindable::Bind for SerializedAxis {
  33    fn bind(
  34        &self,
  35        statement: &sqlez::statement::Statement,
  36        start_index: i32,
  37    ) -> anyhow::Result<i32> {
  38        match self.0 {
  39            gpui::Axis::Horizontal => "Horizontal",
  40            gpui::Axis::Vertical => "Vertical",
  41        }
  42        .bind(statement, start_index)
  43    }
  44}
  45
  46impl sqlez::bindable::Column for SerializedAxis {
  47    fn column(
  48        statement: &mut sqlez::statement::Statement,
  49        start_index: i32,
  50    ) -> anyhow::Result<(Self, i32)> {
  51        String::column(statement, start_index).and_then(|(axis_text, next_index)| {
  52            Ok((
  53                match axis_text.as_str() {
  54                    "Horizontal" => Self(Axis::Horizontal),
  55                    "Vertical" => Self(Axis::Vertical),
  56                    _ => anyhow::bail!("Stored serialized item kind is incorrect"),
  57                },
  58                next_index,
  59            ))
  60        })
  61    }
  62}
  63
  64#[derive(Copy, Clone, Debug, PartialEq, Default)]
  65pub(crate) struct SerializedWindowBounds(pub(crate) WindowBounds);
  66
  67impl StaticColumnCount for SerializedWindowBounds {
  68    fn column_count() -> usize {
  69        5
  70    }
  71}
  72
  73impl Bind for SerializedWindowBounds {
  74    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
  75        match self.0 {
  76            WindowBounds::Windowed(bounds) => {
  77                let next_index = statement.bind(&"Windowed", start_index)?;
  78                statement.bind(
  79                    &(
  80                        SerializedDevicePixels(bounds.origin.x),
  81                        SerializedDevicePixels(bounds.origin.y),
  82                        SerializedDevicePixels(bounds.size.width),
  83                        SerializedDevicePixels(bounds.size.height),
  84                    ),
  85                    next_index,
  86                )
  87            }
  88            WindowBounds::Maximized(bounds) => {
  89                let next_index = statement.bind(&"Maximized", start_index)?;
  90                statement.bind(
  91                    &(
  92                        SerializedDevicePixels(bounds.origin.x),
  93                        SerializedDevicePixels(bounds.origin.y),
  94                        SerializedDevicePixels(bounds.size.width),
  95                        SerializedDevicePixels(bounds.size.height),
  96                    ),
  97                    next_index,
  98                )
  99            }
 100            WindowBounds::Fullscreen(bounds) => {
 101                let next_index = statement.bind(&"FullScreen", start_index)?;
 102                statement.bind(
 103                    &(
 104                        SerializedDevicePixels(bounds.origin.x),
 105                        SerializedDevicePixels(bounds.origin.y),
 106                        SerializedDevicePixels(bounds.size.width),
 107                        SerializedDevicePixels(bounds.size.height),
 108                    ),
 109                    next_index,
 110                )
 111            }
 112        }
 113    }
 114}
 115
 116impl Column for SerializedWindowBounds {
 117    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 118        let (window_state, next_index) = String::column(statement, start_index)?;
 119        let status = match window_state.as_str() {
 120            "Windowed" | "Fixed" => {
 121                let ((x, y, width, height), _) = Column::column(statement, next_index)?;
 122                let x: i32 = x;
 123                let y: i32 = y;
 124                let width: i32 = width;
 125                let height: i32 = height;
 126                SerializedWindowBounds(WindowBounds::Windowed(Bounds {
 127                    origin: point(x.into(), y.into()),
 128                    size: size(width.into(), height.into()),
 129                }))
 130            }
 131            "Maximized" => {
 132                let ((x, y, width, height), _) = Column::column(statement, next_index)?;
 133                let x: i32 = x;
 134                let y: i32 = y;
 135                let width: i32 = width;
 136                let height: i32 = height;
 137                SerializedWindowBounds(WindowBounds::Maximized(Bounds {
 138                    origin: point(x.into(), y.into()),
 139                    size: size(width.into(), height.into()),
 140                }))
 141            }
 142            "FullScreen" => {
 143                let ((x, y, width, height), _) = Column::column(statement, next_index)?;
 144                let x: i32 = x;
 145                let y: i32 = y;
 146                let width: i32 = width;
 147                let height: i32 = height;
 148                SerializedWindowBounds(WindowBounds::Fullscreen(Bounds {
 149                    origin: point(x.into(), y.into()),
 150                    size: size(width.into(), height.into()),
 151                }))
 152            }
 153            _ => bail!("Window State did not have a valid string"),
 154        };
 155
 156        Ok((status, next_index + 4))
 157    }
 158}
 159
 160#[derive(Clone, Debug, PartialEq)]
 161struct SerializedDevicePixels(gpui::DevicePixels);
 162impl sqlez::bindable::StaticColumnCount for SerializedDevicePixels {}
 163
 164impl sqlez::bindable::Bind for SerializedDevicePixels {
 165    fn bind(
 166        &self,
 167        statement: &sqlez::statement::Statement,
 168        start_index: i32,
 169    ) -> anyhow::Result<i32> {
 170        let this: i32 = self.0.into();
 171        this.bind(statement, start_index)
 172    }
 173}
 174
 175define_connection! {
 176    // Current schema shape using pseudo-rust syntax:
 177    //
 178    // workspaces(
 179    //   workspace_id: usize, // Primary key for workspaces
 180    //   local_paths: Bincode<Vec<PathBuf>>,
 181    //   local_paths_order: Bincode<Vec<usize>>,
 182    //   dock_visible: bool, // Deprecated
 183    //   dock_anchor: DockAnchor, // Deprecated
 184    //   dock_pane: Option<usize>, // Deprecated
 185    //   left_sidebar_open: boolean,
 186    //   timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
 187    //   window_state: String, // WindowBounds Discriminant
 188    //   window_x: Option<f32>, // WindowBounds::Fixed RectF x
 189    //   window_y: Option<f32>, // WindowBounds::Fixed RectF y
 190    //   window_width: Option<f32>, // WindowBounds::Fixed RectF width
 191    //   window_height: Option<f32>, // WindowBounds::Fixed RectF height
 192    //   display: Option<Uuid>, // Display id
 193    //   fullscreen: Option<bool>, // Is the window fullscreen?
 194    //   centered_layout: Option<bool>, // Is the Centered Layout mode activated?
 195    // )
 196    //
 197    // pane_groups(
 198    //   group_id: usize, // Primary key for pane_groups
 199    //   workspace_id: usize, // References workspaces table
 200    //   parent_group_id: Option<usize>, // None indicates that this is the root node
 201    //   position: Optiopn<usize>, // None indicates that this is the root node
 202    //   axis: Option<Axis>, // 'Vertical', 'Horizontal'
 203    //   flexes: Option<Vec<f32>>, // A JSON array of floats
 204    // )
 205    //
 206    // panes(
 207    //     pane_id: usize, // Primary key for panes
 208    //     workspace_id: usize, // References workspaces table
 209    //     active: bool,
 210    // )
 211    //
 212    // center_panes(
 213    //     pane_id: usize, // Primary key for center_panes
 214    //     parent_group_id: Option<usize>, // References pane_groups. If none, this is the root
 215    //     position: Option<usize>, // None indicates this is the root
 216    // )
 217    //
 218    // CREATE TABLE items(
 219    //     item_id: usize, // This is the item's view id, so this is not unique
 220    //     workspace_id: usize, // References workspaces table
 221    //     pane_id: usize, // References panes table
 222    //     kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global
 223    //     position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column
 224    //     active: bool, // Indicates if this item is the active one in the pane
 225    //     preview: bool // Indicates if this item is a preview item
 226    // )
 227    pub static ref DB: WorkspaceDb<()> =
 228    &[sql!(
 229        CREATE TABLE workspaces(
 230            workspace_id INTEGER PRIMARY KEY,
 231            workspace_location BLOB UNIQUE,
 232            dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
 233            dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
 234            dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
 235            left_sidebar_open INTEGER, // Boolean
 236            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 237            FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
 238        ) STRICT;
 239
 240        CREATE TABLE pane_groups(
 241            group_id INTEGER PRIMARY KEY,
 242            workspace_id INTEGER NOT NULL,
 243            parent_group_id INTEGER, // NULL indicates that this is a root node
 244            position INTEGER, // NULL indicates that this is a root node
 245            axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
 246            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 247            ON DELETE CASCADE
 248            ON UPDATE CASCADE,
 249            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 250        ) STRICT;
 251
 252        CREATE TABLE panes(
 253            pane_id INTEGER PRIMARY KEY,
 254            workspace_id INTEGER NOT NULL,
 255            active INTEGER NOT NULL, // Boolean
 256            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 257            ON DELETE CASCADE
 258            ON UPDATE CASCADE
 259        ) STRICT;
 260
 261        CREATE TABLE center_panes(
 262            pane_id INTEGER PRIMARY KEY,
 263            parent_group_id INTEGER, // NULL means that this is a root pane
 264            position INTEGER, // NULL means that this is a root pane
 265            FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 266            ON DELETE CASCADE,
 267            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 268        ) STRICT;
 269
 270        CREATE TABLE items(
 271            item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
 272            workspace_id INTEGER NOT NULL,
 273            pane_id INTEGER NOT NULL,
 274            kind TEXT NOT NULL,
 275            position INTEGER NOT NULL,
 276            active INTEGER NOT NULL,
 277            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 278            ON DELETE CASCADE
 279            ON UPDATE CASCADE,
 280            FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 281            ON DELETE CASCADE,
 282            PRIMARY KEY(item_id, workspace_id)
 283        ) STRICT;
 284    ),
 285    sql!(
 286        ALTER TABLE workspaces ADD COLUMN window_state TEXT;
 287        ALTER TABLE workspaces ADD COLUMN window_x REAL;
 288        ALTER TABLE workspaces ADD COLUMN window_y REAL;
 289        ALTER TABLE workspaces ADD COLUMN window_width REAL;
 290        ALTER TABLE workspaces ADD COLUMN window_height REAL;
 291        ALTER TABLE workspaces ADD COLUMN display BLOB;
 292    ),
 293    // Drop foreign key constraint from workspaces.dock_pane to panes table.
 294    sql!(
 295        CREATE TABLE workspaces_2(
 296            workspace_id INTEGER PRIMARY KEY,
 297            workspace_location BLOB UNIQUE,
 298            dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
 299            dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
 300            dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
 301            left_sidebar_open INTEGER, // Boolean
 302            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 303            window_state TEXT,
 304            window_x REAL,
 305            window_y REAL,
 306            window_width REAL,
 307            window_height REAL,
 308            display BLOB
 309        ) STRICT;
 310        INSERT INTO workspaces_2 SELECT * FROM workspaces;
 311        DROP TABLE workspaces;
 312        ALTER TABLE workspaces_2 RENAME TO workspaces;
 313    ),
 314    // Add panels related information
 315    sql!(
 316        ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
 317        ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
 318        ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
 319        ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
 320        ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
 321        ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
 322    ),
 323    // Add panel zoom persistence
 324    sql!(
 325        ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
 326        ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
 327        ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
 328    ),
 329    // Add pane group flex data
 330    sql!(
 331        ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
 332    ),
 333    // Add fullscreen field to workspace
 334    // Deprecated, `WindowBounds` holds the fullscreen state now.
 335    // Preserving so users can downgrade Zed.
 336    sql!(
 337        ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
 338    ),
 339    // Add preview field to items
 340    sql!(
 341        ALTER TABLE items ADD COLUMN preview INTEGER; //bool
 342    ),
 343    // Add centered_layout field to workspace
 344    sql!(
 345        ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
 346    ),
 347    sql!(
 348        CREATE TABLE remote_projects (
 349            remote_project_id INTEGER NOT NULL UNIQUE,
 350            path TEXT,
 351            dev_server_name TEXT
 352        );
 353        ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
 354        ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
 355    ),
 356    sql!(
 357        DROP TABLE remote_projects;
 358        CREATE TABLE dev_server_projects (
 359            id INTEGER NOT NULL UNIQUE,
 360            path TEXT,
 361            dev_server_name TEXT
 362        );
 363        ALTER TABLE workspaces DROP COLUMN remote_project_id;
 364        ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
 365    ),
 366    sql!(
 367        ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
 368    ),
 369    ];
 370}
 371
 372impl WorkspaceDb {
 373    /// Returns a serialized workspace for the given worktree_roots. If the passed array
 374    /// is empty, the most recent workspace is returned instead. If no workspace for the
 375    /// passed roots is stored, returns none.
 376    pub(crate) fn workspace_for_roots<P: AsRef<Path>>(
 377        &self,
 378        worktree_roots: &[P],
 379    ) -> Option<SerializedWorkspace> {
 380        let local_paths = LocalPaths::new(worktree_roots);
 381
 382        // Note that we re-assign the workspace_id here in case it's empty
 383        // and we've grabbed the most recent workspace
 384        let (
 385            workspace_id,
 386            local_paths,
 387            local_paths_order,
 388            dev_server_project_id,
 389            window_bounds,
 390            display,
 391            centered_layout,
 392            docks,
 393        ): (
 394            WorkspaceId,
 395            Option<LocalPaths>,
 396            Option<LocalPathsOrder>,
 397            Option<u64>,
 398            Option<SerializedWindowBounds>,
 399            Option<Uuid>,
 400            Option<bool>,
 401            DockStructure,
 402        ) = self
 403            .select_row_bound(sql! {
 404                SELECT
 405                    workspace_id,
 406                    local_paths,
 407                    local_paths_order,
 408                    dev_server_project_id,
 409                    window_state,
 410                    window_x,
 411                    window_y,
 412                    window_width,
 413                    window_height,
 414                    display,
 415                    centered_layout,
 416                    left_dock_visible,
 417                    left_dock_active_panel,
 418                    left_dock_zoom,
 419                    right_dock_visible,
 420                    right_dock_active_panel,
 421                    right_dock_zoom,
 422                    bottom_dock_visible,
 423                    bottom_dock_active_panel,
 424                    bottom_dock_zoom
 425                FROM workspaces
 426                WHERE local_paths = ?
 427            })
 428            .and_then(|mut prepared_statement| (prepared_statement)(&local_paths))
 429            .context("No workspaces found")
 430            .warn_on_err()
 431            .flatten()?;
 432
 433        let location = if let Some(dev_server_project_id) = dev_server_project_id {
 434            let dev_server_project: SerializedDevServerProject = self
 435                .select_row_bound(sql! {
 436                    SELECT id, path, dev_server_name
 437                    FROM dev_server_projects
 438                    WHERE id = ?
 439                })
 440                .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id))
 441                .context("No remote project found")
 442                .warn_on_err()
 443                .flatten()?;
 444            SerializedWorkspaceLocation::DevServer(dev_server_project)
 445        } else if let Some(local_paths) = local_paths {
 446            match local_paths_order {
 447                Some(order) => SerializedWorkspaceLocation::Local(local_paths, order),
 448                None => {
 449                    let order = LocalPathsOrder::default_for_paths(&local_paths);
 450                    SerializedWorkspaceLocation::Local(local_paths, order)
 451                }
 452            }
 453        } else {
 454            return None;
 455        };
 456
 457        Some(SerializedWorkspace {
 458            id: workspace_id,
 459            location,
 460            center_group: self
 461                .get_center_pane_group(workspace_id)
 462                .context("Getting center group")
 463                .log_err()?,
 464            window_bounds,
 465            centered_layout: centered_layout.unwrap_or(false),
 466            display,
 467            docks,
 468        })
 469    }
 470
 471    pub(crate) fn workspace_for_dev_server_project(
 472        &self,
 473        dev_server_project_id: DevServerProjectId,
 474    ) -> Option<SerializedWorkspace> {
 475        // Note that we re-assign the workspace_id here in case it's empty
 476        // and we've grabbed the most recent workspace
 477        let (
 478            workspace_id,
 479            local_paths,
 480            local_paths_order,
 481            dev_server_project_id,
 482            window_bounds,
 483            display,
 484            centered_layout,
 485            docks,
 486        ): (
 487            WorkspaceId,
 488            Option<LocalPaths>,
 489            Option<LocalPathsOrder>,
 490            Option<u64>,
 491            Option<SerializedWindowBounds>,
 492            Option<Uuid>,
 493            Option<bool>,
 494            DockStructure,
 495        ) = self
 496            .select_row_bound(sql! {
 497                SELECT
 498                    workspace_id,
 499                    local_paths,
 500                    local_paths_order,
 501                    dev_server_project_id,
 502                    window_state,
 503                    window_x,
 504                    window_y,
 505                    window_width,
 506                    window_height,
 507                    display,
 508                    centered_layout,
 509                    left_dock_visible,
 510                    left_dock_active_panel,
 511                    left_dock_zoom,
 512                    right_dock_visible,
 513                    right_dock_active_panel,
 514                    right_dock_zoom,
 515                    bottom_dock_visible,
 516                    bottom_dock_active_panel,
 517                    bottom_dock_zoom
 518                FROM workspaces
 519                WHERE dev_server_project_id = ?
 520            })
 521            .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id.0))
 522            .context("No workspaces found")
 523            .warn_on_err()
 524            .flatten()?;
 525
 526        let location = if let Some(dev_server_project_id) = dev_server_project_id {
 527            let dev_server_project: SerializedDevServerProject = self
 528                .select_row_bound(sql! {
 529                    SELECT id, path, dev_server_name
 530                    FROM dev_server_projects
 531                    WHERE id = ?
 532                })
 533                .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id))
 534                .context("No remote project found")
 535                .warn_on_err()
 536                .flatten()?;
 537            SerializedWorkspaceLocation::DevServer(dev_server_project)
 538        } else if let Some(local_paths) = local_paths {
 539            match local_paths_order {
 540                Some(order) => SerializedWorkspaceLocation::Local(local_paths, order),
 541                None => {
 542                    let order = LocalPathsOrder::default_for_paths(&local_paths);
 543                    SerializedWorkspaceLocation::Local(local_paths, order)
 544                }
 545            }
 546        } else {
 547            return None;
 548        };
 549
 550        Some(SerializedWorkspace {
 551            id: workspace_id,
 552            location,
 553            center_group: self
 554                .get_center_pane_group(workspace_id)
 555                .context("Getting center group")
 556                .log_err()?,
 557            window_bounds,
 558            centered_layout: centered_layout.unwrap_or(false),
 559            display,
 560            docks,
 561        })
 562    }
 563
 564    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
 565    /// that used this workspace previously
 566    pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
 567        self.write(move |conn| {
 568            conn.with_savepoint("update_worktrees", || {
 569                // Clear out panes and pane_groups
 570                conn.exec_bound(sql!(
 571                    DELETE FROM pane_groups WHERE workspace_id = ?1;
 572                    DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
 573                .context("Clearing old panes")?;
 574
 575                match workspace.location {
 576                    SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => {
 577                        conn.exec_bound(sql!(
 578                            DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ?
 579                        ))?((&local_paths, workspace.id))
 580                        .context("clearing out old locations")?;
 581
 582                        // Upsert
 583                        conn.exec_bound(sql!(
 584                            INSERT INTO workspaces(
 585                                workspace_id,
 586                                local_paths,
 587                                local_paths_order,
 588                                left_dock_visible,
 589                                left_dock_active_panel,
 590                                left_dock_zoom,
 591                                right_dock_visible,
 592                                right_dock_active_panel,
 593                                right_dock_zoom,
 594                                bottom_dock_visible,
 595                                bottom_dock_active_panel,
 596                                bottom_dock_zoom,
 597                                timestamp
 598                            )
 599                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, CURRENT_TIMESTAMP)
 600                            ON CONFLICT DO
 601                            UPDATE SET
 602                                local_paths = ?2,
 603                                local_paths_order = ?3,
 604                                left_dock_visible = ?4,
 605                                left_dock_active_panel = ?5,
 606                                left_dock_zoom = ?6,
 607                                right_dock_visible = ?7,
 608                                right_dock_active_panel = ?8,
 609                                right_dock_zoom = ?9,
 610                                bottom_dock_visible = ?10,
 611                                bottom_dock_active_panel = ?11,
 612                                bottom_dock_zoom = ?12,
 613                                timestamp = CURRENT_TIMESTAMP
 614                        ))?((workspace.id, &local_paths, &local_paths_order, workspace.docks))
 615                        .context("Updating workspace")?;
 616                    }
 617                    SerializedWorkspaceLocation::DevServer(dev_server_project) => {
 618                        conn.exec_bound(sql!(
 619                            DELETE FROM workspaces WHERE dev_server_project_id = ? AND workspace_id != ?
 620                        ))?((dev_server_project.id.0, workspace.id))
 621                        .context("clearing out old locations")?;
 622
 623                        conn.exec_bound(sql!(
 624                            INSERT INTO dev_server_projects(
 625                                id,
 626                                path,
 627                                dev_server_name
 628                            ) VALUES (?1, ?2, ?3)
 629                            ON CONFLICT DO
 630                            UPDATE SET
 631                                path = ?2,
 632                                dev_server_name = ?3
 633                        ))?(&dev_server_project)?;
 634
 635                        // Upsert
 636                        conn.exec_bound(sql!(
 637                            INSERT INTO workspaces(
 638                                workspace_id,
 639                                dev_server_project_id,
 640                                left_dock_visible,
 641                                left_dock_active_panel,
 642                                left_dock_zoom,
 643                                right_dock_visible,
 644                                right_dock_active_panel,
 645                                right_dock_zoom,
 646                                bottom_dock_visible,
 647                                bottom_dock_active_panel,
 648                                bottom_dock_zoom,
 649                                timestamp
 650                            )
 651                            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
 652                            ON CONFLICT DO
 653                            UPDATE SET
 654                                dev_server_project_id = ?2,
 655                                left_dock_visible = ?3,
 656                                left_dock_active_panel = ?4,
 657                                left_dock_zoom = ?5,
 658                                right_dock_visible = ?6,
 659                                right_dock_active_panel = ?7,
 660                                right_dock_zoom = ?8,
 661                                bottom_dock_visible = ?9,
 662                                bottom_dock_active_panel = ?10,
 663                                bottom_dock_zoom = ?11,
 664                                timestamp = CURRENT_TIMESTAMP
 665                        ))?((
 666                            workspace.id,
 667                            dev_server_project.id.0,
 668                            workspace.docks,
 669                        ))
 670                        .context("Updating workspace")?;
 671                    }
 672                }
 673
 674                // Save center pane group
 675                Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
 676                    .context("save pane group in save workspace")?;
 677
 678                Ok(())
 679            })
 680            .log_err();
 681        })
 682        .await;
 683    }
 684
 685    query! {
 686        pub async fn next_id() -> Result<WorkspaceId> {
 687            INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
 688        }
 689    }
 690
 691    query! {
 692        fn recent_workspaces() -> Result<Vec<(WorkspaceId, LocalPaths, Option<u64>)>> {
 693            SELECT workspace_id, local_paths, dev_server_project_id
 694            FROM workspaces
 695            WHERE local_paths IS NOT NULL OR dev_server_project_id IS NOT NULL
 696            ORDER BY timestamp DESC
 697        }
 698    }
 699
 700    query! {
 701        fn dev_server_projects() -> Result<Vec<SerializedDevServerProject>> {
 702            SELECT id, path, dev_server_name
 703            FROM dev_server_projects
 704        }
 705    }
 706
 707    pub(crate) fn last_window(
 708        &self,
 709    ) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
 710        let mut prepared_query =
 711            self.select::<(Option<Uuid>, Option<SerializedWindowBounds>)>(sql!(
 712                SELECT
 713                display,
 714                window_state, window_x, window_y, window_width, window_height
 715                FROM workspaces
 716                WHERE local_paths
 717                IS NOT NULL
 718                ORDER BY timestamp DESC
 719                LIMIT 1
 720            ))?;
 721        let result = prepared_query()?;
 722        Ok(result.into_iter().next().unwrap_or_else(|| (None, None)))
 723    }
 724
 725    query! {
 726        pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
 727            DELETE FROM workspaces
 728            WHERE workspace_id IS ?
 729        }
 730    }
 731
 732    pub async fn delete_workspace_by_dev_server_project_id(
 733        &self,
 734        id: DevServerProjectId,
 735    ) -> Result<()> {
 736        self.write(move |conn| {
 737            conn.exec_bound(sql!(
 738                DELETE FROM dev_server_projects WHERE id = ?
 739            ))?(id.0)?;
 740            conn.exec_bound(sql!(
 741                DELETE FROM workspaces
 742                WHERE dev_server_project_id IS ?
 743            ))?(id.0)
 744        })
 745        .await
 746    }
 747
 748    // Returns the recent locations which are still valid on disk and deletes ones which no longer
 749    // exist.
 750    pub async fn recent_workspaces_on_disk(
 751        &self,
 752    ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation)>> {
 753        let mut result = Vec::new();
 754        let mut delete_tasks = Vec::new();
 755        let dev_server_projects = self.dev_server_projects()?;
 756
 757        for (id, location, dev_server_project_id) in self.recent_workspaces()? {
 758            if let Some(dev_server_project_id) = dev_server_project_id.map(DevServerProjectId) {
 759                if let Some(dev_server_project) = dev_server_projects
 760                    .iter()
 761                    .find(|rp| rp.id == dev_server_project_id)
 762                {
 763                    result.push((id, dev_server_project.clone().into()));
 764                } else {
 765                    delete_tasks.push(self.delete_workspace_by_id(id));
 766                }
 767                continue;
 768            }
 769
 770            if location.paths().iter().all(|path| path.exists())
 771                && location.paths().iter().any(|path| path.is_dir())
 772            {
 773                result.push((id, location.into()));
 774            } else {
 775                delete_tasks.push(self.delete_workspace_by_id(id));
 776            }
 777        }
 778
 779        futures::future::join_all(delete_tasks).await;
 780        Ok(result)
 781    }
 782
 783    pub async fn last_workspace(&self) -> Result<Option<LocalPaths>> {
 784        Ok(self
 785            .recent_workspaces_on_disk()
 786            .await?
 787            .into_iter()
 788            .filter_map(|(_, location)| match location {
 789                SerializedWorkspaceLocation::Local(local_paths, _) => Some(local_paths),
 790                SerializedWorkspaceLocation::DevServer(_) => None,
 791            })
 792            .next())
 793    }
 794
 795    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
 796        Ok(self
 797            .get_pane_group(workspace_id, None)?
 798            .into_iter()
 799            .next()
 800            .unwrap_or_else(|| {
 801                SerializedPaneGroup::Pane(SerializedPane {
 802                    active: true,
 803                    children: vec![],
 804                })
 805            }))
 806    }
 807
 808    fn get_pane_group(
 809        &self,
 810        workspace_id: WorkspaceId,
 811        group_id: Option<GroupId>,
 812    ) -> Result<Vec<SerializedPaneGroup>> {
 813        type GroupKey = (Option<GroupId>, WorkspaceId);
 814        type GroupOrPane = (
 815            Option<GroupId>,
 816            Option<SerializedAxis>,
 817            Option<PaneId>,
 818            Option<bool>,
 819            Option<String>,
 820        );
 821        self.select_bound::<GroupKey, GroupOrPane>(sql!(
 822            SELECT group_id, axis, pane_id, active, flexes
 823                FROM (SELECT
 824                        group_id,
 825                        axis,
 826                        NULL as pane_id,
 827                        NULL as active,
 828                        position,
 829                        parent_group_id,
 830                        workspace_id,
 831                        flexes
 832                      FROM pane_groups
 833                    UNION
 834                      SELECT
 835                        NULL,
 836                        NULL,
 837                        center_panes.pane_id,
 838                        panes.active as active,
 839                        position,
 840                        parent_group_id,
 841                        panes.workspace_id as workspace_id,
 842                        NULL
 843                      FROM center_panes
 844                      JOIN panes ON center_panes.pane_id = panes.pane_id)
 845                WHERE parent_group_id IS ? AND workspace_id = ?
 846                ORDER BY position
 847        ))?((group_id, workspace_id))?
 848        .into_iter()
 849        .map(|(group_id, axis, pane_id, active, flexes)| {
 850            if let Some((group_id, axis)) = group_id.zip(axis) {
 851                let flexes = flexes
 852                    .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
 853                    .transpose()?;
 854
 855                Ok(SerializedPaneGroup::Group {
 856                    axis,
 857                    children: self.get_pane_group(workspace_id, Some(group_id))?,
 858                    flexes,
 859                })
 860            } else if let Some((pane_id, active)) = pane_id.zip(active) {
 861                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
 862                    self.get_items(pane_id)?,
 863                    active,
 864                )))
 865            } else {
 866                bail!("Pane Group Child was neither a pane group or a pane");
 867            }
 868        })
 869        // Filter out panes and pane groups which don't have any children or items
 870        .filter(|pane_group| match pane_group {
 871            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
 872            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
 873            _ => true,
 874        })
 875        .collect::<Result<_>>()
 876    }
 877
 878    fn save_pane_group(
 879        conn: &Connection,
 880        workspace_id: WorkspaceId,
 881        pane_group: &SerializedPaneGroup,
 882        parent: Option<(GroupId, usize)>,
 883    ) -> Result<()> {
 884        match pane_group {
 885            SerializedPaneGroup::Group {
 886                axis,
 887                children,
 888                flexes,
 889            } => {
 890                let (parent_id, position) = parent.unzip();
 891
 892                let flex_string = flexes
 893                    .as_ref()
 894                    .map(|flexes| serde_json::json!(flexes).to_string());
 895
 896                let group_id = conn.select_row_bound::<_, i64>(sql!(
 897                    INSERT INTO pane_groups(
 898                        workspace_id,
 899                        parent_group_id,
 900                        position,
 901                        axis,
 902                        flexes
 903                    )
 904                    VALUES (?, ?, ?, ?, ?)
 905                    RETURNING group_id
 906                ))?((
 907                    workspace_id,
 908                    parent_id,
 909                    position,
 910                    *axis,
 911                    flex_string,
 912                ))?
 913                .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
 914
 915                for (position, group) in children.iter().enumerate() {
 916                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
 917                }
 918
 919                Ok(())
 920            }
 921            SerializedPaneGroup::Pane(pane) => {
 922                Self::save_pane(conn, workspace_id, pane, parent)?;
 923                Ok(())
 924            }
 925        }
 926    }
 927
 928    fn save_pane(
 929        conn: &Connection,
 930        workspace_id: WorkspaceId,
 931        pane: &SerializedPane,
 932        parent: Option<(GroupId, usize)>,
 933    ) -> Result<PaneId> {
 934        let pane_id = conn.select_row_bound::<_, i64>(sql!(
 935            INSERT INTO panes(workspace_id, active)
 936            VALUES (?, ?)
 937            RETURNING pane_id
 938        ))?((workspace_id, pane.active))?
 939        .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
 940
 941        let (parent_id, order) = parent.unzip();
 942        conn.exec_bound(sql!(
 943            INSERT INTO center_panes(pane_id, parent_group_id, position)
 944            VALUES (?, ?, ?)
 945        ))?((pane_id, parent_id, order))?;
 946
 947        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
 948
 949        Ok(pane_id)
 950    }
 951
 952    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
 953        self.select_bound(sql!(
 954            SELECT kind, item_id, active, preview FROM items
 955            WHERE pane_id = ?
 956                ORDER BY position
 957        ))?(pane_id)
 958    }
 959
 960    fn save_items(
 961        conn: &Connection,
 962        workspace_id: WorkspaceId,
 963        pane_id: PaneId,
 964        items: &[SerializedItem],
 965    ) -> Result<()> {
 966        let mut insert = conn.exec_bound(sql!(
 967            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
 968        )).context("Preparing insertion")?;
 969        for (position, item) in items.iter().enumerate() {
 970            insert((workspace_id, pane_id, position, item))?;
 971        }
 972
 973        Ok(())
 974    }
 975
 976    query! {
 977        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
 978            UPDATE workspaces
 979            SET timestamp = CURRENT_TIMESTAMP
 980            WHERE workspace_id = ?
 981        }
 982    }
 983
 984    query! {
 985        pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
 986            UPDATE workspaces
 987            SET window_state = ?2,
 988                window_x = ?3,
 989                window_y = ?4,
 990                window_width = ?5,
 991                window_height = ?6,
 992                display = ?7
 993            WHERE workspace_id = ?1
 994        }
 995    }
 996
 997    query! {
 998        pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
 999            UPDATE workspaces
1000            SET centered_layout = ?2
1001            WHERE workspace_id = ?1
1002        }
1003    }
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008    use super::*;
1009    use db::open_test_db;
1010    use gpui;
1011
1012    #[gpui::test]
1013    async fn test_next_id_stability() {
1014        env_logger::try_init().ok();
1015
1016        let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
1017
1018        db.write(|conn| {
1019            conn.migrate(
1020                "test_table",
1021                &[sql!(
1022                    CREATE TABLE test_table(
1023                        text TEXT,
1024                        workspace_id INTEGER,
1025                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1026                        ON DELETE CASCADE
1027                    ) STRICT;
1028                )],
1029            )
1030            .unwrap();
1031        })
1032        .await;
1033
1034        let id = db.next_id().await.unwrap();
1035        // Assert the empty row got inserted
1036        assert_eq!(
1037            Some(id),
1038            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
1039                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
1040            ))
1041            .unwrap()(id)
1042            .unwrap()
1043        );
1044
1045        db.write(move |conn| {
1046            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1047                .unwrap()(("test-text-1", id))
1048            .unwrap()
1049        })
1050        .await;
1051
1052        let test_text_1 = db
1053            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1054            .unwrap()(1)
1055        .unwrap()
1056        .unwrap();
1057        assert_eq!(test_text_1, "test-text-1");
1058    }
1059
1060    #[gpui::test]
1061    async fn test_workspace_id_stability() {
1062        env_logger::try_init().ok();
1063
1064        let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
1065
1066        db.write(|conn| {
1067            conn.migrate(
1068                "test_table",
1069                &[sql!(
1070                        CREATE TABLE test_table(
1071                            text TEXT,
1072                            workspace_id INTEGER,
1073                            FOREIGN KEY(workspace_id)
1074                                REFERENCES workspaces(workspace_id)
1075                            ON DELETE CASCADE
1076                        ) STRICT;)],
1077            )
1078        })
1079        .await
1080        .unwrap();
1081
1082        let mut workspace_1 = SerializedWorkspace {
1083            id: WorkspaceId(1),
1084            location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
1085            center_group: Default::default(),
1086            window_bounds: Default::default(),
1087            display: Default::default(),
1088            docks: Default::default(),
1089            centered_layout: false,
1090        };
1091
1092        let workspace_2 = SerializedWorkspace {
1093            id: WorkspaceId(2),
1094            location: LocalPaths::new(["/tmp"]).into(),
1095            center_group: Default::default(),
1096            window_bounds: Default::default(),
1097            display: Default::default(),
1098            docks: Default::default(),
1099            centered_layout: false,
1100        };
1101
1102        db.save_workspace(workspace_1.clone()).await;
1103
1104        db.write(|conn| {
1105            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1106                .unwrap()(("test-text-1", 1))
1107            .unwrap();
1108        })
1109        .await;
1110
1111        db.save_workspace(workspace_2.clone()).await;
1112
1113        db.write(|conn| {
1114            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1115                .unwrap()(("test-text-2", 2))
1116            .unwrap();
1117        })
1118        .await;
1119
1120        workspace_1.location = LocalPaths::new(["/tmp", "/tmp3"]).into();
1121        db.save_workspace(workspace_1.clone()).await;
1122        db.save_workspace(workspace_1).await;
1123        db.save_workspace(workspace_2).await;
1124
1125        let test_text_2 = db
1126            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1127            .unwrap()(2)
1128        .unwrap()
1129        .unwrap();
1130        assert_eq!(test_text_2, "test-text-2");
1131
1132        let test_text_1 = db
1133            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1134            .unwrap()(1)
1135        .unwrap()
1136        .unwrap();
1137        assert_eq!(test_text_1, "test-text-1");
1138    }
1139
1140    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
1141        SerializedPaneGroup::Group {
1142            axis: SerializedAxis(axis),
1143            flexes: None,
1144            children,
1145        }
1146    }
1147
1148    #[gpui::test]
1149    async fn test_full_workspace_serialization() {
1150        env_logger::try_init().ok();
1151
1152        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
1153
1154        //  -----------------
1155        //  | 1,2   | 5,6   |
1156        //  | - - - |       |
1157        //  | 3,4   |       |
1158        //  -----------------
1159        let center_group = group(
1160            Axis::Horizontal,
1161            vec![
1162                group(
1163                    Axis::Vertical,
1164                    vec![
1165                        SerializedPaneGroup::Pane(SerializedPane::new(
1166                            vec![
1167                                SerializedItem::new("Terminal", 5, false, false),
1168                                SerializedItem::new("Terminal", 6, true, false),
1169                            ],
1170                            false,
1171                        )),
1172                        SerializedPaneGroup::Pane(SerializedPane::new(
1173                            vec![
1174                                SerializedItem::new("Terminal", 7, true, false),
1175                                SerializedItem::new("Terminal", 8, false, false),
1176                            ],
1177                            false,
1178                        )),
1179                    ],
1180                ),
1181                SerializedPaneGroup::Pane(SerializedPane::new(
1182                    vec![
1183                        SerializedItem::new("Terminal", 9, false, false),
1184                        SerializedItem::new("Terminal", 10, true, false),
1185                    ],
1186                    false,
1187                )),
1188            ],
1189        );
1190
1191        let workspace = SerializedWorkspace {
1192            id: WorkspaceId(5),
1193            location: SerializedWorkspaceLocation::Local(
1194                LocalPaths::new(["/tmp", "/tmp2"]),
1195                LocalPathsOrder::new([1, 0]),
1196            ),
1197            center_group,
1198            window_bounds: Default::default(),
1199            display: Default::default(),
1200            docks: Default::default(),
1201            centered_layout: false,
1202        };
1203
1204        db.save_workspace(workspace.clone()).await;
1205
1206        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
1207        assert_eq!(workspace, round_trip_workspace.unwrap());
1208
1209        // Test guaranteed duplicate IDs
1210        db.save_workspace(workspace.clone()).await;
1211        db.save_workspace(workspace.clone()).await;
1212
1213        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
1214        assert_eq!(workspace, round_trip_workspace.unwrap());
1215    }
1216
1217    #[gpui::test]
1218    async fn test_workspace_assignment() {
1219        env_logger::try_init().ok();
1220
1221        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
1222
1223        let workspace_1 = SerializedWorkspace {
1224            id: WorkspaceId(1),
1225            location: SerializedWorkspaceLocation::Local(
1226                LocalPaths::new(["/tmp", "/tmp2"]),
1227                LocalPathsOrder::new([0, 1]),
1228            ),
1229            center_group: Default::default(),
1230            window_bounds: Default::default(),
1231            display: Default::default(),
1232            docks: Default::default(),
1233            centered_layout: false,
1234        };
1235
1236        let mut workspace_2 = SerializedWorkspace {
1237            id: WorkspaceId(2),
1238            location: LocalPaths::new(["/tmp"]).into(),
1239            center_group: Default::default(),
1240            window_bounds: Default::default(),
1241            display: Default::default(),
1242            docks: Default::default(),
1243            centered_layout: false,
1244        };
1245
1246        db.save_workspace(workspace_1.clone()).await;
1247        db.save_workspace(workspace_2.clone()).await;
1248
1249        // Test that paths are treated as a set
1250        assert_eq!(
1251            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1252            workspace_1
1253        );
1254        assert_eq!(
1255            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
1256            workspace_1
1257        );
1258
1259        // Make sure that other keys work
1260        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
1261        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
1262
1263        // Test 'mutate' case of updating a pre-existing id
1264        workspace_2.location = LocalPaths::new(["/tmp", "/tmp2"]).into();
1265
1266        db.save_workspace(workspace_2.clone()).await;
1267        assert_eq!(
1268            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1269            workspace_2
1270        );
1271
1272        // Test other mechanism for mutating
1273        let mut workspace_3 = SerializedWorkspace {
1274            id: WorkspaceId(3),
1275            location: SerializedWorkspaceLocation::Local(
1276                LocalPaths::new(&["/tmp", "/tmp2"]),
1277                LocalPathsOrder::new([1, 0]),
1278            ),
1279            center_group: Default::default(),
1280            window_bounds: Default::default(),
1281            display: Default::default(),
1282            docks: Default::default(),
1283            centered_layout: false,
1284        };
1285
1286        db.save_workspace(workspace_3.clone()).await;
1287        assert_eq!(
1288            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1289            workspace_3
1290        );
1291
1292        // Make sure that updating paths differently also works
1293        workspace_3.location = LocalPaths::new(["/tmp3", "/tmp4", "/tmp2"]).into();
1294        db.save_workspace(workspace_3.clone()).await;
1295        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
1296        assert_eq!(
1297            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
1298                .unwrap(),
1299            workspace_3
1300        );
1301    }
1302
1303    use crate::persistence::model::SerializedWorkspace;
1304    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
1305
1306    fn default_workspace<P: AsRef<Path>>(
1307        workspace_id: &[P],
1308        center_group: &SerializedPaneGroup,
1309    ) -> SerializedWorkspace {
1310        SerializedWorkspace {
1311            id: WorkspaceId(4),
1312            location: LocalPaths::new(workspace_id).into(),
1313            center_group: center_group.clone(),
1314            window_bounds: Default::default(),
1315            display: Default::default(),
1316            docks: Default::default(),
1317            centered_layout: false,
1318        }
1319    }
1320
1321    #[gpui::test]
1322    async fn test_simple_split() {
1323        env_logger::try_init().ok();
1324
1325        let db = WorkspaceDb(open_test_db("simple_split").await);
1326
1327        //  -----------------
1328        //  | 1,2   | 5,6   |
1329        //  | - - - |       |
1330        //  | 3,4   |       |
1331        //  -----------------
1332        let center_pane = group(
1333            Axis::Horizontal,
1334            vec![
1335                group(
1336                    Axis::Vertical,
1337                    vec![
1338                        SerializedPaneGroup::Pane(SerializedPane::new(
1339                            vec![
1340                                SerializedItem::new("Terminal", 1, false, false),
1341                                SerializedItem::new("Terminal", 2, true, false),
1342                            ],
1343                            false,
1344                        )),
1345                        SerializedPaneGroup::Pane(SerializedPane::new(
1346                            vec![
1347                                SerializedItem::new("Terminal", 4, false, false),
1348                                SerializedItem::new("Terminal", 3, true, false),
1349                            ],
1350                            true,
1351                        )),
1352                    ],
1353                ),
1354                SerializedPaneGroup::Pane(SerializedPane::new(
1355                    vec![
1356                        SerializedItem::new("Terminal", 5, true, false),
1357                        SerializedItem::new("Terminal", 6, false, false),
1358                    ],
1359                    false,
1360                )),
1361            ],
1362        );
1363
1364        let workspace = default_workspace(&["/tmp"], &center_pane);
1365
1366        db.save_workspace(workspace.clone()).await;
1367
1368        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
1369
1370        assert_eq!(workspace.center_group, new_workspace.center_group);
1371    }
1372
1373    #[gpui::test]
1374    async fn test_cleanup_panes() {
1375        env_logger::try_init().ok();
1376
1377        let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
1378
1379        let center_pane = group(
1380            Axis::Horizontal,
1381            vec![
1382                group(
1383                    Axis::Vertical,
1384                    vec![
1385                        SerializedPaneGroup::Pane(SerializedPane::new(
1386                            vec![
1387                                SerializedItem::new("Terminal", 1, false, false),
1388                                SerializedItem::new("Terminal", 2, true, false),
1389                            ],
1390                            false,
1391                        )),
1392                        SerializedPaneGroup::Pane(SerializedPane::new(
1393                            vec![
1394                                SerializedItem::new("Terminal", 4, false, false),
1395                                SerializedItem::new("Terminal", 3, true, false),
1396                            ],
1397                            true,
1398                        )),
1399                    ],
1400                ),
1401                SerializedPaneGroup::Pane(SerializedPane::new(
1402                    vec![
1403                        SerializedItem::new("Terminal", 5, false, false),
1404                        SerializedItem::new("Terminal", 6, true, false),
1405                    ],
1406                    false,
1407                )),
1408            ],
1409        );
1410
1411        let id = &["/tmp"];
1412
1413        let mut workspace = default_workspace(id, &center_pane);
1414
1415        db.save_workspace(workspace.clone()).await;
1416
1417        workspace.center_group = group(
1418            Axis::Vertical,
1419            vec![
1420                SerializedPaneGroup::Pane(SerializedPane::new(
1421                    vec![
1422                        SerializedItem::new("Terminal", 1, false, false),
1423                        SerializedItem::new("Terminal", 2, true, false),
1424                    ],
1425                    false,
1426                )),
1427                SerializedPaneGroup::Pane(SerializedPane::new(
1428                    vec![
1429                        SerializedItem::new("Terminal", 4, true, false),
1430                        SerializedItem::new("Terminal", 3, false, false),
1431                    ],
1432                    true,
1433                )),
1434            ],
1435        );
1436
1437        db.save_workspace(workspace.clone()).await;
1438
1439        let new_workspace = db.workspace_for_roots(id).unwrap();
1440
1441        assert_eq!(workspace.center_group, new_workspace.center_group);
1442    }
1443}