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