persistence.rs

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