persistence.rs

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