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