workspace.rs

  1mod items;
  2pub mod model;
  3pub(crate) mod pane;
  4
  5use anyhow::{Context, Result};
  6use util::{iife, ResultExt};
  7
  8use std::path::{Path, PathBuf};
  9
 10use indoc::indoc;
 11use sqlez::migrations::Migration;
 12
 13// If you need to debug the worktree root code, change 'BLOB' here to 'TEXT' for easier debugging
 14// you might want to update some of the parsing code as well, I've left the variations in but commented
 15// out. This will panic if run on an existing db that has already been migrated
 16pub(crate) const WORKSPACES_MIGRATION: Migration = Migration::new(
 17    "workspace",
 18    &[indoc! {"
 19        CREATE TABLE workspaces(
 20            workspace_id BLOB PRIMARY KEY,
 21            dock_anchor TEXT, -- Enum: 'Bottom' / 'Right' / 'Expanded'
 22            dock_visible INTEGER, -- Boolean
 23            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL
 24        ) STRICT;
 25    "}],
 26);
 27
 28use self::model::{SerializedWorkspace, WorkspaceId, WorkspaceRow};
 29
 30use super::Db;
 31
 32impl Db {
 33    /// Returns a serialized workspace for the given worktree_roots. If the passed array
 34    /// is empty, the most recent workspace is returned instead. If no workspace for the
 35    /// passed roots is stored, returns none.
 36    pub fn workspace_for_roots<P: AsRef<Path>>(
 37        &self,
 38        worktree_roots: &[P],
 39    ) -> Option<SerializedWorkspace> {
 40        let workspace_id: WorkspaceId = worktree_roots.into();
 41
 42        let (_, dock_anchor, dock_visible) = iife!({
 43            if worktree_roots.len() == 0 {
 44                self.prepare(indoc! {"
 45                        SELECT workspace_id, dock_anchor, dock_visible 
 46                        FROM workspaces 
 47                        ORDER BY timestamp DESC LIMIT 1"})?
 48                    .maybe_row::<WorkspaceRow>()
 49            } else {
 50                self.prepare(indoc! {"
 51                        SELECT workspace_id, dock_anchor, dock_visible 
 52                        FROM workspaces 
 53                        WHERE workspace_id = ?"})?
 54                    .with_bindings(workspace_id)?
 55                    .maybe_row::<WorkspaceRow>()
 56            }
 57        })
 58        .log_err()
 59        .flatten()?;
 60
 61        Some(SerializedWorkspace {
 62            dock_pane: self.get_dock_pane(workspace_id)?,
 63            center_group: self.get_center_group(workspace_id),
 64            dock_anchor,
 65            dock_visible,
 66        })
 67    }
 68
 69    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
 70    /// that used this workspace previously
 71    pub fn save_workspace<P: AsRef<Path>>(
 72        &self,
 73        worktree_roots: &[P],
 74        workspace: SerializedWorkspace,
 75    ) {
 76        let workspace_id: WorkspaceId = worktree_roots.into();
 77
 78        self.with_savepoint("update_worktrees", |conn| {
 79            // Delete any previous workspaces with the same roots. This cascades to all
 80            // other tables that are based on the same roots set.
 81            // Insert new workspace into workspaces table if none were found
 82            self.prepare(indoc!{"
 83                DELETE FROM workspaces WHERE workspace_id = ?1;
 84                INSERT INTO workspaces(workspace_id, dock_anchor, dock_visible) VALUES (?1, ?, ?)"})?
 85            .with_bindings((workspace_id, workspace.dock_anchor, workspace.dock_visible))?
 86            .exec()?;
 87            
 88            // Save center pane group and dock pane
 89            Self::save_center_group(workspace_id, &workspace.center_group, conn)?;
 90            Self::save_dock_pane(workspace_id, &workspace.dock_pane, conn)?;
 91
 92            Ok(())
 93        })
 94        .with_context(|| format!("Update workspace with roots {:?}", worktree_roots.iter().map(|p| p.as_ref()).collect::<Vec<_>>()))
 95        .log_err();
 96    }
 97
 98    /// Returns the previous workspace ids sorted by last modified along with their opened worktree roots
 99    pub fn recent_workspaces(&self, limit: usize) -> Vec<Vec<PathBuf>> {
100        iife!({
101            self.prepare("SELECT workspace_id FROM workspaces ORDER BY timestamp DESC LIMIT ?")?
102                .with_bindings(limit)?
103                .rows::<WorkspaceId>()?
104                .into_iter().map(|id| id.0)
105                .collect()
106        }).log_err().unwrap_or_default()
107    }
108}
109
110#[cfg(test)]
111mod tests {
112
113    // use std::{path::PathBuf, thread::sleep, time::Duration};
114
115    // use crate::Db;
116
117    // use super::WorkspaceId;
118
119    // #[test]
120    // fn test_workspace_saving() {
121    //     env_logger::init();
122    //     let db = Db::open_in_memory("test_new_worktrees_for_roots");
123
124    //     // Test nothing returned with no roots at first
125    //     assert_eq!(db.workspace_for_roots::<String>(&[]), None);
126
127    //     // Test creation
128    //     let workspace_1 = db.workspace_for_roots::<String>(&[]);
129    //     assert_eq!(workspace_1.workspace_id, WorkspaceId(1));
130
131    //     // Ensure the timestamps are different
132    //     sleep(Duration::from_secs(1));
133    //     db.make_new_workspace::<String>(&[]);
134
135    //     // Test pulling another value from recent workspaces
136    //     let workspace_2 = db.workspace_for_roots::<String>(&[]);
137    //     assert_eq!(workspace_2.workspace_id, WorkspaceId(2));
138
139    //     // Ensure the timestamps are different
140    //     sleep(Duration::from_secs(1));
141
142    //     // Test creating a new workspace that doesn't exist already
143    //     let workspace_3 = db.workspace_for_roots(&["/tmp", "/tmp2"]);
144    //     assert_eq!(workspace_3.workspace_id, WorkspaceId(3));
145
146    //     // Make sure it's in the recent workspaces....
147    //     let workspace_3 = db.workspace_for_roots::<String>(&[]);
148    //     assert_eq!(workspace_3.workspace_id, WorkspaceId(3));
149
150    //     // And that it can be pulled out again
151    //     let workspace_3 = db.workspace_for_roots(&["/tmp", "/tmp2"]);
152    //     assert_eq!(workspace_3.workspace_id, WorkspaceId(3));
153    // }
154
155    // #[test]
156    // fn test_empty_worktrees() {
157    //     let db = Db::open_in_memory("test_empty_worktrees");
158
159    //     assert_eq!(None, db.workspace::<String>(&[]));
160
161    //     db.make_new_workspace::<String>(&[]); //ID 1
162    //     db.make_new_workspace::<String>(&[]); //ID 2
163    //     db.update_worktrees(&WorkspaceId(1), &["/tmp", "/tmp2"]);
164
165    //     // Sanity check
166    //     assert_eq!(db.workspace(&["/tmp", "/tmp2"]).unwrap().0, WorkspaceId(1));
167
168    //     db.update_worktrees::<String>(&WorkspaceId(1), &[]);
169
170    //     // Make sure 'no worktrees' fails correctly. returning [1, 2] from this
171    //     // call would be semantically correct (as those are the workspaces that
172    //     // don't have roots) but I'd prefer that this API to either return exactly one
173    //     // workspace, and None otherwise
174    //     assert_eq!(db.workspace::<String>(&[]), None,);
175
176    //     assert_eq!(db.last_workspace().unwrap().0, WorkspaceId(1));
177
178    //     assert_eq!(
179    //         db.recent_workspaces(2),
180    //         vec![Vec::<PathBuf>::new(), Vec::<PathBuf>::new()],
181    //     )
182    // }
183
184    // #[test]
185    // fn test_more_workspace_ids() {
186    //     let data = &[
187    //         (WorkspaceId(1), vec!["/tmp1"]),
188    //         (WorkspaceId(2), vec!["/tmp1", "/tmp2"]),
189    //         (WorkspaceId(3), vec!["/tmp1", "/tmp2", "/tmp3"]),
190    //         (WorkspaceId(4), vec!["/tmp2", "/tmp3"]),
191    //         (WorkspaceId(5), vec!["/tmp2", "/tmp3", "/tmp4"]),
192    //         (WorkspaceId(6), vec!["/tmp2", "/tmp4"]),
193    //         (WorkspaceId(7), vec!["/tmp2"]),
194    //     ];
195
196    //     let db = Db::open_in_memory("test_more_workspace_ids");
197
198    //     for (workspace_id, entries) in data {
199    //         db.make_new_workspace::<String>(&[]);
200    //         db.update_worktrees(workspace_id, entries);
201    //     }
202
203    //     assert_eq!(WorkspaceId(1), db.workspace(&["/tmp1"]).unwrap().0);
204    //     assert_eq!(db.workspace(&["/tmp1", "/tmp2"]).unwrap().0, WorkspaceId(2));
205    //     assert_eq!(
206    //         db.workspace(&["/tmp1", "/tmp2", "/tmp3"]).unwrap().0,
207    //         WorkspaceId(3)
208    //     );
209    //     assert_eq!(db.workspace(&["/tmp2", "/tmp3"]).unwrap().0, WorkspaceId(4));
210    //     assert_eq!(
211    //         db.workspace(&["/tmp2", "/tmp3", "/tmp4"]).unwrap().0,
212    //         WorkspaceId(5)
213    //     );
214    //     assert_eq!(db.workspace(&["/tmp2", "/tmp4"]).unwrap().0, WorkspaceId(6));
215    //     assert_eq!(db.workspace(&["/tmp2"]).unwrap().0, WorkspaceId(7));
216
217    //     assert_eq!(db.workspace(&["/tmp1", "/tmp5"]), None);
218    //     assert_eq!(db.workspace(&["/tmp5"]), None);
219    //     assert_eq!(db.workspace(&["/tmp2", "/tmp3", "/tmp4", "/tmp5"]), None);
220    // }
221
222    // #[test]
223    // fn test_detect_workspace_id() {
224    //     let data = &[
225    //         (WorkspaceId(1), vec!["/tmp"]),
226    //         (WorkspaceId(2), vec!["/tmp", "/tmp2"]),
227    //         (WorkspaceId(3), vec!["/tmp", "/tmp2", "/tmp3"]),
228    //     ];
229
230    //     let db = Db::open_in_memory("test_detect_workspace_id");
231
232    //     for (workspace_id, entries) in data {
233    //         db.make_new_workspace::<String>(&[]);
234    //         db.update_worktrees(workspace_id, entries);
235    //     }
236
237    //     assert_eq!(db.workspace(&["/tmp2"]), None);
238    //     assert_eq!(db.workspace(&["/tmp2", "/tmp3"]), None);
239    //     assert_eq!(db.workspace(&["/tmp"]).unwrap().0, WorkspaceId(1));
240    //     assert_eq!(db.workspace(&["/tmp", "/tmp2"]).unwrap().0, WorkspaceId(2));
241    //     assert_eq!(
242    //         db.workspace(&["/tmp", "/tmp2", "/tmp3"]).unwrap().0,
243    //         WorkspaceId(3)
244    //     );
245    // }
246
247    // #[test]
248    // fn test_tricky_overlapping_updates() {
249    //     // DB state:
250    //     // (/tree) -> ID: 1
251    //     // (/tree, /tree2) -> ID: 2
252    //     // (/tree2, /tree3) -> ID: 3
253
254    //     // -> User updates 2 to: (/tree2, /tree3)
255
256    //     // DB state:
257    //     // (/tree) -> ID: 1
258    //     // (/tree2, /tree3) -> ID: 2
259    //     // Get rid of 3 for garbage collection
260
261    //     let data = &[
262    //         (WorkspaceId(1), vec!["/tmp"]),
263    //         (WorkspaceId(2), vec!["/tmp", "/tmp2"]),
264    //         (WorkspaceId(3), vec!["/tmp2", "/tmp3"]),
265    //     ];
266
267    //     let db = Db::open_in_memory("test_tricky_overlapping_update");
268
269    //     // Load in the test data
270    //     for (workspace_id, entries) in data {
271    //         db.make_new_workspace::<String>(&[]);
272    //         db.update_worktrees(workspace_id, entries);
273    //     }
274
275    //     sleep(Duration::from_secs(1));
276    //     // Execute the update
277    //     db.update_worktrees(&WorkspaceId(2), &["/tmp2", "/tmp3"]);
278
279    //     // Make sure that workspace 3 doesn't exist
280    //     assert_eq!(db.workspace(&["/tmp2", "/tmp3"]).unwrap().0, WorkspaceId(2));
281
282    //     // And that workspace 1 was untouched
283    //     assert_eq!(db.workspace(&["/tmp"]).unwrap().0, WorkspaceId(1));
284
285    //     // And that workspace 2 is no longer registered under these roots
286    //     assert_eq!(db.workspace(&["/tmp", "/tmp2"]), None);
287
288    //     assert_eq!(db.last_workspace().unwrap().0, WorkspaceId(2));
289
290    //     let recent_workspaces = db.recent_workspaces(10);
291    //     assert_eq!(
292    //         recent_workspaces.get(0).unwrap(),
293    //         &vec![PathBuf::from("/tmp2"), PathBuf::from("/tmp3")]
294    //     );
295    //     assert_eq!(
296    //         recent_workspaces.get(1).unwrap(),
297    //         &vec![PathBuf::from("/tmp")]
298    //     );
299    // }
300}