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