workspace.rs

  1pub(crate) mod items;
  2pub mod model;
  3pub(crate) mod pane;
  4
  5use anyhow::Context;
  6use util::{iife, ResultExt};
  7
  8use std::path::{Path, PathBuf};
  9
 10use indoc::indoc;
 11use sqlez::migrations::Migration;
 12
 13pub(crate) const WORKSPACES_MIGRATION: Migration = Migration::new(
 14    "workspace",
 15    &[indoc! {"
 16        CREATE TABLE workspaces(
 17            workspace_id BLOB PRIMARY KEY,
 18            dock_anchor TEXT, -- Enum: 'Bottom' / 'Right' / 'Expanded'
 19            dock_visible INTEGER, -- Boolean
 20            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL
 21        ) STRICT;
 22    "}],
 23);
 24
 25use self::model::{SerializedWorkspace, WorkspaceId, WorkspaceRow};
 26
 27use super::Db;
 28
 29impl Db {
 30    /// Returns a serialized workspace for the given worktree_roots. If the passed array
 31    /// is empty, the most recent workspace is returned instead. If no workspace for the
 32    /// passed roots is stored, returns none.
 33    pub fn workspace_for_roots<P: AsRef<Path>>(
 34        &self,
 35        worktree_roots: &[P],
 36    ) -> Option<SerializedWorkspace> {
 37        let workspace_id: WorkspaceId = worktree_roots.into();
 38
 39        // Note that we re-assign the workspace_id here in case it's empty
 40        // and we've grabbed the most recent workspace
 41        let (workspace_id, dock_anchor, dock_visible) = iife!({
 42            if worktree_roots.len() == 0 {
 43                self.prepare(indoc! {"
 44                        SELECT workspace_id, dock_anchor, dock_visible 
 45                        FROM workspaces 
 46                        ORDER BY timestamp DESC LIMIT 1"})?
 47                    .maybe_row::<WorkspaceRow>()
 48            } else {
 49                self.prepare(indoc! {"
 50                        SELECT workspace_id, dock_anchor, dock_visible 
 51                        FROM workspaces 
 52                        WHERE workspace_id = ?"})?
 53                    .with_bindings(&workspace_id)?
 54                    .maybe_row::<WorkspaceRow>()
 55            }
 56        })
 57        .log_err()
 58        .flatten()?;
 59
 60        Some(SerializedWorkspace {
 61            dock_pane: self
 62                .get_dock_pane(&workspace_id)
 63                .context("Getting dock pane")
 64                .log_err()?,
 65            center_group: self
 66                .get_center_pane_group(&workspace_id)
 67                .context("Getting center group")
 68                .log_err()?,
 69            dock_anchor,
 70            dock_visible,
 71        })
 72    }
 73
 74    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
 75    /// that used this workspace previously
 76    pub fn save_workspace<P: AsRef<Path>>(
 77        &self,
 78        worktree_roots: &[P],
 79        old_roots: Option<&[P]>,
 80        workspace: &SerializedWorkspace,
 81    ) {
 82        let workspace_id: WorkspaceId = worktree_roots.into();
 83
 84        self.with_savepoint("update_worktrees", || {
 85            if let Some(old_roots) = old_roots {
 86                let old_id: WorkspaceId = old_roots.into();
 87
 88                self.prepare("DELETE FROM WORKSPACES WHERE workspace_id = ?")?
 89                    .with_bindings(&old_id)?
 90                    .exec()?;
 91            }
 92
 93            // Delete any previous workspaces with the same roots. This cascades to all
 94            // other tables that are based on the same roots set.
 95            // Insert new workspace into workspaces table if none were found
 96            self.prepare("DELETE FROM workspaces WHERE workspace_id = ?;")?
 97                .with_bindings(&workspace_id)?
 98                .exec()?;
 99
100            self.prepare(
101                "INSERT INTO workspaces(workspace_id, dock_anchor, dock_visible) VALUES (?, ?, ?)",
102            )?
103            .with_bindings((&workspace_id, workspace.dock_anchor, workspace.dock_visible))?
104            .exec()?;
105
106            // Save center pane group and dock pane
107            self.save_pane_group(&workspace_id, &workspace.center_group, None)?;
108            self.save_pane(&workspace_id, &workspace.dock_pane, None)?;
109
110            Ok(())
111        })
112        .with_context(|| {
113            format!(
114                "Update workspace with roots {:?}",
115                worktree_roots
116                    .iter()
117                    .map(|p| p.as_ref())
118                    .collect::<Vec<_>>()
119            )
120        })
121        .log_err();
122    }
123
124    /// Returns the previous workspace ids sorted by last modified along with their opened worktree roots
125    pub fn recent_workspaces(&self, limit: usize) -> Vec<Vec<PathBuf>> {
126        iife!({
127            // TODO, upgrade anyhow: https://docs.rs/anyhow/1.0.66/anyhow/fn.Ok.html
128            Ok::<_, anyhow::Error>(
129                self.prepare(
130                    "SELECT workspace_id FROM workspaces ORDER BY timestamp DESC LIMIT ?",
131                )?
132                .with_bindings(limit)?
133                .rows::<WorkspaceId>()?
134                .into_iter()
135                .map(|id| id.paths())
136                .collect::<Vec<Vec<PathBuf>>>(),
137            )
138        })
139        .log_err()
140        .unwrap_or_default()
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use crate::{
147        model::{
148            DockAnchor::{Bottom, Expanded, Right},
149            SerializedWorkspace,
150        },
151        Db,
152    };
153
154    #[test]
155    fn test_workspace_assignment() {
156        env_logger::try_init().ok();
157
158        let db = Db::open_in_memory("test_basic_functionality");
159
160        let workspace_1 = SerializedWorkspace {
161            dock_anchor: Bottom,
162            dock_visible: true,
163            center_group: Default::default(),
164            dock_pane: Default::default(),
165        };
166
167        let workspace_2 = SerializedWorkspace {
168            dock_anchor: Expanded,
169            dock_visible: false,
170            center_group: Default::default(),
171            dock_pane: Default::default(),
172        };
173
174        let workspace_3 = SerializedWorkspace {
175            dock_anchor: Right,
176            dock_visible: true,
177            center_group: Default::default(),
178            dock_pane: Default::default(),
179        };
180
181        db.save_workspace(&["/tmp", "/tmp2"], None, &workspace_1);
182        db.save_workspace(&["/tmp"], None, &workspace_2);
183
184        db.write_to("test.db").unwrap();
185
186        // Test that paths are treated as a set
187        assert_eq!(
188            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
189            workspace_1
190        );
191        assert_eq!(
192            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
193            workspace_1
194        );
195
196        // Make sure that other keys work
197        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
198        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
199
200        // Test 'mutate' case of updating a pre-existing id
201        db.save_workspace(&["/tmp", "/tmp2"], Some(&["/tmp", "/tmp2"]), &workspace_2);
202        assert_eq!(
203            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
204            workspace_2
205        );
206
207        // Test other mechanism for mutating
208        db.save_workspace(&["/tmp", "/tmp2"], None, &workspace_3);
209        assert_eq!(
210            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
211            workspace_3
212        );
213
214        // Make sure that updating paths differently also works
215        db.save_workspace(
216            &["/tmp3", "/tmp4", "/tmp2"],
217            Some(&["/tmp", "/tmp2"]),
218            &workspace_3,
219        );
220        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
221        assert_eq!(
222            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
223                .unwrap(),
224            workspace_3
225        );
226    }
227}