persistence.rs

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