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
 295impl WorkspaceDb {
 296    /// Returns a serialized workspace for the given worktree_roots. If the passed array
 297    /// is empty, the most recent workspace is returned instead. If no workspace for the
 298    /// passed roots is stored, returns none.
 299    pub(crate) fn workspace_for_roots<P: AsRef<Path>>(
 300        &self,
 301        worktree_roots: &[P],
 302    ) -> Option<SerializedWorkspace> {
 303        let workspace_location: WorkspaceLocation = worktree_roots.into();
 304
 305        // Note that we re-assign the workspace_id here in case it's empty
 306        // and we've grabbed the most recent workspace
 307        let (workspace_id, workspace_location, bounds, display, fullscreen, centered_layout, docks): (
 308            WorkspaceId,
 309            WorkspaceLocation,
 310            Option<SerializedWindowsBounds>,
 311            Option<Uuid>,
 312            Option<bool>,
 313            Option<bool>,
 314            DockStructure,
 315        ) = self
 316            .select_row_bound(sql! {
 317                SELECT
 318                    workspace_id,
 319                    workspace_location,
 320                    window_state,
 321                    window_x,
 322                    window_y,
 323                    window_width,
 324                    window_height,
 325                    display,
 326                    fullscreen,
 327                    centered_layout,
 328                    left_dock_visible,
 329                    left_dock_active_panel,
 330                    left_dock_zoom,
 331                    right_dock_visible,
 332                    right_dock_active_panel,
 333                    right_dock_zoom,
 334                    bottom_dock_visible,
 335                    bottom_dock_active_panel,
 336                    bottom_dock_zoom
 337                FROM workspaces
 338                WHERE workspace_location = ?
 339            })
 340            .and_then(|mut prepared_statement| (prepared_statement)(&workspace_location))
 341            .context("No workspaces found")
 342            .warn_on_err()
 343            .flatten()?;
 344
 345        Some(SerializedWorkspace {
 346            id: workspace_id,
 347            location: workspace_location.clone(),
 348            center_group: self
 349                .get_center_pane_group(workspace_id)
 350                .context("Getting center group")
 351                .log_err()?,
 352            bounds: bounds.map(|bounds| bounds.0),
 353            fullscreen: fullscreen.unwrap_or(false),
 354            centered_layout: centered_layout.unwrap_or(false),
 355            display,
 356            docks,
 357        })
 358    }
 359
 360    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
 361    /// that used this workspace previously
 362    pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
 363        self.write(move |conn| {
 364            conn.with_savepoint("update_worktrees", || {
 365                // Clear out panes and pane_groups
 366                conn.exec_bound(sql!(
 367                    DELETE FROM pane_groups WHERE workspace_id = ?1;
 368                    DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
 369                .context("Clearing old panes")?;
 370
 371                conn.exec_bound(sql!(
 372                    DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?
 373                ))?((&workspace.location, workspace.id))
 374                .context("clearing out old locations")?;
 375
 376                // Upsert
 377                conn.exec_bound(sql!(
 378                    INSERT INTO workspaces(
 379                        workspace_id,
 380                        workspace_location,
 381                        left_dock_visible,
 382                        left_dock_active_panel,
 383                        left_dock_zoom,
 384                        right_dock_visible,
 385                        right_dock_active_panel,
 386                        right_dock_zoom,
 387                        bottom_dock_visible,
 388                        bottom_dock_active_panel,
 389                        bottom_dock_zoom,
 390                        timestamp
 391                    )
 392                    VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
 393                    ON CONFLICT DO
 394                    UPDATE SET
 395                        workspace_location = ?2,
 396                        left_dock_visible = ?3,
 397                        left_dock_active_panel = ?4,
 398                        left_dock_zoom = ?5,
 399                        right_dock_visible = ?6,
 400                        right_dock_active_panel = ?7,
 401                        right_dock_zoom = ?8,
 402                        bottom_dock_visible = ?9,
 403                        bottom_dock_active_panel = ?10,
 404                        bottom_dock_zoom = ?11,
 405                        timestamp = CURRENT_TIMESTAMP
 406                ))?((workspace.id, &workspace.location, workspace.docks))
 407                .context("Updating workspace")?;
 408
 409                // Save center pane group
 410                Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
 411                    .context("save pane group in save workspace")?;
 412
 413                Ok(())
 414            })
 415            .log_err();
 416        })
 417        .await;
 418    }
 419
 420    query! {
 421        pub async fn next_id() -> Result<WorkspaceId> {
 422            INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
 423        }
 424    }
 425
 426    query! {
 427        fn recent_workspaces() -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
 428            SELECT workspace_id, workspace_location
 429            FROM workspaces
 430            WHERE workspace_location IS NOT NULL
 431            ORDER BY timestamp DESC
 432        }
 433    }
 434
 435    query! {
 436        pub fn last_window() -> Result<(Option<Uuid>, Option<SerializedWindowsBounds>, Option<bool>)> {
 437            SELECT display, window_state, window_x, window_y, window_width, window_height, fullscreen
 438            FROM workspaces
 439            WHERE workspace_location IS NOT NULL
 440            ORDER BY timestamp DESC
 441            LIMIT 1
 442        }
 443    }
 444
 445    query! {
 446        pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
 447            DELETE FROM workspaces
 448            WHERE workspace_id IS ?
 449        }
 450    }
 451
 452    // Returns the recent locations which are still valid on disk and deletes ones which no longer
 453    // exist.
 454    pub async fn recent_workspaces_on_disk(&self) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
 455        let mut result = Vec::new();
 456        let mut delete_tasks = Vec::new();
 457        for (id, location) in self.recent_workspaces()? {
 458            if location.paths().iter().all(|path| path.exists())
 459                && location.paths().iter().any(|path| path.is_dir())
 460            {
 461                result.push((id, location));
 462            } else {
 463                delete_tasks.push(self.delete_workspace_by_id(id));
 464            }
 465        }
 466
 467        futures::future::join_all(delete_tasks).await;
 468        Ok(result)
 469    }
 470
 471    pub async fn last_workspace(&self) -> Result<Option<WorkspaceLocation>> {
 472        Ok(self
 473            .recent_workspaces_on_disk()
 474            .await?
 475            .into_iter()
 476            .next()
 477            .map(|(_, location)| location))
 478    }
 479
 480    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
 481        Ok(self
 482            .get_pane_group(workspace_id, None)?
 483            .into_iter()
 484            .next()
 485            .unwrap_or_else(|| {
 486                SerializedPaneGroup::Pane(SerializedPane {
 487                    active: true,
 488                    children: vec![],
 489                })
 490            }))
 491    }
 492
 493    fn get_pane_group(
 494        &self,
 495        workspace_id: WorkspaceId,
 496        group_id: Option<GroupId>,
 497    ) -> Result<Vec<SerializedPaneGroup>> {
 498        type GroupKey = (Option<GroupId>, WorkspaceId);
 499        type GroupOrPane = (
 500            Option<GroupId>,
 501            Option<SerializedAxis>,
 502            Option<PaneId>,
 503            Option<bool>,
 504            Option<String>,
 505        );
 506        self.select_bound::<GroupKey, GroupOrPane>(sql!(
 507            SELECT group_id, axis, pane_id, active, flexes
 508                FROM (SELECT
 509                        group_id,
 510                        axis,
 511                        NULL as pane_id,
 512                        NULL as active,
 513                        position,
 514                        parent_group_id,
 515                        workspace_id,
 516                        flexes
 517                      FROM pane_groups
 518                    UNION
 519                      SELECT
 520                        NULL,
 521                        NULL,
 522                        center_panes.pane_id,
 523                        panes.active as active,
 524                        position,
 525                        parent_group_id,
 526                        panes.workspace_id as workspace_id,
 527                        NULL
 528                      FROM center_panes
 529                      JOIN panes ON center_panes.pane_id = panes.pane_id)
 530                WHERE parent_group_id IS ? AND workspace_id = ?
 531                ORDER BY position
 532        ))?((group_id, workspace_id))?
 533        .into_iter()
 534        .map(|(group_id, axis, pane_id, active, flexes)| {
 535            if let Some((group_id, axis)) = group_id.zip(axis) {
 536                let flexes = flexes
 537                    .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
 538                    .transpose()?;
 539
 540                Ok(SerializedPaneGroup::Group {
 541                    axis,
 542                    children: self.get_pane_group(workspace_id, Some(group_id))?,
 543                    flexes,
 544                })
 545            } else if let Some((pane_id, active)) = pane_id.zip(active) {
 546                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
 547                    self.get_items(pane_id)?,
 548                    active,
 549                )))
 550            } else {
 551                bail!("Pane Group Child was neither a pane group or a pane");
 552            }
 553        })
 554        // Filter out panes and pane groups which don't have any children or items
 555        .filter(|pane_group| match pane_group {
 556            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
 557            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
 558            _ => true,
 559        })
 560        .collect::<Result<_>>()
 561    }
 562
 563    fn save_pane_group(
 564        conn: &Connection,
 565        workspace_id: WorkspaceId,
 566        pane_group: &SerializedPaneGroup,
 567        parent: Option<(GroupId, usize)>,
 568    ) -> Result<()> {
 569        match pane_group {
 570            SerializedPaneGroup::Group {
 571                axis,
 572                children,
 573                flexes,
 574            } => {
 575                let (parent_id, position) = unzip_option(parent);
 576
 577                let flex_string = flexes
 578                    .as_ref()
 579                    .map(|flexes| serde_json::json!(flexes).to_string());
 580
 581                let group_id = conn.select_row_bound::<_, i64>(sql!(
 582                    INSERT INTO pane_groups(
 583                        workspace_id,
 584                        parent_group_id,
 585                        position,
 586                        axis,
 587                        flexes
 588                    )
 589                    VALUES (?, ?, ?, ?, ?)
 590                    RETURNING group_id
 591                ))?((
 592                    workspace_id,
 593                    parent_id,
 594                    position,
 595                    *axis,
 596                    flex_string,
 597                ))?
 598                .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
 599
 600                for (position, group) in children.iter().enumerate() {
 601                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
 602                }
 603
 604                Ok(())
 605            }
 606            SerializedPaneGroup::Pane(pane) => {
 607                Self::save_pane(conn, workspace_id, pane, parent)?;
 608                Ok(())
 609            }
 610        }
 611    }
 612
 613    fn save_pane(
 614        conn: &Connection,
 615        workspace_id: WorkspaceId,
 616        pane: &SerializedPane,
 617        parent: Option<(GroupId, usize)>,
 618    ) -> Result<PaneId> {
 619        let pane_id = conn.select_row_bound::<_, i64>(sql!(
 620            INSERT INTO panes(workspace_id, active)
 621            VALUES (?, ?)
 622            RETURNING pane_id
 623        ))?((workspace_id, pane.active))?
 624        .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
 625
 626        let (parent_id, order) = unzip_option(parent);
 627        conn.exec_bound(sql!(
 628            INSERT INTO center_panes(pane_id, parent_group_id, position)
 629            VALUES (?, ?, ?)
 630        ))?((pane_id, parent_id, order))?;
 631
 632        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
 633
 634        Ok(pane_id)
 635    }
 636
 637    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
 638        self.select_bound(sql!(
 639            SELECT kind, item_id, active, preview FROM items
 640            WHERE pane_id = ?
 641                ORDER BY position
 642        ))?(pane_id)
 643    }
 644
 645    fn save_items(
 646        conn: &Connection,
 647        workspace_id: WorkspaceId,
 648        pane_id: PaneId,
 649        items: &[SerializedItem],
 650    ) -> Result<()> {
 651        let mut insert = conn.exec_bound(sql!(
 652            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
 653        )).context("Preparing insertion")?;
 654        for (position, item) in items.iter().enumerate() {
 655            insert((workspace_id, pane_id, position, item))?;
 656        }
 657
 658        Ok(())
 659    }
 660
 661    query! {
 662        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
 663            UPDATE workspaces
 664            SET timestamp = CURRENT_TIMESTAMP
 665            WHERE workspace_id = ?
 666        }
 667    }
 668
 669    query! {
 670        pub(crate) async fn set_window_bounds(workspace_id: WorkspaceId, bounds: SerializedWindowsBounds, display: Uuid) -> Result<()> {
 671            UPDATE workspaces
 672            SET window_state = ?2,
 673                window_x = ?3,
 674                window_y = ?4,
 675                window_width = ?5,
 676                window_height = ?6,
 677                display = ?7
 678            WHERE workspace_id = ?1
 679        }
 680    }
 681
 682    query! {
 683        pub(crate) async fn set_fullscreen(workspace_id: WorkspaceId, fullscreen: bool) -> Result<()> {
 684            UPDATE workspaces
 685            SET fullscreen = ?2
 686            WHERE workspace_id = ?1
 687        }
 688    }
 689
 690    query! {
 691        pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
 692            UPDATE workspaces
 693            SET centered_layout = ?2
 694            WHERE workspace_id = ?1
 695        }
 696    }
 697}
 698
 699#[cfg(test)]
 700mod tests {
 701    use super::*;
 702    use db::open_test_db;
 703    use gpui;
 704
 705    #[gpui::test]
 706    async fn test_next_id_stability() {
 707        env_logger::try_init().ok();
 708
 709        let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
 710
 711        db.write(|conn| {
 712            conn.migrate(
 713                "test_table",
 714                &[sql!(
 715                    CREATE TABLE test_table(
 716                        text TEXT,
 717                        workspace_id INTEGER,
 718                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 719                        ON DELETE CASCADE
 720                    ) STRICT;
 721                )],
 722            )
 723            .unwrap();
 724        })
 725        .await;
 726
 727        let id = db.next_id().await.unwrap();
 728        // Assert the empty row got inserted
 729        assert_eq!(
 730            Some(id),
 731            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
 732                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
 733            ))
 734            .unwrap()(id)
 735            .unwrap()
 736        );
 737
 738        db.write(move |conn| {
 739            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
 740                .unwrap()(("test-text-1", id))
 741            .unwrap()
 742        })
 743        .await;
 744
 745        let test_text_1 = db
 746            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
 747            .unwrap()(1)
 748        .unwrap()
 749        .unwrap();
 750        assert_eq!(test_text_1, "test-text-1");
 751    }
 752
 753    #[gpui::test]
 754    async fn test_workspace_id_stability() {
 755        env_logger::try_init().ok();
 756
 757        let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
 758
 759        db.write(|conn| {
 760            conn.migrate(
 761                "test_table",
 762                &[sql!(
 763                        CREATE TABLE test_table(
 764                            text TEXT,
 765                            workspace_id INTEGER,
 766                            FOREIGN KEY(workspace_id)
 767                                REFERENCES workspaces(workspace_id)
 768                            ON DELETE CASCADE
 769                        ) STRICT;)],
 770            )
 771        })
 772        .await
 773        .unwrap();
 774
 775        let mut workspace_1 = SerializedWorkspace {
 776            id: WorkspaceId(1),
 777            location: (["/tmp", "/tmp2"]).into(),
 778            center_group: Default::default(),
 779            bounds: Default::default(),
 780            display: Default::default(),
 781            docks: Default::default(),
 782            fullscreen: false,
 783            centered_layout: false,
 784        };
 785
 786        let workspace_2 = SerializedWorkspace {
 787            id: WorkspaceId(2),
 788            location: (["/tmp"]).into(),
 789            center_group: Default::default(),
 790            bounds: Default::default(),
 791            display: Default::default(),
 792            docks: Default::default(),
 793            fullscreen: false,
 794            centered_layout: false,
 795        };
 796
 797        db.save_workspace(workspace_1.clone()).await;
 798
 799        db.write(|conn| {
 800            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
 801                .unwrap()(("test-text-1", 1))
 802            .unwrap();
 803        })
 804        .await;
 805
 806        db.save_workspace(workspace_2.clone()).await;
 807
 808        db.write(|conn| {
 809            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
 810                .unwrap()(("test-text-2", 2))
 811            .unwrap();
 812        })
 813        .await;
 814
 815        workspace_1.location = (["/tmp", "/tmp3"]).into();
 816        db.save_workspace(workspace_1.clone()).await;
 817        db.save_workspace(workspace_1).await;
 818        db.save_workspace(workspace_2).await;
 819
 820        let test_text_2 = db
 821            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
 822            .unwrap()(2)
 823        .unwrap()
 824        .unwrap();
 825        assert_eq!(test_text_2, "test-text-2");
 826
 827        let test_text_1 = db
 828            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
 829            .unwrap()(1)
 830        .unwrap()
 831        .unwrap();
 832        assert_eq!(test_text_1, "test-text-1");
 833    }
 834
 835    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
 836        SerializedPaneGroup::Group {
 837            axis: SerializedAxis(axis),
 838            flexes: None,
 839            children,
 840        }
 841    }
 842
 843    #[gpui::test]
 844    async fn test_full_workspace_serialization() {
 845        env_logger::try_init().ok();
 846
 847        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
 848
 849        //  -----------------
 850        //  | 1,2   | 5,6   |
 851        //  | - - - |       |
 852        //  | 3,4   |       |
 853        //  -----------------
 854        let center_group = group(
 855            Axis::Horizontal,
 856            vec![
 857                group(
 858                    Axis::Vertical,
 859                    vec![
 860                        SerializedPaneGroup::Pane(SerializedPane::new(
 861                            vec![
 862                                SerializedItem::new("Terminal", 5, false, false),
 863                                SerializedItem::new("Terminal", 6, true, false),
 864                            ],
 865                            false,
 866                        )),
 867                        SerializedPaneGroup::Pane(SerializedPane::new(
 868                            vec![
 869                                SerializedItem::new("Terminal", 7, true, false),
 870                                SerializedItem::new("Terminal", 8, false, false),
 871                            ],
 872                            false,
 873                        )),
 874                    ],
 875                ),
 876                SerializedPaneGroup::Pane(SerializedPane::new(
 877                    vec![
 878                        SerializedItem::new("Terminal", 9, false, false),
 879                        SerializedItem::new("Terminal", 10, true, false),
 880                    ],
 881                    false,
 882                )),
 883            ],
 884        );
 885
 886        let workspace = SerializedWorkspace {
 887            id: WorkspaceId(5),
 888            location: (["/tmp", "/tmp2"]).into(),
 889            center_group,
 890            bounds: Default::default(),
 891            display: Default::default(),
 892            docks: Default::default(),
 893            fullscreen: false,
 894            centered_layout: false,
 895        };
 896
 897        db.save_workspace(workspace.clone()).await;
 898        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
 899
 900        assert_eq!(workspace, round_trip_workspace.unwrap());
 901
 902        // Test guaranteed duplicate IDs
 903        db.save_workspace(workspace.clone()).await;
 904        db.save_workspace(workspace.clone()).await;
 905
 906        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
 907        assert_eq!(workspace, round_trip_workspace.unwrap());
 908    }
 909
 910    #[gpui::test]
 911    async fn test_workspace_assignment() {
 912        env_logger::try_init().ok();
 913
 914        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
 915
 916        let workspace_1 = SerializedWorkspace {
 917            id: WorkspaceId(1),
 918            location: (["/tmp", "/tmp2"]).into(),
 919            center_group: Default::default(),
 920            bounds: Default::default(),
 921            display: Default::default(),
 922            docks: Default::default(),
 923            fullscreen: false,
 924            centered_layout: false,
 925        };
 926
 927        let mut workspace_2 = SerializedWorkspace {
 928            id: WorkspaceId(2),
 929            location: (["/tmp"]).into(),
 930            center_group: Default::default(),
 931            bounds: Default::default(),
 932            display: Default::default(),
 933            docks: Default::default(),
 934            fullscreen: false,
 935            centered_layout: false,
 936        };
 937
 938        db.save_workspace(workspace_1.clone()).await;
 939        db.save_workspace(workspace_2.clone()).await;
 940
 941        // Test that paths are treated as a set
 942        assert_eq!(
 943            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
 944            workspace_1
 945        );
 946        assert_eq!(
 947            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
 948            workspace_1
 949        );
 950
 951        // Make sure that other keys work
 952        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
 953        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
 954
 955        // Test 'mutate' case of updating a pre-existing id
 956        workspace_2.location = (["/tmp", "/tmp2"]).into();
 957
 958        db.save_workspace(workspace_2.clone()).await;
 959        assert_eq!(
 960            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
 961            workspace_2
 962        );
 963
 964        // Test other mechanism for mutating
 965        let mut workspace_3 = SerializedWorkspace {
 966            id: WorkspaceId(3),
 967            location: (&["/tmp", "/tmp2"]).into(),
 968            center_group: Default::default(),
 969            bounds: Default::default(),
 970            display: Default::default(),
 971            docks: Default::default(),
 972            fullscreen: false,
 973            centered_layout: false,
 974        };
 975
 976        db.save_workspace(workspace_3.clone()).await;
 977        assert_eq!(
 978            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
 979            workspace_3
 980        );
 981
 982        // Make sure that updating paths differently also works
 983        workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
 984        db.save_workspace(workspace_3.clone()).await;
 985        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
 986        assert_eq!(
 987            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
 988                .unwrap(),
 989            workspace_3
 990        );
 991    }
 992
 993    use crate::persistence::model::SerializedWorkspace;
 994    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
 995
 996    fn default_workspace<P: AsRef<Path>>(
 997        workspace_id: &[P],
 998        center_group: &SerializedPaneGroup,
 999    ) -> SerializedWorkspace {
1000        SerializedWorkspace {
1001            id: WorkspaceId(4),
1002            location: workspace_id.into(),
1003            center_group: center_group.clone(),
1004            bounds: Default::default(),
1005            display: Default::default(),
1006            docks: Default::default(),
1007            fullscreen: false,
1008            centered_layout: false,
1009        }
1010    }
1011
1012    #[gpui::test]
1013    async fn test_simple_split() {
1014        env_logger::try_init().ok();
1015
1016        let db = WorkspaceDb(open_test_db("simple_split").await);
1017
1018        //  -----------------
1019        //  | 1,2   | 5,6   |
1020        //  | - - - |       |
1021        //  | 3,4   |       |
1022        //  -----------------
1023        let center_pane = group(
1024            Axis::Horizontal,
1025            vec![
1026                group(
1027                    Axis::Vertical,
1028                    vec![
1029                        SerializedPaneGroup::Pane(SerializedPane::new(
1030                            vec![
1031                                SerializedItem::new("Terminal", 1, false, false),
1032                                SerializedItem::new("Terminal", 2, true, false),
1033                            ],
1034                            false,
1035                        )),
1036                        SerializedPaneGroup::Pane(SerializedPane::new(
1037                            vec![
1038                                SerializedItem::new("Terminal", 4, false, false),
1039                                SerializedItem::new("Terminal", 3, true, false),
1040                            ],
1041                            true,
1042                        )),
1043                    ],
1044                ),
1045                SerializedPaneGroup::Pane(SerializedPane::new(
1046                    vec![
1047                        SerializedItem::new("Terminal", 5, true, false),
1048                        SerializedItem::new("Terminal", 6, false, false),
1049                    ],
1050                    false,
1051                )),
1052            ],
1053        );
1054
1055        let workspace = default_workspace(&["/tmp"], &center_pane);
1056
1057        db.save_workspace(workspace.clone()).await;
1058
1059        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
1060
1061        assert_eq!(workspace.center_group, new_workspace.center_group);
1062    }
1063
1064    #[gpui::test]
1065    async fn test_cleanup_panes() {
1066        env_logger::try_init().ok();
1067
1068        let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
1069
1070        let center_pane = group(
1071            Axis::Horizontal,
1072            vec![
1073                group(
1074                    Axis::Vertical,
1075                    vec![
1076                        SerializedPaneGroup::Pane(SerializedPane::new(
1077                            vec![
1078                                SerializedItem::new("Terminal", 1, false, false),
1079                                SerializedItem::new("Terminal", 2, true, false),
1080                            ],
1081                            false,
1082                        )),
1083                        SerializedPaneGroup::Pane(SerializedPane::new(
1084                            vec![
1085                                SerializedItem::new("Terminal", 4, false, false),
1086                                SerializedItem::new("Terminal", 3, true, false),
1087                            ],
1088                            true,
1089                        )),
1090                    ],
1091                ),
1092                SerializedPaneGroup::Pane(SerializedPane::new(
1093                    vec![
1094                        SerializedItem::new("Terminal", 5, false, false),
1095                        SerializedItem::new("Terminal", 6, true, false),
1096                    ],
1097                    false,
1098                )),
1099            ],
1100        );
1101
1102        let id = &["/tmp"];
1103
1104        let mut workspace = default_workspace(id, &center_pane);
1105
1106        db.save_workspace(workspace.clone()).await;
1107
1108        workspace.center_group = group(
1109            Axis::Vertical,
1110            vec![
1111                SerializedPaneGroup::Pane(SerializedPane::new(
1112                    vec![
1113                        SerializedItem::new("Terminal", 1, false, false),
1114                        SerializedItem::new("Terminal", 2, true, false),
1115                    ],
1116                    false,
1117                )),
1118                SerializedPaneGroup::Pane(SerializedPane::new(
1119                    vec![
1120                        SerializedItem::new("Terminal", 4, true, false),
1121                        SerializedItem::new("Terminal", 3, false, false),
1122                    ],
1123                    true,
1124                )),
1125            ],
1126        );
1127
1128        db.save_workspace(workspace.clone()).await;
1129
1130        let new_workspace = db.workspace_for_roots(id).unwrap();
1131
1132        assert_eq!(workspace.center_group, new_workspace.center_group);
1133    }
1134}