persistence.rs

  1#![allow(dead_code)]
  2
  3pub mod model;
  4
  5use std::path::{Path, PathBuf};
  6use std::sync::Arc;
  7
  8use anyhow::{anyhow, bail, Result, Context};
  9use db::connection;
 10use gpui::Axis;
 11use indoc::indoc;
 12use lazy_static::lazy_static;
 13
 14
 15use sqlez::domain::Domain;
 16use util::{iife, unzip_option, ResultExt};
 17
 18use crate::dock::DockPosition;
 19
 20use super::Workspace;
 21
 22use model::{
 23    GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
 24    SerializedWorkspace, WorkspaceId,
 25};
 26
 27connection!(DB: WorkspaceDb<Workspace>);
 28
 29impl Domain for Workspace {
 30    fn name() -> &'static str {
 31        "workspace"
 32    }
 33    
 34    fn migrations() -> &'static [&'static str] {
 35        &[indoc! {"
 36            CREATE TABLE workspaces(
 37                workspace_id BLOB PRIMARY KEY,
 38                dock_visible INTEGER, -- Boolean
 39                dock_anchor TEXT, -- Enum: 'Bottom' / 'Right' / 'Expanded'
 40                timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL
 41            ) STRICT;
 42            
 43            CREATE TABLE pane_groups(
 44                group_id INTEGER PRIMARY KEY,
 45                workspace_id BLOB NOT NULL,
 46                parent_group_id INTEGER, -- NULL indicates that this is a root node
 47                position INTEGER, -- NULL indicates that this is a root node
 48                axis TEXT NOT NULL, -- Enum:  'Vertical' / 'Horizontal'
 49                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) 
 50                    ON DELETE CASCADE 
 51                    ON UPDATE CASCADE,
 52                FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 53            ) STRICT;
 54            
 55            CREATE TABLE panes(
 56                pane_id INTEGER PRIMARY KEY,
 57                workspace_id BLOB NOT NULL,
 58                parent_group_id INTEGER, -- NULL means that this is a dock pane
 59                position INTEGER, -- NULL means that this is a dock pane
 60                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) 
 61                    ON DELETE CASCADE 
 62                    ON UPDATE CASCADE,
 63                FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
 64            ) STRICT;
 65            
 66            CREATE TABLE items(
 67                item_id INTEGER NOT NULL, -- This is the item's view id, so this is not unique
 68                workspace_id BLOB NOT NULL,
 69                pane_id INTEGER NOT NULL,
 70                kind TEXT NOT NULL,
 71                position INTEGER NOT NULL,
 72                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
 73                    ON DELETE CASCADE
 74                    ON UPDATE CASCADE,
 75                FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
 76                    ON DELETE CASCADE,
 77                PRIMARY KEY(item_id, workspace_id)
 78            ) STRICT;
 79        "}]
 80    }
 81}
 82
 83impl WorkspaceDb {
 84    /// Returns a serialized workspace for the given worktree_roots. If the passed array
 85    /// is empty, the most recent workspace is returned instead. If no workspace for the
 86    /// passed roots is stored, returns none.
 87    pub fn workspace_for_roots<P: AsRef<Path>>(
 88        &self,
 89        worktree_roots: &[P],
 90    ) -> Option<SerializedWorkspace> {
 91        let workspace_id: WorkspaceId = worktree_roots.into();
 92
 93        // Note that we re-assign the workspace_id here in case it's empty
 94        // and we've grabbed the most recent workspace
 95        let (workspace_id, dock_position): (WorkspaceId, DockPosition) = iife!({
 96            if worktree_roots.len() == 0 {
 97                self.select_row(indoc! {"
 98                    SELECT workspace_id, dock_visible, dock_anchor
 99                    FROM workspaces 
100                    ORDER BY timestamp DESC LIMIT 1"})?()?
101            } else {
102                self.select_row_bound(indoc! {"
103                    SELECT workspace_id, dock_visible, dock_anchor
104                    FROM workspaces 
105                    WHERE workspace_id = ?"})?(&workspace_id)?
106            }
107            .context("No workspaces found")
108        })
109        .warn_on_err()
110        .flatten()?;
111
112        Some(SerializedWorkspace {
113            workspace_id: workspace_id.clone(),
114            dock_pane: self
115                .get_dock_pane(&workspace_id)
116                .context("Getting dock pane")
117                .log_err()?,
118            center_group: self
119                .get_center_pane_group(&workspace_id)
120                .context("Getting center group")
121                .log_err()?,
122            dock_position,
123        })
124    }
125
126    /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
127    /// that used this workspace previously
128    pub fn save_workspace(
129        &self,
130        old_id: Option<WorkspaceId>,
131        workspace: &SerializedWorkspace,
132    ) {
133        self.with_savepoint("update_worktrees", || {
134            if let Some(old_id) = old_id {
135                self.exec_bound(indoc! {"
136                    DELETE FROM pane_groups WHERE workspace_id = ?"})?(&old_id)?;
137                
138                // If collision, delete
139                
140                self.exec_bound(indoc! {"
141                    UPDATE OR REPLACE workspaces
142                    SET workspace_id = ?,
143                        dock_visible = ?,
144                        dock_anchor = ?,
145                        timestamp = CURRENT_TIMESTAMP
146                    WHERE workspace_id = ?"})?((
147                    &workspace.workspace_id,
148                    workspace.dock_position,
149                    &old_id,
150                ))?;
151            } else {
152                self.exec_bound(indoc! {"
153                    DELETE FROM pane_groups WHERE workspace_id = ?"})?(&workspace.workspace_id)?;
154                self.exec_bound(
155                    "INSERT OR REPLACE INTO workspaces(workspace_id, dock_visible, dock_anchor) VALUES (?, ?, ?)",
156                )?((&workspace.workspace_id, workspace.dock_position))?;
157            }
158            
159            // Save center pane group and dock pane
160            self.save_pane_group(&workspace.workspace_id, &workspace.center_group, None)?;
161            self.save_pane(&workspace.workspace_id, &workspace.dock_pane, None)?;
162
163            Ok(())
164        })
165        .with_context(|| {
166            format!(
167                "Update workspace with roots {:?} failed.",
168                workspace.workspace_id.paths()
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<Arc<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<Arc<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            .map(|pane_group| {
200                // Rewrite the special case of the root being a leaf node
201                if let SerializedPaneGroup::Group { axis: Axis::Horizontal, ref children } = pane_group {
202                    if children.len() == 1 {
203                        if let Some(SerializedPaneGroup::Pane(pane)) = children.get(0) {
204                            return SerializedPaneGroup::Pane(pane.clone())
205                        }
206                    }
207                }
208                pane_group
209            })
210    }
211
212    fn get_pane_group_children<'a>(
213        &self,
214        workspace_id: &WorkspaceId,
215        group_id: Option<GroupId>,
216    ) -> Result<Vec<SerializedPaneGroup>> {
217        self.select_bound::<(Option<GroupId>, &WorkspaceId), (Option<GroupId>, Option<Axis>, Option<PaneId>)>(indoc! {"
218            SELECT group_id, axis, pane_id
219            FROM (SELECT group_id, axis, NULL as pane_id, position,  parent_group_id, workspace_id
220                  FROM pane_groups
221                 UNION
222                  SELECT NULL, NULL,  pane_id,  position,  parent_group_id, workspace_id
223                  FROM panes
224                  -- Remove the dock panes from the union
225                  WHERE parent_group_id IS NOT NULL and position IS NOT NULL) 
226            WHERE parent_group_id IS ? AND workspace_id = ?
227            ORDER BY position
228            "})?((group_id, workspace_id))?
229        .into_iter()
230        .map(|(group_id, axis, pane_id)| {
231            if let Some((group_id, axis)) = group_id.zip(axis) {
232                Ok(SerializedPaneGroup::Group {
233                    axis,
234                    children: self.get_pane_group_children(
235                        workspace_id,
236                        Some(group_id),
237                    )?,
238                })
239            } else if let Some(pane_id) = pane_id {
240                Ok(SerializedPaneGroup::Pane(SerializedPane {
241                    children: self.get_items( pane_id)?,
242                }))
243            } else {
244                bail!("Pane Group Child was neither a pane group or a pane");
245            }
246        })
247        .collect::<Result<_>>()
248    }
249
250    pub(crate) fn save_pane_group(
251        &self,
252        workspace_id: &WorkspaceId,
253        pane_group: &SerializedPaneGroup,
254        parent: Option<(GroupId, usize)>,
255    ) -> Result<()> {
256        // Rewrite the root node to fit with the database
257        let pane_group = if parent.is_none() && matches!(pane_group, SerializedPaneGroup::Pane { .. }) {
258            SerializedPaneGroup::Group { axis: Axis::Horizontal, children: vec![pane_group.clone()] }
259        } else {
260            pane_group.clone()
261        };
262
263        match pane_group {
264            SerializedPaneGroup::Group { axis, children } => {
265                let (parent_id, position) = unzip_option(parent);
266
267                let group_id = self.select_row_bound::<_, i64>(indoc!{"
268                    INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) 
269                    VALUES (?, ?, ?, ?) 
270                    RETURNING group_id"})?
271                    ((workspace_id, parent_id, position, axis))?
272                    .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
273                
274                for (position, group) in children.iter().enumerate() {
275                    self.save_pane_group(workspace_id, group, Some((group_id, position)))?
276                }
277                Ok(())
278            }
279            SerializedPaneGroup::Pane(pane) => {
280                self.save_pane(workspace_id, &pane, parent)
281            },
282        }
283    }
284
285    pub(crate) fn get_dock_pane(&self, workspace_id: &WorkspaceId) -> Result<SerializedPane> {
286        let pane_id = self.select_row_bound(indoc! {"
287            SELECT pane_id FROM panes 
288            WHERE workspace_id = ? AND parent_group_id IS NULL AND position IS NULL"})?(
289            workspace_id,
290        )?
291        .context("No dock pane for workspace")?;
292
293        Ok(SerializedPane::new(
294            self.get_items(pane_id).context("Reading items")?,
295        ))
296    }
297
298    pub(crate) fn save_pane(
299        &self,
300        workspace_id: &WorkspaceId,
301        pane: &SerializedPane,
302        parent: Option<(GroupId, usize)>,
303    ) -> Result<()> {
304        let (parent_id, order) = unzip_option(parent);
305        
306        let pane_id = self.select_row_bound::<_, i64>(indoc!{"
307            INSERT INTO panes(workspace_id, parent_group_id, position) 
308            VALUES (?, ?, ?) 
309            RETURNING pane_id"},
310        )?((workspace_id, parent_id, order))?
311        .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
312
313        self.save_items(workspace_id, pane_id, &pane.children)
314            .context("Saving items")
315    }
316
317    pub(crate) fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
318        Ok(self.select_bound(indoc! {"
319            SELECT kind, item_id FROM items
320            WHERE pane_id = ?
321            ORDER BY position"})?(pane_id)?)
322    }
323
324    pub(crate) fn save_items(
325        &self,
326        workspace_id: &WorkspaceId,
327        pane_id: PaneId,
328        items: &[SerializedItem],
329    ) -> Result<()> {
330        let mut insert = self.exec_bound(
331            "INSERT INTO items(workspace_id, pane_id, position, kind, item_id) VALUES (?, ?, ?, ?, ?)",
332        ).context("Preparing insertion")?;
333        for (position, item) in items.iter().enumerate() {
334            insert((workspace_id, pane_id, position, item))?;
335        }
336
337        Ok(())
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use db::{open_memory_db, write_db_to};
344    use settings::DockAnchor;
345
346    use super::*;
347
348    #[test]
349    fn test_full_workspace_serialization() {
350        env_logger::try_init().ok();
351
352        let db = WorkspaceDb(open_memory_db(Some("test_full_workspace_serialization")));
353
354        let dock_pane = crate::persistence::model::SerializedPane {
355            children: vec![
356                SerializedItem::new("Terminal", 1),
357                SerializedItem::new("Terminal", 2),
358                SerializedItem::new("Terminal", 3),
359                SerializedItem::new("Terminal", 4),
360
361            ],
362        };
363
364        //  -----------------
365        //  | 1,2   | 5,6   |
366        //  | - - - |       |
367        //  | 3,4   |       |
368        //  -----------------
369        let center_group = SerializedPaneGroup::Group {
370            axis: gpui::Axis::Horizontal,
371            children: vec![
372                SerializedPaneGroup::Group {
373                    axis: gpui::Axis::Vertical,
374                    children: vec![
375                        SerializedPaneGroup::Pane(SerializedPane {
376                            children: vec![
377                                SerializedItem::new("Terminal", 5),
378                                SerializedItem::new("Terminal", 6),
379                            ],
380                        }),
381                        SerializedPaneGroup::Pane(SerializedPane {
382                            children: vec![
383                                SerializedItem::new("Terminal", 7),
384                                SerializedItem::new("Terminal", 8),
385
386                            ],
387                        }),
388                    ],
389                },
390                SerializedPaneGroup::Pane(SerializedPane {
391                    children: vec![
392                        SerializedItem::new("Terminal", 9),
393                        SerializedItem::new("Terminal", 10),
394
395                    ],
396                }),
397            ],
398        };
399
400        let workspace = SerializedWorkspace {
401            workspace_id: (["/tmp", "/tmp2"]).into(),
402            dock_position:  DockPosition::Shown(DockAnchor::Bottom),
403            center_group,
404            dock_pane,
405        };
406        
407        db.save_workspace(None, &workspace);
408        let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
409        
410        assert_eq!(workspace, round_trip_workspace.unwrap());
411
412        // Test guaranteed duplicate IDs
413        db.save_workspace(None, &workspace);
414        db.save_workspace(None, &workspace);
415        
416        let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
417        assert_eq!(workspace, round_trip_workspace.unwrap());
418        
419        
420    }
421
422    #[test]
423    fn test_workspace_assignment() {
424        env_logger::try_init().ok();
425
426        let db = WorkspaceDb(open_memory_db(Some("test_basic_functionality")));
427
428        let workspace_1 = SerializedWorkspace {
429            workspace_id: (["/tmp", "/tmp2"]).into(),
430            dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
431            center_group: Default::default(),
432            dock_pane: Default::default(),
433        };
434
435        let mut workspace_2 = SerializedWorkspace {
436            workspace_id: (["/tmp"]).into(),
437            dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
438            center_group: Default::default(),
439            dock_pane: Default::default(),
440        };
441
442        db.save_workspace(None, &workspace_1);
443        db.save_workspace(None, &workspace_2);
444
445        // Test that paths are treated as a set
446        assert_eq!(
447            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
448            workspace_1
449        );
450        assert_eq!(
451            db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
452            workspace_1
453        );
454
455        // Make sure that other keys work
456        assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
457        assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
458
459        // Test 'mutate' case of updating a pre-existing id
460        workspace_2.workspace_id = (["/tmp", "/tmp2"]).into();
461        db.save_workspace(Some((&["/tmp"]).into()), &workspace_2);
462        assert_eq!(
463            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
464            workspace_2
465        );
466
467        // Test other mechanism for mutating
468        let mut workspace_3 = SerializedWorkspace {
469            workspace_id: (&["/tmp", "/tmp2"]).into(),
470            dock_position: DockPosition::Shown(DockAnchor::Right),
471            center_group: Default::default(),
472            dock_pane: Default::default(),
473        };
474
475        
476        db.save_workspace(None, &workspace_3);
477        assert_eq!(
478            db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
479            workspace_3
480        );
481
482        // Make sure that updating paths differently also works
483        workspace_3.workspace_id = (["/tmp3", "/tmp4", "/tmp2"]).into();
484        db.save_workspace(
485            Some((&["/tmp", "/tmp2"]).into()),
486            &workspace_3,
487        );
488        assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
489        assert_eq!(
490            db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
491                .unwrap(),
492            workspace_3
493        );
494        
495        
496    }
497
498    use crate::dock::DockPosition;
499    use crate::persistence::model::SerializedWorkspace;
500    use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
501
502    fn default_workspace<P: AsRef<Path>>(
503        workspace_id: &[P],
504        dock_pane: SerializedPane,
505        center_group: &SerializedPaneGroup,
506    ) -> SerializedWorkspace {
507        SerializedWorkspace {
508            workspace_id: workspace_id.into(),
509            dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
510            center_group: center_group.clone(),
511            dock_pane,
512        }
513    }
514
515    #[test]
516    fn test_basic_dock_pane() {
517        env_logger::try_init().ok();
518
519        let db = WorkspaceDb(open_memory_db(Some("basic_dock_pane")));
520
521        let dock_pane = crate::persistence::model::SerializedPane {
522            children: vec![
523                SerializedItem::new("Terminal", 1),
524                SerializedItem::new("Terminal", 4),
525                SerializedItem::new("Terminal", 2),
526                SerializedItem::new("Terminal", 3),
527            ],
528        };
529
530        let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default());
531
532        db.save_workspace(None, &workspace);
533        write_db_to(&db, "dest.db").unwrap();
534        let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
535
536        assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
537    }
538
539    #[test]
540    fn test_simple_split() {
541        // env_logger::try_init().ok();
542
543        let db = WorkspaceDb(open_memory_db(Some("simple_split")));
544
545        //  -----------------
546        //  | 1,2   | 5,6   |
547        //  | - - - |       |
548        //  | 3,4   |       |
549        //  -----------------
550        let center_pane = SerializedPaneGroup::Group {
551            axis: gpui::Axis::Horizontal,
552            children: vec![
553                SerializedPaneGroup::Group {
554                    axis: gpui::Axis::Vertical,
555                    children: vec![
556                        SerializedPaneGroup::Pane(SerializedPane {
557                            children: vec![
558                                SerializedItem::new("Terminal", 1),
559                                SerializedItem::new("Terminal", 2),
560                            ],
561                        }),
562                        SerializedPaneGroup::Pane(SerializedPane {
563                            children: vec![
564                                SerializedItem::new("Terminal", 4),
565                                SerializedItem::new("Terminal", 3),
566                            ],
567                        }),
568                    ],
569                },
570                SerializedPaneGroup::Pane(SerializedPane {
571                    children: vec![
572                        SerializedItem::new("Terminal", 5),
573                        SerializedItem::new("Terminal", 6),
574                    ],
575                }),
576            ],
577        };
578
579        let workspace = default_workspace(&["/tmp"], Default::default(), &center_pane);
580
581        db.save_workspace(None, &workspace);
582
583        assert_eq!(workspace.center_group, center_pane);
584    }
585}