1use anyhow::Result;
2use rusqlite::{params, Connection};
3
4use std::{
5 path::{Path, PathBuf},
6 sync::Arc,
7};
8
9use crate::pane::{PaneGroupId, PaneId, SerializedPane, SerializedPaneGroup};
10
11use super::Db;
12
13// TODO for workspace serialization:
14// - Update return types to unwrap all of the results into dummy values
15// - On database failure to initialize, delete the DB file
16// - Update paths to be blobs ( :( https://users.rust-lang.org/t/how-to-safely-store-a-path-osstring-in-a-sqllite-database/79712/10 )
17// - Convert hot paths to prepare-cache-execute style
18
19pub(crate) const WORKSPACE_M_1: &str = "
20CREATE TABLE workspaces(
21 workspace_id INTEGER PRIMARY KEY AUTOINCREMENT,
22 timestamp TEXT DEFAULT CURRENT_TIMESTAMP
23) STRICT;
24
25CREATE TABLE worktree_roots(
26 worktree_root TEXT NOT NULL,
27 workspace_id INTEGER NOT NULL,
28 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
29 PRIMARY KEY(worktree_root, workspace_id)
30) STRICT;
31";
32
33// Zed stores items with ids which are a combination of a view id during a given run and a workspace id. This
34
35// Case 1: Starting Zed Contextless
36// > Zed -> Reopen the last
37// Case 2: Starting Zed with a project folder
38// > Zed ~/projects/Zed
39// Case 3: Starting Zed with a file
40// > Zed ~/projects/Zed/cargo.toml
41// Case 4: Starting Zed with multiple project folders
42// > Zed ~/projects/Zed ~/projects/Zed.dev
43
44#[derive(Debug, PartialEq, Eq, Copy, Clone, Default)]
45pub struct WorkspaceId(i64);
46
47struct WorkspaceRow {
48 pub center_group_id: PaneGroupId,
49 pub dock_pane_id: PaneId,
50}
51
52#[derive(Default, Debug)]
53pub struct SerializedWorkspace {
54 pub workspace_id: WorkspaceId,
55 // pub center_group: SerializedPaneGroup,
56 // pub dock_pane: Option<SerializedPane>,
57}
58
59impl Db {
60 /// Finds or creates a workspace id for the given set of worktree roots. If the passed worktree roots is empty, return the
61 /// the last workspace id
62 pub fn workspace_for_worktree_roots(
63 &self,
64 worktree_roots: &[Arc<Path>],
65 ) -> SerializedWorkspace {
66 // Find the workspace id which is uniquely identified by this set of paths return it if found
67 if let Ok(Some(workspace_id)) = self.workspace_id(worktree_roots) {
68 // TODO
69 // let workspace_row = self.get_workspace_row(workspace_id);
70 // let center_group = self.get_pane_group(workspace_row.center_group_id);
71 // let dock_pane = self.get_pane(workspace_row.dock_pane_id);
72
73 SerializedWorkspace {
74 workspace_id,
75 // center_group,
76 // dock_pane: Some(dock_pane),
77 }
78 } else {
79 self.make_new_workspace()
80 }
81 }
82
83 fn make_new_workspace(&self) -> SerializedWorkspace {
84 self.real()
85 .map(|db| {
86 let lock = db.connection.lock();
87 match lock.execute("INSERT INTO workspaces DEFAULT VALUES;", []) {
88 Ok(_) => SerializedWorkspace {
89 workspace_id: WorkspaceId(lock.last_insert_rowid()),
90 },
91 Err(_) => Default::default(),
92 }
93 })
94 .unwrap_or_default()
95 }
96
97 fn workspace_id<P>(&self, worktree_roots: &[P]) -> Result<Option<WorkspaceId>>
98 where
99 P: AsRef<Path>,
100 {
101 self.real()
102 .map(|db| {
103 let lock = db.connection.lock();
104
105 get_workspace_id(worktree_roots, &lock)
106 })
107 .unwrap_or(Ok(None))
108 }
109
110 // fn get_workspace_row(&self, workspace_id: WorkspaceId) -> WorkspaceRow {
111 // unimplemented!()
112 // }
113
114 /// Updates the open paths for the given workspace id. Will garbage collect items from
115 /// any workspace ids which are no replaced by the new workspace id. Updates the timestamps
116 /// in the workspace id table
117 pub fn update_worktree_roots<P>(
118 &self,
119 workspace_id: &WorkspaceId,
120 worktree_roots: &[P],
121 ) -> Result<()>
122 where
123 P: AsRef<Path>,
124 {
125 self.real()
126 .map(|db| {
127 let mut lock = db.connection.lock();
128
129 let tx = lock.transaction()?;
130
131 {
132 // Lookup any old WorkspaceIds which have the same set of roots, and delete them.
133 let preexisting_id = get_workspace_id(worktree_roots, &tx)?;
134 if let Some(preexisting_id) = preexisting_id {
135 if preexisting_id != *workspace_id {
136 // Should also delete fields in other tables
137 tx.execute(
138 "DELETE FROM workspaces WHERE workspace_id = ?",
139 [preexisting_id.0],
140 )?;
141 }
142 }
143
144 tx.execute(
145 "DELETE FROM worktree_roots WHERE workspace_id = ?",
146 [workspace_id.0],
147 )?;
148
149 for root in worktree_roots {
150 // TODO: Update this to use blobs
151 let path = root.as_ref().to_string_lossy().to_string();
152
153 let mut stmt = tx.prepare_cached("INSERT INTO worktree_roots(workspace_id, worktree_root) VALUES (?, ?)")?;
154 stmt.execute(params![workspace_id.0, path])?;
155 }
156
157 let mut stmt = tx.prepare_cached("UPDATE workspaces SET timestamp = CURRENT_TIMESTAMP WHERE workspace_id = ?")?;
158 stmt.execute([workspace_id.0])?;
159 }
160 tx.commit()?;
161
162 Ok(())
163 })
164 .unwrap_or(Ok(()))
165 }
166
167 /// Returns the previous workspace ids sorted by last modified along with their opened worktree roots
168 pub fn recent_workspaces(&self, limit: usize) -> Result<Vec<(WorkspaceId, Vec<Arc<Path>>)>> {
169 // Return all the workspace ids and their associated paths ordered by the access timestamp
170 //ORDER BY timestamps
171 self.real()
172 .map(|db| {
173 let mut lock = db.connection.lock();
174
175 let tx = lock.transaction()?;
176 let result = {
177 let mut stmt = tx.prepare_cached(
178 "SELECT workspace_id FROM workspaces ORDER BY timestamp DESC LIMIT ?",
179 )?;
180 let workspace_ids = stmt
181 .query_map([limit], |row| Ok(WorkspaceId(row.get(0)?)))?
182 .collect::<Result<Vec<_>, rusqlite::Error>>()?;
183
184 let mut result = Vec::new();
185 let mut stmt = tx.prepare_cached(
186 "SELECT worktree_root FROM worktree_roots WHERE workspace_id = ?",
187 )?;
188 for workspace_id in workspace_ids {
189 let roots = stmt
190 .query_map([workspace_id.0], |row| {
191 let row = row.get::<_, String>(0)?;
192 Ok(PathBuf::from(Path::new(&row)).into())
193 })?
194 .collect::<Result<Vec<_>, rusqlite::Error>>()?;
195 result.push((workspace_id, roots))
196 }
197
198 result
199 };
200
201 tx.commit()?;
202
203 return Ok(result);
204 })
205 .unwrap_or_else(|| Ok(Vec::new()))
206 }
207}
208
209fn get_workspace_id<P>(
210 worktree_roots: &[P],
211 connection: &Connection,
212) -> Result<Option<WorkspaceId>, anyhow::Error>
213where
214 P: AsRef<Path>,
215{
216 // Prepare the array binding string. SQL doesn't have syntax for this, so
217 // we have to do it ourselves.
218 let mut array_binding_stmt = "(".to_string();
219 for i in 0..worktree_roots.len() {
220 array_binding_stmt.push_str(&format!("?{}", (i + 1))); //sqlite is 1-based
221 if i < worktree_roots.len() - 1 {
222 array_binding_stmt.push(',');
223 array_binding_stmt.push(' ');
224 }
225 }
226 array_binding_stmt.push(')');
227 // Any workspace can have multiple independent paths, and these paths
228 // can overlap in the database. Take this test data for example:
229 //
230 // [/tmp, /tmp2] -> 1
231 // [/tmp] -> 2
232 // [/tmp2, /tmp3] -> 3
233 //
234 // This would be stred in the database like so:
235 //
236 // ID PATH
237 // 1 /tmp
238 // 1 /tmp2
239 // 2 /tmp
240 // 3 /tmp2
241 // 3 /tmp3
242 //
243 // Note how both /tmp and /tmp2 are associated with multiple workspace IDs.
244 // So, given an array of worktree roots, how can we find the exactly matching ID?
245 // Let's analyze what happens when querying for [/tmp, /tmp2], from the inside out:
246 // - We start with a join of this table on itself, generating every possible
247 // pair of ((path, ID), (path, ID)), and filtering the join down to just the
248 // *overlapping* workspace IDs. For this small data set, this would look like:
249 //
250 // wt1.ID wt1.PATH | wt2.ID wt2.PATH
251 // 3 /tmp3 3 /tmp2
252 //
253 // - Moving one SELECT out, we use the first pair's ID column to invert the selection,
254 // meaning we now have a list of all the entries for our array and *subsets*
255 // of our array:
256 //
257 // ID PATH
258 // 1 /tmp
259 // 2 /tmp
260 // 2 /tmp2
261 //
262 // - To trim out the subsets, we need to exploit the fact that there can be no duplicate
263 // entries in this table. We can just use GROUP BY, COUNT, and a WHERE clause that checks
264 // for the length of our array:
265 //
266 // ID num_matching
267 // 1 2
268 //
269 // And we're done! We've found the matching ID correctly :D
270 // However, due to limitations in sqlite's query binding, we still have to do some string
271 // substitution to generate the correct query
272 let query = format!(
273 r#"
274 SELECT workspace_id
275 FROM (SELECT count(workspace_id) as num_matching, workspace_id FROM worktree_roots
276 WHERE worktree_root in {array_bind} AND workspace_id NOT IN
277 (SELECT wt1.workspace_id FROM worktree_roots as wt1
278 JOIN worktree_roots as wt2
279 ON wt1.workspace_id = wt2.workspace_id
280 WHERE wt1.worktree_root NOT in {array_bind} AND wt2.worktree_root in {array_bind})
281 GROUP BY workspace_id)
282 WHERE num_matching = ?
283 "#,
284 array_bind = array_binding_stmt
285 );
286 let mut stmt = connection.prepare_cached(&query)?;
287 // Make sure we bound the parameters correctly
288 debug_assert!(worktree_roots.len() + 1 == stmt.parameter_count());
289
290 for i in 0..worktree_roots.len() {
291 // TODO: Update this to use blobs
292 let path = &worktree_roots[i].as_ref().to_string_lossy().to_string();
293 stmt.raw_bind_parameter(i + 1, path)?
294 }
295 // No -1, because SQLite is 1 based
296 stmt.raw_bind_parameter(worktree_roots.len() + 1, worktree_roots.len())?;
297
298 let mut rows = stmt.raw_query();
299 if let Ok(Some(row)) = rows.next() {
300 return Ok(Some(WorkspaceId(row.get(0)?)));
301 }
302 // Ensure that this query only returns one row. The PRIMARY KEY constraint should catch this case
303 // but this is here to catch it if someone refactors that constraint out.
304 debug_assert!(matches!(rows.next(), Ok(None)));
305 Ok(None)
306}
307
308#[cfg(test)]
309mod tests {
310
311 use std::{
312 path::{Path, PathBuf},
313 sync::Arc,
314 thread::sleep,
315 time::Duration,
316 };
317
318 use crate::Db;
319
320 use super::WorkspaceId;
321
322 #[test]
323 fn test_more_workspace_ids() {
324 let data = &[
325 (WorkspaceId(1), vec!["/tmp1"]),
326 (WorkspaceId(2), vec!["/tmp1", "/tmp2"]),
327 (WorkspaceId(3), vec!["/tmp1", "/tmp2", "/tmp3"]),
328 (WorkspaceId(4), vec!["/tmp2", "/tmp3"]),
329 (WorkspaceId(5), vec!["/tmp2", "/tmp3", "/tmp4"]),
330 (WorkspaceId(6), vec!["/tmp2", "/tmp4"]),
331 (WorkspaceId(7), vec!["/tmp2"]),
332 ];
333
334 let db = Db::open_in_memory();
335
336 for (workspace_id, entries) in data {
337 db.make_new_workspace();
338 db.update_worktree_roots(workspace_id, entries).unwrap();
339 }
340
341 assert_eq!(Some(WorkspaceId(1)), db.workspace_id(&["/tmp1"]).unwrap());
342 assert_eq!(
343 db.workspace_id(&["/tmp1", "/tmp2"]).unwrap(),
344 Some(WorkspaceId(2))
345 );
346 assert_eq!(
347 db.workspace_id(&["/tmp1", "/tmp2", "/tmp3"]).unwrap(),
348 Some(WorkspaceId(3))
349 );
350 assert_eq!(
351 db.workspace_id(&["/tmp2", "/tmp3"]).unwrap(),
352 Some(WorkspaceId(4))
353 );
354 assert_eq!(
355 db.workspace_id(&["/tmp2", "/tmp3", "/tmp4"]).unwrap(),
356 Some(WorkspaceId(5))
357 );
358 assert_eq!(
359 db.workspace_id(&["/tmp2", "/tmp4"]).unwrap(),
360 Some(WorkspaceId(6))
361 );
362 assert_eq!(db.workspace_id(&["/tmp2"]).unwrap(), Some(WorkspaceId(7)));
363
364 assert_eq!(db.workspace_id(&["/tmp1", "/tmp5"]).unwrap(), None);
365 assert_eq!(db.workspace_id(&["/tmp5"]).unwrap(), None);
366 assert_eq!(
367 db.workspace_id(&["/tmp2", "/tmp3", "/tmp4", "/tmp5"])
368 .unwrap(),
369 None
370 );
371 }
372
373 #[test]
374 fn test_detect_workspace_id() {
375 let data = &[
376 (WorkspaceId(1), vec!["/tmp"]),
377 (WorkspaceId(2), vec!["/tmp", "/tmp2"]),
378 (WorkspaceId(3), vec!["/tmp", "/tmp2", "/tmp3"]),
379 ];
380
381 let db = Db::open_in_memory();
382
383 for (workspace_id, entries) in data {
384 db.make_new_workspace();
385 db.update_worktree_roots(workspace_id, entries).unwrap();
386 }
387
388 assert_eq!(db.workspace_id(&["/tmp2"]).unwrap(), None);
389 assert_eq!(db.workspace_id(&["/tmp2", "/tmp3"]).unwrap(), None);
390 assert_eq!(db.workspace_id(&["/tmp"]).unwrap(), Some(WorkspaceId(1)));
391 assert_eq!(
392 db.workspace_id(&["/tmp", "/tmp2"]).unwrap(),
393 Some(WorkspaceId(2))
394 );
395 assert_eq!(
396 db.workspace_id(&["/tmp", "/tmp2", "/tmp3"]).unwrap(),
397 Some(WorkspaceId(3))
398 );
399 }
400
401 fn arc_path(path: &'static str) -> Arc<Path> {
402 PathBuf::from(path).into()
403 }
404
405 #[test]
406 fn test_tricky_overlapping_updates() {
407 // DB state:
408 // (/tree) -> ID: 1
409 // (/tree, /tree2) -> ID: 2
410 // (/tree2, /tree3) -> ID: 3
411
412 // -> User updates 2 to: (/tree2, /tree3)
413
414 // DB state:
415 // (/tree) -> ID: 1
416 // (/tree2, /tree3) -> ID: 2
417 // Get rid of 3 for garbage collection
418
419 let data = &[
420 (WorkspaceId(1), vec!["/tmp"]),
421 (WorkspaceId(2), vec!["/tmp", "/tmp2"]),
422 (WorkspaceId(3), vec!["/tmp2", "/tmp3"]),
423 ];
424
425 let db = Db::open_in_memory();
426
427 // Load in the test data
428 for (workspace_id, entries) in data {
429 db.workspace_for_worktree_roots(&[]);
430 db.update_worktree_roots(workspace_id, entries).unwrap();
431 }
432
433 // Make sure the timestamp updates
434 sleep(Duration::from_secs(1));
435 // Execute the update
436 db.update_worktree_roots(&WorkspaceId(2), &["/tmp2", "/tmp3"])
437 .unwrap();
438
439 // Make sure that workspace 3 doesn't exist
440 assert_eq!(
441 db.workspace_id(&["/tmp2", "/tmp3"]).unwrap(),
442 Some(WorkspaceId(2))
443 );
444
445 // And that workspace 1 was untouched
446 assert_eq!(db.workspace_id(&["/tmp"]).unwrap(), Some(WorkspaceId(1)));
447
448 // And that workspace 2 is no longer registered under this
449 assert_eq!(db.workspace_id(&["/tmp", "/tmp2"]).unwrap(), None);
450
451 let recent_workspaces = db.recent_workspaces(10).unwrap();
452 assert_eq!(
453 recent_workspaces.get(0).unwrap(),
454 &(WorkspaceId(2), vec![arc_path("/tmp2"), arc_path("/tmp3")])
455 );
456 assert_eq!(
457 recent_workspaces.get(1).unwrap(),
458 &(WorkspaceId(1), vec![arc_path("/tmp")])
459 );
460 }
461}