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