workspace.rs

  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}