persistence.rs

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