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, WindowBounds};
   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) WindowBounds);
  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 (region, next_index) = match self.0 {
  73            WindowBounds::Fullscreen => {
  74                let next_index = statement.bind(&"Fullscreen", start_index)?;
  75                (None, next_index)
  76            }
  77            WindowBounds::Maximized => {
  78                let next_index = statement.bind(&"Maximized", start_index)?;
  79                (None, next_index)
  80            }
  81            WindowBounds::Fixed(region) => {
  82                let next_index = statement.bind(&"Fixed", start_index)?;
  83                (Some(region), next_index)
  84            }
  85        };
  86
  87        statement.bind(
  88            &region.map(|region| {
  89                (
  90                    SerializedGlobalPixels(region.origin.x),
  91                    SerializedGlobalPixels(region.origin.y),
  92                    SerializedGlobalPixels(region.size.width),
  93                    SerializedGlobalPixels(region.size.height),
  94                )
  95            }),
  96            next_index,
  97        )
  98    }
  99}
 100
 101impl Column for SerializedWindowsBounds {
 102    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 103        let (window_state, next_index) = String::column(statement, start_index)?;
 104        let bounds = match window_state.as_str() {
 105            "Fullscreen" => SerializedWindowsBounds(WindowBounds::Fullscreen),
 106            "Maximized" => SerializedWindowsBounds(WindowBounds::Maximized),
 107            "Fixed" => {
 108                let ((x, y, width, height), _) = Column::column(statement, next_index)?;
 109                let x: f64 = x;
 110                let y: f64 = y;
 111                let width: f64 = width;
 112                let height: f64 = height;
 113                SerializedWindowsBounds(WindowBounds::Fixed(Bounds {
 114                    origin: point(x.into(), y.into()),
 115                    size: size(width.into(), height.into()),
 116                }))
 117            }
 118            _ => bail!("Window State did not have a valid string"),
 119        };
 120
 121        Ok((bounds, next_index + 4))
 122    }
 123}
 124
 125#[derive(Clone, Debug, PartialEq)]
 126struct SerializedGlobalPixels(gpui::GlobalPixels);
 127impl sqlez::bindable::StaticColumnCount for SerializedGlobalPixels {}
 128
 129impl sqlez::bindable::Bind for SerializedGlobalPixels {
 130    fn bind(
 131        &self,
 132        statement: &sqlez::statement::Statement,
 133        start_index: i32,
 134    ) -> anyhow::Result<i32> {
 135        let this: f64 = self.0.into();
 136        let this: f32 = this as _;
 137        this.bind(statement, start_index)
 138    }
 139}
 140
 141define_connection! {
 142    // Current schema shape using pseudo-rust syntax:
 143    //
 144    // workspaces(
 145    //   workspace_id: usize, // Primary key for workspaces
 146    //   workspace_location: Bincode<Vec<PathBuf>>,
 147    //   dock_visible: bool, // Deprecated
 148    //   dock_anchor: DockAnchor, // Deprecated
 149    //   dock_pane: Option<usize>, // Deprecated
 150    //   left_sidebar_open: boolean,
 151    //   timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
 152    //   window_state: String, // WindowBounds Discriminant
 153    //   window_x: Option<f32>, // WindowBounds::Fixed RectF x
 154    //   window_y: Option<f32>, // WindowBounds::Fixed RectF y
 155    //   window_width: Option<f32>, // WindowBounds::Fixed RectF width
 156    //   window_height: Option<f32>, // WindowBounds::Fixed RectF height
 157    //   display: Option<Uuid>, // Display id
 158    // )
 159    //
 160    // pane_groups(
 161    //   group_id: usize, // Primary key for pane_groups
 162    //   workspace_id: usize, // References workspaces table
 163    //   parent_group_id: Option<usize>, // None indicates that this is the root node
 164    //   position: Optiopn<usize>, // None indicates that this is the root node
 165    //   axis: Option<Axis>, // 'Vertical', 'Horizontal'
 166    //   flexes: Option<Vec<f32>>, // A JSON array of floats
 167    // )
 168    //
 169    // panes(
 170    //     pane_id: usize, // Primary key for panes
 171    //     workspace_id: usize, // References workspaces table
 172    //     active: bool,
 173    // )
 174    //
 175    // center_panes(
 176    //     pane_id: usize, // Primary key for center_panes
 177    //     parent_group_id: Option<usize>, // References pane_groups. If none, this is the root
 178    //     position: Option<usize>, // None indicates this is the root
 179    // )
 180    //
 181    // CREATE TABLE items(
 182    //     item_id: usize, // This is the item's view id, so this is not unique
 183    //     workspace_id: usize, // References workspaces table
 184    //     pane_id: usize, // References panes table
 185    //     kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global
 186    //     position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column
 187    //     active: bool, // Indicates if this item is the active one in the pane
 188    // )
 189    pub static ref DB: WorkspaceDb<()> =
 190    &[sql!(
 191        CREATE TABLE workspaces(
 192            workspace_id INTEGER PRIMARY KEY,
 193            workspace_location BLOB UNIQUE,
 194            dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
 195            dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
 196            dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
 197            left_sidebar_open INTEGER, // Boolean
 198            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 199            FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
 200        ) STRICT;
 201
 202        CREATE TABLE pane_groups(
 203            group_id INTEGER PRIMARY KEY,
 204            workspace_id INTEGER NOT NULL,
 205            parent_group_id INTEGER, // NULL indicates that this is a root node
 206            position INTEGER, // NULL indicates that this is a root node
 207            axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
 208            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 209            ON DELETE CASCADE
 210            ON UPDATE CASCADE,
 211            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 212        ) STRICT;
 213
 214        CREATE TABLE panes(
 215            pane_id INTEGER PRIMARY KEY,
 216            workspace_id INTEGER NOT NULL,
 217            active INTEGER NOT NULL, // Boolean
 218            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 219            ON DELETE CASCADE
 220            ON UPDATE CASCADE
 221        ) STRICT;
 222
 223        CREATE TABLE center_panes(
 224            pane_id INTEGER PRIMARY KEY,
 225            parent_group_id INTEGER, // NULL means that this is a root pane
 226            position INTEGER, // NULL means that this is a root pane
 227            FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 228            ON DELETE CASCADE,
 229            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 230        ) STRICT;
 231
 232        CREATE TABLE items(
 233            item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
 234            workspace_id INTEGER NOT NULL,
 235            pane_id INTEGER NOT NULL,
 236            kind TEXT NOT NULL,
 237            position INTEGER NOT NULL,
 238            active INTEGER NOT NULL,
 239            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 240            ON DELETE CASCADE
 241            ON UPDATE CASCADE,
 242            FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 243            ON DELETE CASCADE,
 244            PRIMARY KEY(item_id, workspace_id)
 245        ) STRICT;
 246    ),
 247    sql!(
 248        ALTER TABLE workspaces ADD COLUMN window_state TEXT;
 249        ALTER TABLE workspaces ADD COLUMN window_x REAL;
 250        ALTER TABLE workspaces ADD COLUMN window_y REAL;
 251        ALTER TABLE workspaces ADD COLUMN window_width REAL;
 252        ALTER TABLE workspaces ADD COLUMN window_height REAL;
 253        ALTER TABLE workspaces ADD COLUMN display BLOB;
 254    ),
 255    // Drop foreign key constraint from workspaces.dock_pane to panes table.
 256    sql!(
 257        CREATE TABLE workspaces_2(
 258            workspace_id INTEGER PRIMARY KEY,
 259            workspace_location BLOB UNIQUE,
 260            dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
 261            dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
 262            dock_pane INTEGER, // Deprecated.  Preserving so users can downgrade Zed.
 263            left_sidebar_open INTEGER, // Boolean
 264            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
 265            window_state TEXT,
 266            window_x REAL,
 267            window_y REAL,
 268            window_width REAL,
 269            window_height REAL,
 270            display BLOB
 271        ) STRICT;
 272        INSERT INTO workspaces_2 SELECT * FROM workspaces;
 273        DROP TABLE workspaces;
 274        ALTER TABLE workspaces_2 RENAME TO workspaces;
 275    ),
 276    // Add panels related information
 277    sql!(
 278        ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
 279        ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
 280        ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
 281        ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
 282        ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
 283        ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
 284    ),
 285    // Add panel zoom persistence
 286    sql!(
 287        ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
 288        ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
 289        ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
 290    ),
 291    // Add pane group flex data
 292    sql!(
 293        ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
 294    )
 295    ];
 296}
 297
 298impl WorkspaceDb {
 299    /// Returns a serialized workspace for the given worktree_roots. If the passed array
 300    /// is empty, the most recent workspace is returned instead. If no workspace for the
 301    /// passed roots is stored, returns none.
 302    pub(crate) fn workspace_for_roots<P: AsRef<Path>>(
 303        &self,
 304        worktree_roots: &[P],
 305    ) -> Option<SerializedWorkspace> {
 306        let workspace_location: WorkspaceLocation = worktree_roots.into();
 307
 308        // Note that we re-assign the workspace_id here in case it's empty
 309        // and we've grabbed the most recent workspace
 310        let (workspace_id, workspace_location, bounds, display, docks): (
 311            WorkspaceId,
 312            WorkspaceLocation,
 313            Option<SerializedWindowsBounds>,
 314            Option<Uuid>,
 315            DockStructure,
 316        ) = self
 317            .select_row_bound(sql! {
 318                SELECT
 319                    workspace_id,
 320                    workspace_location,
 321                    window_state,
 322                    window_x,
 323                    window_y,
 324                    window_width,
 325                    window_height,
 326                    display,
 327                    left_dock_visible,
 328                    left_dock_active_panel,
 329                    left_dock_zoom,
 330                    right_dock_visible,
 331                    right_dock_active_panel,
 332                    right_dock_zoom,
 333                    bottom_dock_visible,
 334                    bottom_dock_active_panel,
 335                    bottom_dock_zoom
 336                FROM workspaces
 337                WHERE workspace_location = ?
 338            })
 339            .and_then(|mut prepared_statement| (prepared_statement)(&workspace_location))
 340            .context("No workspaces found")
 341            .warn_on_err()
 342            .flatten()?;
 343
 344        Some(SerializedWorkspace {
 345            id: workspace_id,
 346            location: workspace_location.clone(),
 347            center_group: self
 348                .get_center_pane_group(workspace_id)
 349                .context("Getting center group")
 350                .log_err()?,
 351            bounds: bounds.map(|bounds| bounds.0),
 352            display,
 353            docks,
 354        })
 355    }
 356
 357    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
 358    /// that used this workspace previously
 359    pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
 360        self.write(move |conn| {
 361            conn.with_savepoint("update_worktrees", || {
 362                // Clear out panes and pane_groups
 363                conn.exec_bound(sql!(
 364                    DELETE FROM pane_groups WHERE workspace_id = ?1;
 365                    DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
 366                .expect("Clearing old panes");
 367
 368                conn.exec_bound(sql!(
 369                    DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?
 370                ))?((&workspace.location, workspace.id.clone()))
 371                .context("clearing out old locations")?;
 372
 373                // Upsert
 374                conn.exec_bound(sql!(
 375                    INSERT INTO workspaces(
 376                        workspace_id,
 377                        workspace_location,
 378                        left_dock_visible,
 379                        left_dock_active_panel,
 380                        left_dock_zoom,
 381                        right_dock_visible,
 382                        right_dock_active_panel,
 383                        right_dock_zoom,
 384                        bottom_dock_visible,
 385                        bottom_dock_active_panel,
 386                        bottom_dock_zoom,
 387                        timestamp
 388                    )
 389                    VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
 390                    ON CONFLICT DO
 391                    UPDATE SET
 392                        workspace_location = ?2,
 393                        left_dock_visible = ?3,
 394                        left_dock_active_panel = ?4,
 395                        left_dock_zoom = ?5,
 396                        right_dock_visible = ?6,
 397                        right_dock_active_panel = ?7,
 398                        right_dock_zoom = ?8,
 399                        bottom_dock_visible = ?9,
 400                        bottom_dock_active_panel = ?10,
 401                        bottom_dock_zoom = ?11,
 402                        timestamp = CURRENT_TIMESTAMP
 403                ))?((workspace.id, &workspace.location, workspace.docks))
 404                .context("Updating workspace")?;
 405
 406                // Save center pane group
 407                Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
 408                    .context("save pane group in save workspace")?;
 409
 410                Ok(())
 411            })
 412            .log_err();
 413        })
 414        .await;
 415    }
 416
 417    query! {
 418        pub async fn next_id() -> Result<WorkspaceId> {
 419            INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
 420        }
 421    }
 422
 423    query! {
 424        fn recent_workspaces() -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
 425            SELECT workspace_id, workspace_location
 426            FROM workspaces
 427            WHERE workspace_location IS NOT NULL
 428            ORDER BY timestamp DESC
 429        }
 430    }
 431
 432    query! {
 433        async fn delete_stale_workspace(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_stale_workspace(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        Ok(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
 670#[cfg(test)]
 671mod tests {
 672    use super::*;
 673    use db::open_test_db;
 674    use gpui;
 675
 676    #[gpui::test]
 677    async fn test_next_id_stability() {
 678        env_logger::try_init().ok();
 679
 680        let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
 681
 682        db.write(|conn| {
 683            conn.migrate(
 684                "test_table",
 685                &[sql!(
 686                    CREATE TABLE test_table(
 687                        text TEXT,
 688                        workspace_id INTEGER,
 689                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 690                        ON DELETE CASCADE
 691                    ) STRICT;
 692                )],
 693            )
 694            .unwrap();
 695        })
 696        .await;
 697
 698        let id = db.next_id().await.unwrap();
 699        // Assert the empty row got inserted
 700        assert_eq!(
 701            Some(id),
 702            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
 703                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
 704            ))
 705            .unwrap()(id)
 706            .unwrap()
 707        );
 708
 709        db.write(move |conn| {
 710            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
 711                .unwrap()(("test-text-1", id))
 712            .unwrap()
 713        })
 714        .await;
 715
 716        let test_text_1 = db
 717            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
 718            .unwrap()(1)
 719        .unwrap()
 720        .unwrap();
 721        assert_eq!(test_text_1, "test-text-1");
 722    }
 723
 724    #[gpui::test]
 725    async fn test_workspace_id_stability() {
 726        env_logger::try_init().ok();
 727
 728        let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
 729
 730        db.write(|conn| {
 731            conn.migrate(
 732                "test_table",
 733                &[sql!(
 734                        CREATE TABLE test_table(
 735                            text TEXT,
 736                            workspace_id INTEGER,
 737                            FOREIGN KEY(workspace_id)
 738                                REFERENCES workspaces(workspace_id)
 739                            ON DELETE CASCADE
 740                        ) STRICT;)],
 741            )
 742        })
 743        .await
 744        .unwrap();
 745
 746        let mut workspace_1 = SerializedWorkspace {
 747            id: 1,
 748            location: (["/tmp", "/tmp2"]).into(),
 749            center_group: Default::default(),
 750            bounds: Default::default(),
 751            display: Default::default(),
 752            docks: Default::default(),
 753        };
 754
 755        let workspace_2 = SerializedWorkspace {
 756            id: 2,
 757            location: (["/tmp"]).into(),
 758            center_group: Default::default(),
 759            bounds: Default::default(),
 760            display: Default::default(),
 761            docks: Default::default(),
 762        };
 763
 764        db.save_workspace(workspace_1.clone()).await;
 765
 766        db.write(|conn| {
 767            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
 768                .unwrap()(("test-text-1", 1))
 769            .unwrap();
 770        })
 771        .await;
 772
 773        db.save_workspace(workspace_2.clone()).await;
 774
 775        db.write(|conn| {
 776            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
 777                .unwrap()(("test-text-2", 2))
 778            .unwrap();
 779        })
 780        .await;
 781
 782        workspace_1.location = (["/tmp", "/tmp3"]).into();
 783        db.save_workspace(workspace_1.clone()).await;
 784        db.save_workspace(workspace_1).await;
 785        db.save_workspace(workspace_2).await;
 786
 787        let test_text_2 = db
 788            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
 789            .unwrap()(2)
 790        .unwrap()
 791        .unwrap();
 792        assert_eq!(test_text_2, "test-text-2");
 793
 794        let test_text_1 = db
 795            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
 796            .unwrap()(1)
 797        .unwrap()
 798        .unwrap();
 799        assert_eq!(test_text_1, "test-text-1");
 800    }
 801
 802    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
 803        SerializedPaneGroup::Group {
 804            axis: SerializedAxis(axis),
 805            flexes: None,
 806            children,
 807        }
 808    }
 809
 810    #[gpui::test]
 811    async fn test_full_workspace_serialization() {
 812        env_logger::try_init().ok();
 813
 814        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
 815
 816        //  -----------------
 817        //  | 1,2   | 5,6   |
 818        //  | - - - |       |
 819        //  | 3,4   |       |
 820        //  -----------------
 821        let center_group = group(
 822            Axis::Horizontal,
 823            vec![
 824                group(
 825                    Axis::Vertical,
 826                    vec![
 827                        SerializedPaneGroup::Pane(SerializedPane::new(
 828                            vec![
 829                                SerializedItem::new("Terminal", 5, false),
 830                                SerializedItem::new("Terminal", 6, true),
 831                            ],
 832                            false,
 833                        )),
 834                        SerializedPaneGroup::Pane(SerializedPane::new(
 835                            vec![
 836                                SerializedItem::new("Terminal", 7, true),
 837                                SerializedItem::new("Terminal", 8, false),
 838                            ],
 839                            false,
 840                        )),
 841                    ],
 842                ),
 843                SerializedPaneGroup::Pane(SerializedPane::new(
 844                    vec![
 845                        SerializedItem::new("Terminal", 9, false),
 846                        SerializedItem::new("Terminal", 10, true),
 847                    ],
 848                    false,
 849                )),
 850            ],
 851        );
 852
 853        let workspace = SerializedWorkspace {
 854            id: 5,
 855            location: (["/tmp", "/tmp2"]).into(),
 856            center_group,
 857            bounds: Default::default(),
 858            display: Default::default(),
 859            docks: Default::default(),
 860        };
 861
 862        db.save_workspace(workspace.clone()).await;
 863        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
 864
 865        assert_eq!(workspace, round_trip_workspace.unwrap());
 866
 867        // Test guaranteed duplicate IDs
 868        db.save_workspace(workspace.clone()).await;
 869        db.save_workspace(workspace.clone()).await;
 870
 871        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
 872        assert_eq!(workspace, round_trip_workspace.unwrap());
 873    }
 874
 875    #[gpui::test]
 876    async fn test_workspace_assignment() {
 877        env_logger::try_init().ok();
 878
 879        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
 880
 881        let workspace_1 = SerializedWorkspace {
 882            id: 1,
 883            location: (["/tmp", "/tmp2"]).into(),
 884            center_group: Default::default(),
 885            bounds: Default::default(),
 886            display: Default::default(),
 887            docks: Default::default(),
 888        };
 889
 890        let mut workspace_2 = SerializedWorkspace {
 891            id: 2,
 892            location: (["/tmp"]).into(),
 893            center_group: Default::default(),
 894            bounds: Default::default(),
 895            display: Default::default(),
 896            docks: Default::default(),
 897        };
 898
 899        db.save_workspace(workspace_1.clone()).await;
 900        db.save_workspace(workspace_2.clone()).await;
 901
 902        // Test that paths are treated as a set
 903        assert_eq!(
 904            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
 905            workspace_1
 906        );
 907        assert_eq!(
 908            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
 909            workspace_1
 910        );
 911
 912        // Make sure that other keys work
 913        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
 914        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
 915
 916        // Test 'mutate' case of updating a pre-existing id
 917        workspace_2.location = (["/tmp", "/tmp2"]).into();
 918
 919        db.save_workspace(workspace_2.clone()).await;
 920        assert_eq!(
 921            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
 922            workspace_2
 923        );
 924
 925        // Test other mechanism for mutating
 926        let mut workspace_3 = SerializedWorkspace {
 927            id: 3,
 928            location: (&["/tmp", "/tmp2"]).into(),
 929            center_group: Default::default(),
 930            bounds: Default::default(),
 931            display: Default::default(),
 932            docks: Default::default(),
 933        };
 934
 935        db.save_workspace(workspace_3.clone()).await;
 936        assert_eq!(
 937            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
 938            workspace_3
 939        );
 940
 941        // Make sure that updating paths differently also works
 942        workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
 943        db.save_workspace(workspace_3.clone()).await;
 944        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
 945        assert_eq!(
 946            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
 947                .unwrap(),
 948            workspace_3
 949        );
 950    }
 951
 952    use crate::persistence::model::SerializedWorkspace;
 953    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
 954
 955    fn default_workspace<P: AsRef<Path>>(
 956        workspace_id: &[P],
 957        center_group: &SerializedPaneGroup,
 958    ) -> SerializedWorkspace {
 959        SerializedWorkspace {
 960            id: 4,
 961            location: workspace_id.into(),
 962            center_group: center_group.clone(),
 963            bounds: Default::default(),
 964            display: Default::default(),
 965            docks: Default::default(),
 966        }
 967    }
 968
 969    #[gpui::test]
 970    async fn test_simple_split() {
 971        env_logger::try_init().ok();
 972
 973        let db = WorkspaceDb(open_test_db("simple_split").await);
 974
 975        //  -----------------
 976        //  | 1,2   | 5,6   |
 977        //  | - - - |       |
 978        //  | 3,4   |       |
 979        //  -----------------
 980        let center_pane = group(
 981            Axis::Horizontal,
 982            vec![
 983                group(
 984                    Axis::Vertical,
 985                    vec![
 986                        SerializedPaneGroup::Pane(SerializedPane::new(
 987                            vec![
 988                                SerializedItem::new("Terminal", 1, false),
 989                                SerializedItem::new("Terminal", 2, true),
 990                            ],
 991                            false,
 992                        )),
 993                        SerializedPaneGroup::Pane(SerializedPane::new(
 994                            vec![
 995                                SerializedItem::new("Terminal", 4, false),
 996                                SerializedItem::new("Terminal", 3, true),
 997                            ],
 998                            true,
 999                        )),
1000                    ],
1001                ),
1002                SerializedPaneGroup::Pane(SerializedPane::new(
1003                    vec![
1004                        SerializedItem::new("Terminal", 5, true),
1005                        SerializedItem::new("Terminal", 6, false),
1006                    ],
1007                    false,
1008                )),
1009            ],
1010        );
1011
1012        let workspace = default_workspace(&["/tmp"], &center_pane);
1013
1014        db.save_workspace(workspace.clone()).await;
1015
1016        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
1017
1018        assert_eq!(workspace.center_group, new_workspace.center_group);
1019    }
1020
1021    #[gpui::test]
1022    async fn test_cleanup_panes() {
1023        env_logger::try_init().ok();
1024
1025        let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
1026
1027        let center_pane = group(
1028            Axis::Horizontal,
1029            vec![
1030                group(
1031                    Axis::Vertical,
1032                    vec![
1033                        SerializedPaneGroup::Pane(SerializedPane::new(
1034                            vec![
1035                                SerializedItem::new("Terminal", 1, false),
1036                                SerializedItem::new("Terminal", 2, true),
1037                            ],
1038                            false,
1039                        )),
1040                        SerializedPaneGroup::Pane(SerializedPane::new(
1041                            vec![
1042                                SerializedItem::new("Terminal", 4, false),
1043                                SerializedItem::new("Terminal", 3, true),
1044                            ],
1045                            true,
1046                        )),
1047                    ],
1048                ),
1049                SerializedPaneGroup::Pane(SerializedPane::new(
1050                    vec![
1051                        SerializedItem::new("Terminal", 5, false),
1052                        SerializedItem::new("Terminal", 6, true),
1053                    ],
1054                    false,
1055                )),
1056            ],
1057        );
1058
1059        let id = &["/tmp"];
1060
1061        let mut workspace = default_workspace(id, &center_pane);
1062
1063        db.save_workspace(workspace.clone()).await;
1064
1065        workspace.center_group = group(
1066            Axis::Vertical,
1067            vec![
1068                SerializedPaneGroup::Pane(SerializedPane::new(
1069                    vec![
1070                        SerializedItem::new("Terminal", 1, false),
1071                        SerializedItem::new("Terminal", 2, true),
1072                    ],
1073                    false,
1074                )),
1075                SerializedPaneGroup::Pane(SerializedPane::new(
1076                    vec![
1077                        SerializedItem::new("Terminal", 4, true),
1078                        SerializedItem::new("Terminal", 3, false),
1079                    ],
1080                    true,
1081                )),
1082            ],
1083        );
1084
1085        db.save_workspace(workspace.clone()).await;
1086
1087        let new_workspace = db.workspace_for_roots(id).unwrap();
1088
1089        assert_eq!(workspace.center_group, new_workspace.center_group);
1090    }
1091}