1mod 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.get_dock_pane(&workspace_id)?,
62 center_group: self.get_center_group(&workspace_id),
63 dock_anchor,
64 dock_visible,
65 })
66 }
67
68 /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
69 /// that used this workspace previously
70 pub fn save_workspace<P: AsRef<Path>>(
71 &self,
72 worktree_roots: &[P],
73 workspace: SerializedWorkspace,
74 ) {
75 let workspace_id: WorkspaceId = worktree_roots.into();
76
77 self.with_savepoint("update_worktrees", |conn| {
78 // Delete any previous workspaces with the same roots. This cascades to all
79 // other tables that are based on the same roots set.
80 // Insert new workspace into workspaces table if none were found
81 self.prepare(indoc!{"
82 DELETE FROM workspaces WHERE workspace_id = ?1;
83 INSERT INTO workspaces(workspace_id, dock_anchor, dock_visible) VALUES (?1, ?, ?)"})?
84 .with_bindings((&workspace_id, workspace.dock_anchor, workspace.dock_visible))?
85 .exec()?;
86
87 // Save center pane group and dock pane
88 Self::save_center_group(&workspace_id, &workspace.center_group, conn)?;
89 Self::save_dock_pane(&workspace_id, &workspace.dock_pane, conn)?;
90
91 Ok(())
92 })
93 .with_context(|| format!("Update workspace with roots {:?}", worktree_roots.iter().map(|p| p.as_ref()).collect::<Vec<_>>()))
94 .log_err();
95 }
96
97 /// Returns the previous workspace ids sorted by last modified along with their opened worktree roots
98 pub fn recent_workspaces(&self, limit: usize) -> Vec<Vec<PathBuf>> {
99 iife!({
100 Ok::<_, anyhow::Error>(self.prepare("SELECT workspace_id FROM workspaces ORDER BY timestamp DESC LIMIT ?")?
101 .with_bindings(limit)?
102 .rows::<WorkspaceId>()?
103 .into_iter().map(|id| id.0)
104 .collect::<Vec<Vec<PathBuf>>>())
105
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}