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