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}