persistence.rs

  1#![allow(dead_code)]
  2
  3pub mod model;
  4
  5use std::ops::Deref;
  6use std::path::{Path, PathBuf};
  7
  8use anyhow::{bail, Context, Result};
  9use db::open_file_db;
 10use gpui::Axis;
 11use indoc::indoc;
 12use lazy_static::lazy_static;
 13
 14use sqlez::thread_safe_connection::ThreadSafeConnection;
 15use sqlez::{connection::Connection, domain::Domain, migrations::Migration};
 16use util::{iife, unzip_option, ResultExt};
 17
 18use super::Workspace;
 19
 20use model::{
 21    GroupId, PaneId, SerializedItem, SerializedItemKind, SerializedPane, SerializedPaneGroup,
 22    SerializedWorkspace, WorkspaceId,
 23};
 24
 25lazy_static! {
 26    pub static ref DB: WorkspaceDb = WorkspaceDb(open_file_db());
 27}
 28
 29pub struct WorkspaceDb(ThreadSafeConnection<Workspace>);
 30
 31impl Deref for WorkspaceDb {
 32    type Target = ThreadSafeConnection<Workspace>;
 33
 34    fn deref(&self) -> &Self::Target {
 35        &self.0
 36    }
 37}
 38
 39pub(crate) const WORKSPACES_MIGRATION: Migration = Migration::new(
 40    "workspace",
 41    &[indoc! {"
 42        CREATE TABLE workspaces(
 43            workspace_id BLOB PRIMARY KEY,
 44            dock_anchor TEXT, -- Enum: 'Bottom' / 'Right' / 'Expanded'
 45            dock_visible INTEGER, -- Boolean
 46            timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL
 47        ) STRICT;
 48        
 49        CREATE TABLE pane_groups(
 50            group_id INTEGER PRIMARY KEY,
 51            workspace_id BLOB NOT NULL,
 52            parent_group_id INTEGER, -- NULL indicates that this is a root node
 53            position INTEGER, -- NULL indicates that this is a root node
 54            axis TEXT NOT NULL, -- Enum:  'Vertical' / 'Horizontal'
 55            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
 56            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 57        ) STRICT;
 58        
 59        CREATE TABLE panes(
 60            pane_id INTEGER PRIMARY KEY,
 61            workspace_id BLOB NOT NULL,
 62            parent_group_id INTEGER, -- NULL, this is a dock pane
 63            position INTEGER, -- NULL, this is a dock pane
 64            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
 65            FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 66        ) STRICT;
 67        
 68        CREATE TABLE items(
 69            item_id INTEGER NOT NULL, -- This is the item's view id, so this is not unique
 70            workspace_id BLOB NOT NULL,
 71            pane_id INTEGER NOT NULL,
 72            kind TEXT NOT NULL,
 73            position INTEGER NOT NULL,
 74            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
 75            FOREIGN KEY(pane_id) REFERENCES panes(pane_id) ON DELETE CASCADE
 76            PRIMARY KEY(item_id, workspace_id)
 77        ) STRICT;
 78    "}],
 79);
 80
 81impl Domain for Workspace {
 82    fn migrate(conn: &Connection) -> anyhow::Result<()> {
 83        WORKSPACES_MIGRATION.run(&conn)
 84    }
 85}
 86
 87impl WorkspaceDb {
 88    /// Returns a serialized workspace for the given worktree_roots. If the passed array
 89    /// is empty, the most recent workspace is returned instead. If no workspace for the
 90    /// passed roots is stored, returns none.
 91    pub fn workspace_for_roots<P: AsRef<Path>>(
 92        &self,
 93        worktree_roots: &[P],
 94    ) -> Option<SerializedWorkspace> {
 95        let workspace_id: WorkspaceId = worktree_roots.into();
 96
 97        // Note that we re-assign the workspace_id here in case it's empty
 98        // and we've grabbed the most recent workspace
 99        let (workspace_id, dock_anchor, dock_visible) = iife!({
100            if worktree_roots.len() == 0 {
101                self.select_row(indoc! {"
102                    SELECT workspace_id, dock_anchor, dock_visible 
103                    FROM workspaces 
104                    ORDER BY timestamp DESC LIMIT 1"})?()?
105            } else {
106                self.select_row_bound(indoc! {"
107                    SELECT workspace_id, dock_anchor, dock_visible 
108                    FROM workspaces 
109                    WHERE workspace_id = ?"})?(&workspace_id)?
110            }
111            .context("No workspaces found")
112        })
113        .warn_on_err()
114        .flatten()?;
115
116        Some(SerializedWorkspace {
117            dock_pane: self
118                .get_dock_pane(&workspace_id)
119                .context("Getting dock pane")
120                .log_err()?,
121            center_group: self
122                .get_center_pane_group(&workspace_id)
123                .context("Getting center group")
124                .log_err()?,
125            dock_anchor,
126            dock_visible,
127        })
128    }
129
130    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
131    /// that used this workspace previously
132    pub fn save_workspace<P: AsRef<Path>>(
133        &self,
134        worktree_roots: &[P],
135        old_roots: Option<&[P]>,
136        workspace: &SerializedWorkspace,
137    ) {
138        let workspace_id: WorkspaceId = worktree_roots.into();
139
140        self.with_savepoint("update_worktrees", || {
141            if let Some(old_roots) = old_roots {
142                let old_id: WorkspaceId = old_roots.into();
143
144                self.exec_bound("DELETE FROM WORKSPACES WHERE workspace_id = ?")?(&old_id)?;
145            }
146
147            // Delete any previous workspaces with the same roots. This cascades to all
148            // other tables that are based on the same roots set.
149            // Insert new workspace into workspaces table if none were found
150            self.exec_bound("DELETE FROM workspaces WHERE workspace_id = ?;")?(&workspace_id)?;
151
152            self.exec_bound(
153                "INSERT INTO workspaces(workspace_id, dock_anchor, dock_visible) VALUES (?, ?, ?)",
154            )?((&workspace_id, workspace.dock_anchor, workspace.dock_visible))?;
155
156            // Save center pane group and dock pane
157            self.save_pane_group(&workspace_id, &workspace.center_group, None)?;
158            self.save_pane(&workspace_id, &workspace.dock_pane, None)?;
159
160            Ok(())
161        })
162        .with_context(|| {
163            format!(
164                "Update workspace with roots {:?}",
165                worktree_roots
166                    .iter()
167                    .map(|p| p.as_ref())
168                    .collect::<Vec<_>>()
169            )
170        })
171        .log_err();
172    }
173
174    /// Returns the previous workspace ids sorted by last modified along with their opened worktree roots
175    pub fn recent_workspaces(&self, limit: usize) -> Vec<Vec<PathBuf>> {
176        iife!({
177            // TODO, upgrade anyhow: https://docs.rs/anyhow/1.0.66/anyhow/fn.Ok.html
178            Ok::<_, anyhow::Error>(
179                self.select_bound::<usize, WorkspaceId>(
180                    "SELECT workspace_id FROM workspaces ORDER BY timestamp DESC LIMIT ?",
181                )?(limit)?
182                .into_iter()
183                .map(|id| id.paths())
184                .collect::<Vec<Vec<PathBuf>>>(),
185            )
186        })
187        .log_err()
188        .unwrap_or_default()
189    }
190
191    pub(crate) fn get_center_pane_group(
192        &self,
193        workspace_id: &WorkspaceId,
194    ) -> Result<SerializedPaneGroup> {
195        self.get_pane_group_children(workspace_id, None)?
196            .into_iter()
197            .next()
198            .context("No center pane group")
199    }
200
201    fn get_pane_group_children<'a>(
202        &self,
203        workspace_id: &WorkspaceId,
204        group_id: Option<GroupId>,
205    ) -> Result<Vec<SerializedPaneGroup>> {
206        self.select_bound::<(Option<GroupId>, &WorkspaceId), (Option<GroupId>, Option<Axis>, Option<PaneId>)>(indoc! {"
207            SELECT group_id, axis, pane_id
208            FROM (SELECT group_id, axis, NULL as pane_id, position,  parent_group_id, workspace_id
209                  FROM pane_groups
210                 UNION
211                  SELECT NULL, NULL,  pane_id,  position,  parent_group_id, workspace_id
212                  FROM panes
213                  -- Remove the dock panes from the union
214                  WHERE parent_group_id IS NOT NULL and position IS NOT NULL) 
215            WHERE parent_group_id IS ? AND workspace_id = ?
216            ORDER BY position
217            "})?((group_id, workspace_id))?
218        .into_iter()
219        .map(|(group_id, axis, pane_id)| {
220            if let Some((group_id, axis)) = group_id.zip(axis) {
221                Ok(SerializedPaneGroup::Group {
222                    axis,
223                    children: self.get_pane_group_children(
224                        workspace_id,
225                        Some(group_id),
226                    )?,
227                })
228            } else if let Some(pane_id) = pane_id {
229                Ok(SerializedPaneGroup::Pane(SerializedPane {
230                    children: self.get_items( pane_id)?,
231                }))
232            } else {
233                bail!("Pane Group Child was neither a pane group or a pane");
234            }
235        })
236        .collect::<Result<_>>()
237    }
238
239    pub(crate) fn save_pane_group(
240        &self,
241        workspace_id: &WorkspaceId,
242        pane_group: &SerializedPaneGroup,
243        parent: Option<(GroupId, usize)>,
244    ) -> Result<()> {
245        if parent.is_none() && !matches!(pane_group, SerializedPaneGroup::Group { .. }) {
246            bail!("Pane groups must have a SerializedPaneGroup::Group at the root")
247        }
248
249        let (parent_id, position) = unzip_option(parent);
250
251        match pane_group {
252            SerializedPaneGroup::Group { axis, children } => {
253                let parent_id = self.insert_bound("INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) VALUES (?, ?, ?, ?)")?
254                    ((workspace_id, parent_id, position, *axis))?;
255
256                for (position, group) in children.iter().enumerate() {
257                    self.save_pane_group(workspace_id, group, Some((parent_id, position)))?
258                }
259                Ok(())
260            }
261            SerializedPaneGroup::Pane(pane) => self.save_pane(workspace_id, pane, parent),
262        }
263    }
264
265    pub(crate) fn get_dock_pane(&self, workspace_id: &WorkspaceId) -> Result<SerializedPane> {
266        let pane_id = self.select_row_bound(indoc! {"
267            SELECT pane_id FROM panes 
268            WHERE workspace_id = ? AND parent_group_id IS NULL AND position IS NULL"})?(
269            workspace_id,
270        )?
271        .context("No dock pane for workspace")?;
272
273        Ok(SerializedPane::new(
274            self.get_items(pane_id).context("Reading items")?,
275        ))
276    }
277
278    pub(crate) fn save_pane(
279        &self,
280        workspace_id: &WorkspaceId,
281        pane: &SerializedPane,
282        parent: Option<(GroupId, usize)>,
283    ) -> Result<()> {
284        let (parent_id, order) = unzip_option(parent);
285
286        let pane_id = self.insert_bound(
287            "INSERT INTO panes(workspace_id, parent_group_id, position) VALUES (?, ?, ?)",
288        )?((workspace_id, parent_id, order))?;
289
290        self.save_items(workspace_id, pane_id, &pane.children)
291            .context("Saving items")
292    }
293
294    pub(crate) fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
295        Ok(self.select_bound(indoc! {"
296            SELECT item_id, kind FROM items
297            WHERE pane_id = ?
298            ORDER BY position"})?(pane_id)?
299        .into_iter()
300        .map(|(item_id, kind)| match kind {
301            SerializedItemKind::Terminal => SerializedItem::Terminal { item_id },
302            _ => unimplemented!(),
303        })
304        .collect())
305    }
306
307    pub(crate) fn save_items(
308        &self,
309        workspace_id: &WorkspaceId,
310        pane_id: PaneId,
311        items: &[SerializedItem],
312    ) -> Result<()> {
313        let mut delete_old = self
314            .exec_bound("DELETE FROM items WHERE workspace_id = ? AND pane_id = ? AND item_id = ?")
315            .context("Preparing deletion")?;
316        let mut insert_new = self.exec_bound(
317            "INSERT INTO items(item_id, workspace_id, pane_id, kind, position) VALUES (?, ?, ?, ?, ?)",
318        ).context("Preparing insertion")?;
319        for (position, item) in items.iter().enumerate() {
320            delete_old((workspace_id, pane_id, item.item_id()))?;
321            insert_new((item.item_id(), workspace_id, pane_id, item.kind(), position))?;
322        }
323
324        Ok(())
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use db::open_memory_db;
331    use settings::DockAnchor;
332
333    use super::*;
334
335    #[test]
336    fn test_workspace_assignment() {
337        // env_logger::try_init().ok();
338
339        let db = WorkspaceDb(open_memory_db("test_basic_functionality"));
340
341        let workspace_1 = SerializedWorkspace {
342            dock_anchor: DockAnchor::Bottom,
343            dock_visible: true,
344            center_group: Default::default(),
345            dock_pane: Default::default(),
346        };
347
348        let workspace_2 = SerializedWorkspace {
349            dock_anchor: DockAnchor::Expanded,
350            dock_visible: false,
351            center_group: Default::default(),
352            dock_pane: Default::default(),
353        };
354
355        let workspace_3 = SerializedWorkspace {
356            dock_anchor: DockAnchor::Right,
357            dock_visible: true,
358            center_group: Default::default(),
359            dock_pane: Default::default(),
360        };
361
362        db.save_workspace(&["/tmp", "/tmp2"], None, &workspace_1);
363        db.save_workspace(&["/tmp"], None, &workspace_2);
364
365        db::write_db_to(&db, "test.db").unwrap();
366
367        // Test that paths are treated as a set
368        assert_eq!(
369            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
370            workspace_1
371        );
372        assert_eq!(
373            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
374            workspace_1
375        );
376
377        // Make sure that other keys work
378        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
379        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
380
381        // Test 'mutate' case of updating a pre-existing id
382        db.save_workspace(&["/tmp", "/tmp2"], Some(&["/tmp", "/tmp2"]), &workspace_2);
383        assert_eq!(
384            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
385            workspace_2
386        );
387
388        // Test other mechanism for mutating
389        db.save_workspace(&["/tmp", "/tmp2"], None, &workspace_3);
390        assert_eq!(
391            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
392            workspace_3
393        );
394
395        // Make sure that updating paths differently also works
396        db.save_workspace(
397            &["/tmp3", "/tmp4", "/tmp2"],
398            Some(&["/tmp", "/tmp2"]),
399            &workspace_3,
400        );
401        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
402        assert_eq!(
403            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
404                .unwrap(),
405            workspace_3
406        );
407    }
408
409    use crate::persistence::model::SerializedWorkspace;
410    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
411
412    fn default_workspace(
413        dock_pane: SerializedPane,
414        center_group: &SerializedPaneGroup,
415    ) -> SerializedWorkspace {
416        SerializedWorkspace {
417            dock_anchor: DockAnchor::Right,
418            dock_visible: false,
419            center_group: center_group.clone(),
420            dock_pane,
421        }
422    }
423
424    #[test]
425    fn test_basic_dock_pane() {
426        // env_logger::try_init().ok();
427
428        let db = WorkspaceDb(open_memory_db("basic_dock_pane"));
429
430        let dock_pane = crate::persistence::model::SerializedPane {
431            children: vec![
432                SerializedItem::Terminal { item_id: 1 },
433                SerializedItem::Terminal { item_id: 4 },
434                SerializedItem::Terminal { item_id: 2 },
435                SerializedItem::Terminal { item_id: 3 },
436            ],
437        };
438
439        let workspace = default_workspace(dock_pane, &Default::default());
440
441        db.save_workspace(&["/tmp"], None, &workspace);
442
443        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
444
445        assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
446    }
447
448    #[test]
449    fn test_simple_split() {
450        // env_logger::try_init().ok();
451
452        let db = WorkspaceDb(open_memory_db("simple_split"));
453
454        //  -----------------
455        //  | 1,2   | 5,6   |
456        //  | - - - |       |
457        //  | 3,4   |       |
458        //  -----------------
459        let center_pane = SerializedPaneGroup::Group {
460            axis: gpui::Axis::Horizontal,
461            children: vec![
462                SerializedPaneGroup::Group {
463                    axis: gpui::Axis::Vertical,
464                    children: vec![
465                        SerializedPaneGroup::Pane(SerializedPane {
466                            children: vec![
467                                SerializedItem::Terminal { item_id: 1 },
468                                SerializedItem::Terminal { item_id: 2 },
469                            ],
470                        }),
471                        SerializedPaneGroup::Pane(SerializedPane {
472                            children: vec![
473                                SerializedItem::Terminal { item_id: 4 },
474                                SerializedItem::Terminal { item_id: 3 },
475                            ],
476                        }),
477                    ],
478                },
479                SerializedPaneGroup::Pane(SerializedPane {
480                    children: vec![
481                        SerializedItem::Terminal { item_id: 5 },
482                        SerializedItem::Terminal { item_id: 6 },
483                    ],
484                }),
485            ],
486        };
487
488        let workspace = default_workspace(Default::default(), &center_pane);
489
490        db.save_workspace(&["/tmp"], None, &workspace);
491
492        assert_eq!(workspace.center_group, center_pane);
493    }
494}