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