persistence.rs

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