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    pub async fn delete_workspace_by_dev_server_project_id(
 580        &self,
 581        id: DevServerProjectId,
 582    ) -> Result<()> {
 583        self.write(move |conn| {
 584            conn.exec_bound(sql!(
 585                DELETE FROM dev_server_projects WHERE id = ?
 586            ))?(id.0)?;
 587            conn.exec_bound(sql!(
 588                DELETE FROM workspaces
 589                WHERE dev_server_project_id IS ?
 590            ))?(id.0)
 591        })
 592        .await
 593    }
 594
 595    // Returns the recent locations which are still valid on disk and deletes ones which no longer
 596    // exist.
 597    pub async fn recent_workspaces_on_disk(
 598        &self,
 599    ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation)>> {
 600        let mut result = Vec::new();
 601        let mut delete_tasks = Vec::new();
 602        let dev_server_projects = self.dev_server_projects()?;
 603
 604        for (id, location, dev_server_project_id) in self.recent_workspaces()? {
 605            if let Some(dev_server_project_id) = dev_server_project_id.map(DevServerProjectId) {
 606                if let Some(dev_server_project) = dev_server_projects
 607                    .iter()
 608                    .find(|rp| rp.id == dev_server_project_id)
 609                {
 610                    result.push((id, dev_server_project.clone().into()));
 611                } else {
 612                    delete_tasks.push(self.delete_workspace_by_id(id));
 613                }
 614                continue;
 615            }
 616
 617            if location.paths().iter().all(|path| path.exists())
 618                && location.paths().iter().any(|path| path.is_dir())
 619            {
 620                result.push((id, location.into()));
 621            } else {
 622                delete_tasks.push(self.delete_workspace_by_id(id));
 623            }
 624        }
 625
 626        futures::future::join_all(delete_tasks).await;
 627        Ok(result)
 628    }
 629
 630    pub async fn last_workspace(&self) -> Result<Option<LocalPaths>> {
 631        Ok(self
 632            .recent_workspaces_on_disk()
 633            .await?
 634            .into_iter()
 635            .filter_map(|(_, location)| match location {
 636                SerializedWorkspaceLocation::Local(local_paths) => Some(local_paths),
 637                SerializedWorkspaceLocation::DevServer(_) => None,
 638            })
 639            .next())
 640    }
 641
 642    fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
 643        Ok(self
 644            .get_pane_group(workspace_id, None)?
 645            .into_iter()
 646            .next()
 647            .unwrap_or_else(|| {
 648                SerializedPaneGroup::Pane(SerializedPane {
 649                    active: true,
 650                    children: vec![],
 651                })
 652            }))
 653    }
 654
 655    fn get_pane_group(
 656        &self,
 657        workspace_id: WorkspaceId,
 658        group_id: Option<GroupId>,
 659    ) -> Result<Vec<SerializedPaneGroup>> {
 660        type GroupKey = (Option<GroupId>, WorkspaceId);
 661        type GroupOrPane = (
 662            Option<GroupId>,
 663            Option<SerializedAxis>,
 664            Option<PaneId>,
 665            Option<bool>,
 666            Option<String>,
 667        );
 668        self.select_bound::<GroupKey, GroupOrPane>(sql!(
 669            SELECT group_id, axis, pane_id, active, flexes
 670                FROM (SELECT
 671                        group_id,
 672                        axis,
 673                        NULL as pane_id,
 674                        NULL as active,
 675                        position,
 676                        parent_group_id,
 677                        workspace_id,
 678                        flexes
 679                      FROM pane_groups
 680                    UNION
 681                      SELECT
 682                        NULL,
 683                        NULL,
 684                        center_panes.pane_id,
 685                        panes.active as active,
 686                        position,
 687                        parent_group_id,
 688                        panes.workspace_id as workspace_id,
 689                        NULL
 690                      FROM center_panes
 691                      JOIN panes ON center_panes.pane_id = panes.pane_id)
 692                WHERE parent_group_id IS ? AND workspace_id = ?
 693                ORDER BY position
 694        ))?((group_id, workspace_id))?
 695        .into_iter()
 696        .map(|(group_id, axis, pane_id, active, flexes)| {
 697            if let Some((group_id, axis)) = group_id.zip(axis) {
 698                let flexes = flexes
 699                    .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
 700                    .transpose()?;
 701
 702                Ok(SerializedPaneGroup::Group {
 703                    axis,
 704                    children: self.get_pane_group(workspace_id, Some(group_id))?,
 705                    flexes,
 706                })
 707            } else if let Some((pane_id, active)) = pane_id.zip(active) {
 708                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
 709                    self.get_items(pane_id)?,
 710                    active,
 711                )))
 712            } else {
 713                bail!("Pane Group Child was neither a pane group or a pane");
 714            }
 715        })
 716        // Filter out panes and pane groups which don't have any children or items
 717        .filter(|pane_group| match pane_group {
 718            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
 719            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
 720            _ => true,
 721        })
 722        .collect::<Result<_>>()
 723    }
 724
 725    fn save_pane_group(
 726        conn: &Connection,
 727        workspace_id: WorkspaceId,
 728        pane_group: &SerializedPaneGroup,
 729        parent: Option<(GroupId, usize)>,
 730    ) -> Result<()> {
 731        match pane_group {
 732            SerializedPaneGroup::Group {
 733                axis,
 734                children,
 735                flexes,
 736            } => {
 737                let (parent_id, position) = unzip_option(parent);
 738
 739                let flex_string = flexes
 740                    .as_ref()
 741                    .map(|flexes| serde_json::json!(flexes).to_string());
 742
 743                let group_id = conn.select_row_bound::<_, i64>(sql!(
 744                    INSERT INTO pane_groups(
 745                        workspace_id,
 746                        parent_group_id,
 747                        position,
 748                        axis,
 749                        flexes
 750                    )
 751                    VALUES (?, ?, ?, ?, ?)
 752                    RETURNING group_id
 753                ))?((
 754                    workspace_id,
 755                    parent_id,
 756                    position,
 757                    *axis,
 758                    flex_string,
 759                ))?
 760                .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
 761
 762                for (position, group) in children.iter().enumerate() {
 763                    Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
 764                }
 765
 766                Ok(())
 767            }
 768            SerializedPaneGroup::Pane(pane) => {
 769                Self::save_pane(conn, workspace_id, pane, parent)?;
 770                Ok(())
 771            }
 772        }
 773    }
 774
 775    fn save_pane(
 776        conn: &Connection,
 777        workspace_id: WorkspaceId,
 778        pane: &SerializedPane,
 779        parent: Option<(GroupId, usize)>,
 780    ) -> Result<PaneId> {
 781        let pane_id = conn.select_row_bound::<_, i64>(sql!(
 782            INSERT INTO panes(workspace_id, active)
 783            VALUES (?, ?)
 784            RETURNING pane_id
 785        ))?((workspace_id, pane.active))?
 786        .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
 787
 788        let (parent_id, order) = unzip_option(parent);
 789        conn.exec_bound(sql!(
 790            INSERT INTO center_panes(pane_id, parent_group_id, position)
 791            VALUES (?, ?, ?)
 792        ))?((pane_id, parent_id, order))?;
 793
 794        Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
 795
 796        Ok(pane_id)
 797    }
 798
 799    fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
 800        self.select_bound(sql!(
 801            SELECT kind, item_id, active, preview FROM items
 802            WHERE pane_id = ?
 803                ORDER BY position
 804        ))?(pane_id)
 805    }
 806
 807    fn save_items(
 808        conn: &Connection,
 809        workspace_id: WorkspaceId,
 810        pane_id: PaneId,
 811        items: &[SerializedItem],
 812    ) -> Result<()> {
 813        let mut insert = conn.exec_bound(sql!(
 814            INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
 815        )).context("Preparing insertion")?;
 816        for (position, item) in items.iter().enumerate() {
 817            insert((workspace_id, pane_id, position, item))?;
 818        }
 819
 820        Ok(())
 821    }
 822
 823    query! {
 824        pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
 825            UPDATE workspaces
 826            SET timestamp = CURRENT_TIMESTAMP
 827            WHERE workspace_id = ?
 828        }
 829    }
 830
 831    query! {
 832        pub(crate) async fn set_window_bounds(workspace_id: WorkspaceId, bounds: SerializedWindowsBounds, display: Uuid) -> Result<()> {
 833            UPDATE workspaces
 834            SET window_state = ?2,
 835                window_x = ?3,
 836                window_y = ?4,
 837                window_width = ?5,
 838                window_height = ?6,
 839                display = ?7
 840            WHERE workspace_id = ?1
 841        }
 842    }
 843
 844    query! {
 845        pub(crate) async fn set_fullscreen(workspace_id: WorkspaceId, fullscreen: bool) -> Result<()> {
 846            UPDATE workspaces
 847            SET fullscreen = ?2
 848            WHERE workspace_id = ?1
 849        }
 850    }
 851
 852    query! {
 853        pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
 854            UPDATE workspaces
 855            SET centered_layout = ?2
 856            WHERE workspace_id = ?1
 857        }
 858    }
 859}
 860
 861#[cfg(test)]
 862mod tests {
 863    use super::*;
 864    use db::open_test_db;
 865    use gpui;
 866
 867    #[gpui::test]
 868    async fn test_next_id_stability() {
 869        env_logger::try_init().ok();
 870
 871        let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
 872
 873        db.write(|conn| {
 874            conn.migrate(
 875                "test_table",
 876                &[sql!(
 877                    CREATE TABLE test_table(
 878                        text TEXT,
 879                        workspace_id INTEGER,
 880                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 881                        ON DELETE CASCADE
 882                    ) STRICT;
 883                )],
 884            )
 885            .unwrap();
 886        })
 887        .await;
 888
 889        let id = db.next_id().await.unwrap();
 890        // Assert the empty row got inserted
 891        assert_eq!(
 892            Some(id),
 893            db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
 894                SELECT workspace_id FROM workspaces WHERE workspace_id = ?
 895            ))
 896            .unwrap()(id)
 897            .unwrap()
 898        );
 899
 900        db.write(move |conn| {
 901            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
 902                .unwrap()(("test-text-1", id))
 903            .unwrap()
 904        })
 905        .await;
 906
 907        let test_text_1 = db
 908            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
 909            .unwrap()(1)
 910        .unwrap()
 911        .unwrap();
 912        assert_eq!(test_text_1, "test-text-1");
 913    }
 914
 915    #[gpui::test]
 916    async fn test_workspace_id_stability() {
 917        env_logger::try_init().ok();
 918
 919        let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
 920
 921        db.write(|conn| {
 922            conn.migrate(
 923                "test_table",
 924                &[sql!(
 925                        CREATE TABLE test_table(
 926                            text TEXT,
 927                            workspace_id INTEGER,
 928                            FOREIGN KEY(workspace_id)
 929                                REFERENCES workspaces(workspace_id)
 930                            ON DELETE CASCADE
 931                        ) STRICT;)],
 932            )
 933        })
 934        .await
 935        .unwrap();
 936
 937        let mut workspace_1 = SerializedWorkspace {
 938            id: WorkspaceId(1),
 939            location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
 940            center_group: Default::default(),
 941            bounds: Default::default(),
 942            display: Default::default(),
 943            docks: Default::default(),
 944            fullscreen: false,
 945            centered_layout: false,
 946        };
 947
 948        let workspace_2 = SerializedWorkspace {
 949            id: WorkspaceId(2),
 950            location: LocalPaths::new(["/tmp"]).into(),
 951            center_group: Default::default(),
 952            bounds: Default::default(),
 953            display: Default::default(),
 954            docks: Default::default(),
 955            fullscreen: false,
 956            centered_layout: false,
 957        };
 958
 959        db.save_workspace(workspace_1.clone()).await;
 960
 961        db.write(|conn| {
 962            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
 963                .unwrap()(("test-text-1", 1))
 964            .unwrap();
 965        })
 966        .await;
 967
 968        db.save_workspace(workspace_2.clone()).await;
 969
 970        db.write(|conn| {
 971            conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
 972                .unwrap()(("test-text-2", 2))
 973            .unwrap();
 974        })
 975        .await;
 976
 977        workspace_1.location = LocalPaths::new(["/tmp", "/tmp3"]).into();
 978        db.save_workspace(workspace_1.clone()).await;
 979        db.save_workspace(workspace_1).await;
 980        db.save_workspace(workspace_2).await;
 981
 982        let test_text_2 = db
 983            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
 984            .unwrap()(2)
 985        .unwrap()
 986        .unwrap();
 987        assert_eq!(test_text_2, "test-text-2");
 988
 989        let test_text_1 = db
 990            .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
 991            .unwrap()(1)
 992        .unwrap()
 993        .unwrap();
 994        assert_eq!(test_text_1, "test-text-1");
 995    }
 996
 997    fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
 998        SerializedPaneGroup::Group {
 999            axis: SerializedAxis(axis),
1000            flexes: None,
1001            children,
1002        }
1003    }
1004
1005    #[gpui::test]
1006    async fn test_full_workspace_serialization() {
1007        env_logger::try_init().ok();
1008
1009        let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
1010
1011        //  -----------------
1012        //  | 1,2   | 5,6   |
1013        //  | - - - |       |
1014        //  | 3,4   |       |
1015        //  -----------------
1016        let center_group = group(
1017            Axis::Horizontal,
1018            vec![
1019                group(
1020                    Axis::Vertical,
1021                    vec![
1022                        SerializedPaneGroup::Pane(SerializedPane::new(
1023                            vec![
1024                                SerializedItem::new("Terminal", 5, false, false),
1025                                SerializedItem::new("Terminal", 6, true, false),
1026                            ],
1027                            false,
1028                        )),
1029                        SerializedPaneGroup::Pane(SerializedPane::new(
1030                            vec![
1031                                SerializedItem::new("Terminal", 7, true, false),
1032                                SerializedItem::new("Terminal", 8, false, false),
1033                            ],
1034                            false,
1035                        )),
1036                    ],
1037                ),
1038                SerializedPaneGroup::Pane(SerializedPane::new(
1039                    vec![
1040                        SerializedItem::new("Terminal", 9, false, false),
1041                        SerializedItem::new("Terminal", 10, true, false),
1042                    ],
1043                    false,
1044                )),
1045            ],
1046        );
1047
1048        let workspace = SerializedWorkspace {
1049            id: WorkspaceId(5),
1050            location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
1051            center_group,
1052            bounds: Default::default(),
1053            display: Default::default(),
1054            docks: Default::default(),
1055            fullscreen: false,
1056            centered_layout: false,
1057        };
1058
1059        db.save_workspace(workspace.clone()).await;
1060        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
1061
1062        assert_eq!(workspace, round_trip_workspace.unwrap());
1063
1064        // Test guaranteed duplicate IDs
1065        db.save_workspace(workspace.clone()).await;
1066        db.save_workspace(workspace.clone()).await;
1067
1068        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
1069        assert_eq!(workspace, round_trip_workspace.unwrap());
1070    }
1071
1072    #[gpui::test]
1073    async fn test_workspace_assignment() {
1074        env_logger::try_init().ok();
1075
1076        let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
1077
1078        let workspace_1 = SerializedWorkspace {
1079            id: WorkspaceId(1),
1080            location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
1081            center_group: Default::default(),
1082            bounds: Default::default(),
1083            display: Default::default(),
1084            docks: Default::default(),
1085            fullscreen: false,
1086            centered_layout: false,
1087        };
1088
1089        let mut workspace_2 = SerializedWorkspace {
1090            id: WorkspaceId(2),
1091            location: LocalPaths::new(["/tmp"]).into(),
1092            center_group: Default::default(),
1093            bounds: Default::default(),
1094            display: Default::default(),
1095            docks: Default::default(),
1096            fullscreen: false,
1097            centered_layout: false,
1098        };
1099
1100        db.save_workspace(workspace_1.clone()).await;
1101        db.save_workspace(workspace_2.clone()).await;
1102
1103        // Test that paths are treated as a set
1104        assert_eq!(
1105            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1106            workspace_1
1107        );
1108        assert_eq!(
1109            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
1110            workspace_1
1111        );
1112
1113        // Make sure that other keys work
1114        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
1115        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
1116
1117        // Test 'mutate' case of updating a pre-existing id
1118        workspace_2.location = LocalPaths::new(["/tmp", "/tmp2"]).into();
1119
1120        db.save_workspace(workspace_2.clone()).await;
1121        assert_eq!(
1122            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1123            workspace_2
1124        );
1125
1126        // Test other mechanism for mutating
1127        let mut workspace_3 = SerializedWorkspace {
1128            id: WorkspaceId(3),
1129            location: LocalPaths::new(&["/tmp", "/tmp2"]).into(),
1130            center_group: Default::default(),
1131            bounds: Default::default(),
1132            display: Default::default(),
1133            docks: Default::default(),
1134            fullscreen: false,
1135            centered_layout: false,
1136        };
1137
1138        db.save_workspace(workspace_3.clone()).await;
1139        assert_eq!(
1140            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1141            workspace_3
1142        );
1143
1144        // Make sure that updating paths differently also works
1145        workspace_3.location = LocalPaths::new(["/tmp3", "/tmp4", "/tmp2"]).into();
1146        db.save_workspace(workspace_3.clone()).await;
1147        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
1148        assert_eq!(
1149            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
1150                .unwrap(),
1151            workspace_3
1152        );
1153    }
1154
1155    use crate::persistence::model::SerializedWorkspace;
1156    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
1157
1158    fn default_workspace<P: AsRef<Path>>(
1159        workspace_id: &[P],
1160        center_group: &SerializedPaneGroup,
1161    ) -> SerializedWorkspace {
1162        SerializedWorkspace {
1163            id: WorkspaceId(4),
1164            location: LocalPaths::new(workspace_id).into(),
1165            center_group: center_group.clone(),
1166            bounds: Default::default(),
1167            display: Default::default(),
1168            docks: Default::default(),
1169            fullscreen: false,
1170            centered_layout: false,
1171        }
1172    }
1173
1174    #[gpui::test]
1175    async fn test_simple_split() {
1176        env_logger::try_init().ok();
1177
1178        let db = WorkspaceDb(open_test_db("simple_split").await);
1179
1180        //  -----------------
1181        //  | 1,2   | 5,6   |
1182        //  | - - - |       |
1183        //  | 3,4   |       |
1184        //  -----------------
1185        let center_pane = group(
1186            Axis::Horizontal,
1187            vec![
1188                group(
1189                    Axis::Vertical,
1190                    vec![
1191                        SerializedPaneGroup::Pane(SerializedPane::new(
1192                            vec![
1193                                SerializedItem::new("Terminal", 1, false, false),
1194                                SerializedItem::new("Terminal", 2, true, false),
1195                            ],
1196                            false,
1197                        )),
1198                        SerializedPaneGroup::Pane(SerializedPane::new(
1199                            vec![
1200                                SerializedItem::new("Terminal", 4, false, false),
1201                                SerializedItem::new("Terminal", 3, true, false),
1202                            ],
1203                            true,
1204                        )),
1205                    ],
1206                ),
1207                SerializedPaneGroup::Pane(SerializedPane::new(
1208                    vec![
1209                        SerializedItem::new("Terminal", 5, true, false),
1210                        SerializedItem::new("Terminal", 6, false, false),
1211                    ],
1212                    false,
1213                )),
1214            ],
1215        );
1216
1217        let workspace = default_workspace(&["/tmp"], &center_pane);
1218
1219        db.save_workspace(workspace.clone()).await;
1220
1221        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
1222
1223        assert_eq!(workspace.center_group, new_workspace.center_group);
1224    }
1225
1226    #[gpui::test]
1227    async fn test_cleanup_panes() {
1228        env_logger::try_init().ok();
1229
1230        let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
1231
1232        let center_pane = group(
1233            Axis::Horizontal,
1234            vec![
1235                group(
1236                    Axis::Vertical,
1237                    vec![
1238                        SerializedPaneGroup::Pane(SerializedPane::new(
1239                            vec![
1240                                SerializedItem::new("Terminal", 1, false, false),
1241                                SerializedItem::new("Terminal", 2, true, false),
1242                            ],
1243                            false,
1244                        )),
1245                        SerializedPaneGroup::Pane(SerializedPane::new(
1246                            vec![
1247                                SerializedItem::new("Terminal", 4, false, false),
1248                                SerializedItem::new("Terminal", 3, true, false),
1249                            ],
1250                            true,
1251                        )),
1252                    ],
1253                ),
1254                SerializedPaneGroup::Pane(SerializedPane::new(
1255                    vec![
1256                        SerializedItem::new("Terminal", 5, false, false),
1257                        SerializedItem::new("Terminal", 6, true, false),
1258                    ],
1259                    false,
1260                )),
1261            ],
1262        );
1263
1264        let id = &["/tmp"];
1265
1266        let mut workspace = default_workspace(id, &center_pane);
1267
1268        db.save_workspace(workspace.clone()).await;
1269
1270        workspace.center_group = group(
1271            Axis::Vertical,
1272            vec![
1273                SerializedPaneGroup::Pane(SerializedPane::new(
1274                    vec![
1275                        SerializedItem::new("Terminal", 1, false, false),
1276                        SerializedItem::new("Terminal", 2, true, false),
1277                    ],
1278                    false,
1279                )),
1280                SerializedPaneGroup::Pane(SerializedPane::new(
1281                    vec![
1282                        SerializedItem::new("Terminal", 4, true, false),
1283                        SerializedItem::new("Terminal", 3, false, false),
1284                    ],
1285                    true,
1286                )),
1287            ],
1288        );
1289
1290        db.save_workspace(workspace.clone()).await;
1291
1292        let new_workspace = db.workspace_for_roots(id).unwrap();
1293
1294        assert_eq!(workspace.center_group, new_workspace.center_group);
1295    }
1296}