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