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