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
29
30impl Domain for Workspace {
31 fn name() -> &'static str {
32 "workspace"
33 }
34
35 fn migrations() -> &'static [&'static str] {
36 &[indoc! {"
37 CREATE TABLE workspaces(
38 workspace_id BLOB PRIMARY KEY,
39 dock_visible INTEGER, -- Boolean
40 dock_anchor TEXT, -- Enum: 'Bottom' / 'Right' / 'Expanded'
41 dock_pane INTEGER, -- NULL indicates that we don't have a dock pane yet
42 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
43 FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
44 ) STRICT;
45
46 CREATE TABLE pane_groups(
47 group_id INTEGER PRIMARY KEY,
48 workspace_id BLOB NOT NULL,
49 parent_group_id INTEGER, -- NULL indicates that this is a root node
50 position INTEGER, -- NULL indicates that this is a root node
51 axis TEXT NOT NULL, -- Enum: 'Vertical' / 'Horizontal'
52 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
53 ON DELETE CASCADE
54 ON UPDATE CASCADE,
55 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
56 ) STRICT;
57
58 CREATE TABLE panes(
59 pane_id INTEGER PRIMARY KEY,
60 workspace_id BLOB NOT NULL,
61 active INTEGER NOT NULL, -- Boolean
62 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
63 ON DELETE CASCADE
64 ON UPDATE CASCADE
65 ) STRICT;
66
67 CREATE TABLE center_panes(
68 pane_id INTEGER PRIMARY KEY,
69 parent_group_id INTEGER, -- NULL means that this is a root pane
70 position INTEGER, -- NULL means that this is a root pane
71 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
72 ON DELETE CASCADE,
73 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
74 ) STRICT;
75
76 CREATE TABLE items(
77 item_id INTEGER NOT NULL, -- This is the item's view id, so this is not unique
78 workspace_id BLOB NOT NULL,
79 pane_id INTEGER NOT NULL,
80 kind TEXT NOT NULL,
81 position INTEGER NOT NULL,
82 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
83 ON DELETE CASCADE
84 ON UPDATE CASCADE,
85 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
86 ON DELETE CASCADE,
87 PRIMARY KEY(item_id, workspace_id)
88 ) STRICT;
89 "}]
90 }
91}
92
93impl WorkspaceDb {
94 /// Returns a serialized workspace for the given worktree_roots. If the passed array
95 /// is empty, the most recent workspace is returned instead. If no workspace for the
96 /// passed roots is stored, returns none.
97 pub fn workspace_for_roots<P: AsRef<Path>>(
98 &self,
99 worktree_roots: &[P],
100 ) -> Option<SerializedWorkspace> {
101 let workspace_id: WorkspaceId = worktree_roots.into();
102
103 // Note that we re-assign the workspace_id here in case it's empty
104 // and we've grabbed the most recent workspace
105 let (workspace_id, dock_position): (WorkspaceId, DockPosition) = iife!({
106 if worktree_roots.len() == 0 {
107 self.select_row(indoc! {"
108 SELECT workspace_id, dock_visible, dock_anchor
109 FROM workspaces
110 ORDER BY timestamp DESC LIMIT 1"})?()?
111 } else {
112 self.select_row_bound(indoc! {"
113 SELECT workspace_id, dock_visible, dock_anchor
114 FROM workspaces
115 WHERE workspace_id = ?"})?(&workspace_id)?
116 }
117 .context("No workspaces found")
118 })
119 .warn_on_err()
120 .flatten()?;
121
122 Some(SerializedWorkspace {
123 workspace_id: workspace_id.clone(),
124 dock_pane: self
125 .get_dock_pane(&workspace_id)
126 .context("Getting dock pane")
127 .log_err()?,
128 center_group: self
129 .get_center_pane_group(&workspace_id)
130 .context("Getting center group")
131 .log_err()?,
132 dock_position,
133 })
134 }
135
136 /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
137 /// that used this workspace previously
138 pub fn save_workspace(
139 &self,
140 old_id: Option<WorkspaceId>,
141 workspace: &SerializedWorkspace,
142 ) {
143 self.with_savepoint("update_worktrees", || {
144 self.exec_bound(indoc! {"
145 UPDATE workspaces SET dock_pane = NULL WHERE workspace_id = ?1;
146 DELETE FROM pane_groups WHERE workspace_id = ?1;
147 DELETE FROM panes WHERE workspace_id = ?1;"})?
148 (old_id.as_ref().unwrap_or(&workspace.workspace_id)).context("Clearing old panes")?;
149
150 if let Some(old_id) = old_id {
151 self.exec_bound(indoc! {"
152 UPDATE OR REPLACE workspaces
153 SET workspace_id = ?,
154 dock_visible = ?,
155 dock_anchor = ?,
156 timestamp = CURRENT_TIMESTAMP
157 WHERE workspace_id = ?"})?((
158 &workspace.workspace_id,
159 workspace.dock_position,
160 &old_id,
161 )).context("Updating workspace with new worktree roots")?;
162 } else {
163 self.exec_bound(
164 "INSERT OR REPLACE INTO workspaces(workspace_id, dock_visible, dock_anchor) VALUES (?, ?, ?)",
165 )?((&workspace.workspace_id, workspace.dock_position)).context("Uodating workspace")?;
166 }
167
168 // Save center pane group and dock pane
169 self.save_pane_group(&workspace.workspace_id, &workspace.center_group, None).context("save pane group in save workspace")?;
170
171 let dock_id = self.save_pane(&workspace.workspace_id, &workspace.dock_pane, None, true).context("save pane in save workspace")?;
172
173 // Complete workspace initialization
174 self.exec_bound(indoc! {"
175 UPDATE workspaces
176 SET dock_pane = ?
177 WHERE workspace_id = ?"})?((
178 dock_id,
179 &workspace.workspace_id,
180 )).context("Finishing initialization with dock pane")?;
181
182 Ok(())
183 })
184 .with_context(|| {
185 format!(
186 "Update workspace with roots {:?} failed.",
187 workspace.workspace_id.paths()
188 )
189 })
190 .log_err();
191 }
192
193 /// Returns the previous workspace ids sorted by last modified along with their opened worktree roots
194 pub fn recent_workspaces(&self, limit: usize) -> Vec<Arc<Vec<PathBuf>>> {
195 iife!({
196 // TODO, upgrade anyhow: https://docs.rs/anyhow/1.0.66/anyhow/fn.Ok.html
197 Ok::<_, anyhow::Error>(
198 self.select_bound::<usize, WorkspaceId>(
199 "SELECT workspace_id FROM workspaces ORDER BY timestamp DESC LIMIT ?",
200 )?(limit)?
201 .into_iter()
202 .map(|id| id.paths())
203 .collect::<Vec<Arc<Vec<PathBuf>>>>(),
204 )
205 })
206 .log_err()
207 .unwrap_or_default()
208 }
209
210 pub(crate) fn get_center_pane_group(
211 &self,
212 workspace_id: &WorkspaceId,
213 ) -> Result<SerializedPaneGroup> {
214 self.get_pane_group(workspace_id, None)?
215 .into_iter()
216 .next()
217 .context("No center pane group")
218 }
219
220 fn get_pane_group(
221 &self,
222 workspace_id: &WorkspaceId,
223 group_id: Option<GroupId>,
224 ) -> Result<Vec<SerializedPaneGroup>> {
225 type GroupKey<'a> = (Option<GroupId>, &'a WorkspaceId);
226 type GroupOrPane = (Option<GroupId>, Option<Axis>, Option<PaneId>, Option<bool>);
227 self.select_bound::<GroupKey, GroupOrPane>(indoc! {"
228 SELECT group_id, axis, pane_id, active
229 FROM (SELECT
230 group_id,
231 axis,
232 NULL as pane_id,
233 NULL as active,
234 position,
235 parent_group_id,
236 workspace_id
237 FROM pane_groups
238 UNION
239 SELECT
240 NULL,
241 NULL,
242 center_panes.pane_id,
243 panes.active as active,
244 position,
245 parent_group_id,
246 panes.workspace_id as workspace_id
247 FROM center_panes
248 JOIN panes ON center_panes.pane_id = panes.pane_id)
249 WHERE parent_group_id IS ? AND workspace_id = ?
250 ORDER BY position
251 "})?((group_id, workspace_id))?
252 .into_iter()
253 .map(|(group_id, axis, pane_id, active)| {
254 if let Some((group_id, axis)) = group_id.zip(axis) {
255 Ok(SerializedPaneGroup::Group {
256 axis,
257 children: self.get_pane_group(
258 workspace_id,
259 Some(group_id),
260 )?,
261 })
262 } else if let Some((pane_id, active)) = pane_id.zip(active) {
263 Ok(SerializedPaneGroup::Pane(SerializedPane::new(self.get_items( pane_id)?, active)))
264 } else {
265 bail!("Pane Group Child was neither a pane group or a pane");
266 }
267 })
268 // Filter out panes and pane groups which don't have any children or items
269 .filter(|pane_group| {
270 match pane_group {
271 Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
272 Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
273 _ => true,
274 }
275 })
276 .collect::<Result<_>>()
277 }
278
279 pub(crate) fn save_pane_group(
280 &self,
281 workspace_id: &WorkspaceId,
282 pane_group: &SerializedPaneGroup,
283 parent: Option<(GroupId, usize)>,
284 ) -> Result<()> {
285 match pane_group {
286 SerializedPaneGroup::Group { axis, children } => {
287 let (parent_id, position) = unzip_option(parent);
288
289 let group_id = self.select_row_bound::<_, i64>(indoc!{"
290 INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis)
291 VALUES (?, ?, ?, ?)
292 RETURNING group_id"})?
293 ((workspace_id, parent_id, position, *axis))?
294 .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
295
296 for (position, group) in children.iter().enumerate() {
297 self.save_pane_group(workspace_id, group, Some((group_id, position)))?
298 }
299 Ok(())
300 }
301 SerializedPaneGroup::Pane(pane) => {
302 self.save_pane(workspace_id, &pane, parent, false)?;
303 Ok(())
304 },
305 }
306 }
307
308 pub(crate) fn get_dock_pane(&self, workspace_id: &WorkspaceId) -> Result<SerializedPane> {
309 let (pane_id, active) = self.select_row_bound(indoc! {"
310 SELECT pane_id, active
311 FROM panes
312 WHERE pane_id = (SELECT dock_pane FROM workspaces WHERE workspace_id = ?)"})?(
313 workspace_id,
314 )?
315 .context("No dock pane for workspace")?;
316
317 Ok(SerializedPane::new(
318 self.get_items(pane_id).context("Reading items")?,
319 active
320 ))
321 }
322
323 pub(crate) fn save_pane(
324 &self,
325 workspace_id: &WorkspaceId,
326 pane: &SerializedPane,
327 parent: Option<(GroupId, usize)>, // None indicates BOTH dock pane AND center_pane
328 dock: bool,
329 ) -> Result<PaneId> {
330 let pane_id = self.select_row_bound::<_, i64>(indoc!{"
331 INSERT INTO panes(workspace_id, active)
332 VALUES (?, ?)
333 RETURNING pane_id"},
334 )?((workspace_id, pane.active))?
335 .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
336
337 if !dock {
338 let (parent_id, order) = unzip_option(parent);
339 self.exec_bound(indoc! {"
340 INSERT INTO center_panes(pane_id, parent_group_id, position)
341 VALUES (?, ?, ?)"})?((
342 pane_id, parent_id, order
343 ))?;
344 }
345
346 self.save_items(workspace_id, pane_id, &pane.children)
347 .context("Saving items")?;
348
349 Ok(pane_id)
350 }
351
352
353
354 pub(crate) fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
355 Ok(self.select_bound(indoc! {"
356 SELECT kind, item_id FROM items
357 WHERE pane_id = ?
358 ORDER BY position"})?(pane_id)?)
359 }
360
361 pub(crate) fn save_items(
362 &self,
363 workspace_id: &WorkspaceId,
364 pane_id: PaneId,
365 items: &[SerializedItem],
366 ) -> Result<()> {
367 let mut insert = self.exec_bound(
368 "INSERT INTO items(workspace_id, pane_id, position, kind, item_id) VALUES (?, ?, ?, ?, ?)",
369 ).context("Preparing insertion")?;
370 for (position, item) in items.iter().enumerate() {
371 insert((workspace_id, pane_id, position, item))?;
372 }
373
374 Ok(())
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use db::{open_memory_db};
381 use settings::DockAnchor;
382
383 use super::*;
384
385 #[test]
386 fn test_full_workspace_serialization() {
387 env_logger::try_init().ok();
388
389 let db = WorkspaceDb(open_memory_db(Some("test_full_workspace_serialization")));
390
391 let dock_pane = crate::persistence::model::SerializedPane {
392
393 children: vec![
394 SerializedItem::new("Terminal", 1),
395 SerializedItem::new("Terminal", 2),
396 SerializedItem::new("Terminal", 3),
397 SerializedItem::new("Terminal", 4),
398
399 ],
400 active: false
401 };
402
403 // -----------------
404 // | 1,2 | 5,6 |
405 // | - - - | |
406 // | 3,4 | |
407 // -----------------
408 let center_group = SerializedPaneGroup::Group {
409 axis: gpui::Axis::Horizontal,
410 children: vec![
411 SerializedPaneGroup::Group {
412 axis: gpui::Axis::Vertical,
413 children: vec![
414 SerializedPaneGroup::Pane(SerializedPane::new(
415 vec![
416 SerializedItem::new("Terminal", 5),
417 SerializedItem::new("Terminal", 6),
418 ],
419 false)
420 ),
421 SerializedPaneGroup::Pane(SerializedPane::new(
422 vec![
423 SerializedItem::new("Terminal", 7),
424 SerializedItem::new("Terminal", 8),
425 ],
426 false,
427 )),
428 ],
429 },
430 SerializedPaneGroup::Pane(SerializedPane::new(
431 vec![
432 SerializedItem::new("Terminal", 9),
433 SerializedItem::new("Terminal", 10),
434
435 ],
436 false,
437 )),
438 ],
439 };
440
441 let workspace = SerializedWorkspace {
442 workspace_id: (["/tmp", "/tmp2"]).into(),
443 dock_position: DockPosition::Shown(DockAnchor::Bottom),
444 center_group,
445 dock_pane,
446 };
447
448 db.save_workspace(None, &workspace);
449 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
450
451 assert_eq!(workspace, round_trip_workspace.unwrap());
452
453 // Test guaranteed duplicate IDs
454 db.save_workspace(None, &workspace);
455 db.save_workspace(None, &workspace);
456
457 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
458 assert_eq!(workspace, round_trip_workspace.unwrap());
459
460
461 }
462
463 #[test]
464 fn test_workspace_assignment() {
465 env_logger::try_init().ok();
466
467 let db = WorkspaceDb(open_memory_db(Some("test_basic_functionality")));
468
469 let workspace_1 = SerializedWorkspace {
470 workspace_id: (["/tmp", "/tmp2"]).into(),
471 dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
472 center_group: Default::default(),
473 dock_pane: Default::default(),
474 };
475
476 let mut workspace_2 = SerializedWorkspace {
477 workspace_id: (["/tmp"]).into(),
478 dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
479 center_group: Default::default(),
480 dock_pane: Default::default(),
481 };
482
483 db.save_workspace(None, &workspace_1);
484 db.save_workspace(None, &workspace_2);
485
486 // Test that paths are treated as a set
487 assert_eq!(
488 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
489 workspace_1
490 );
491 assert_eq!(
492 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
493 workspace_1
494 );
495
496 // Make sure that other keys work
497 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
498 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
499
500 // Test 'mutate' case of updating a pre-existing id
501 workspace_2.workspace_id = (["/tmp", "/tmp2"]).into();
502 db.save_workspace(Some((&["/tmp"]).into()), &workspace_2);
503 assert_eq!(
504 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
505 workspace_2
506 );
507
508 // Test other mechanism for mutating
509 let mut workspace_3 = SerializedWorkspace {
510 workspace_id: (&["/tmp", "/tmp2"]).into(),
511 dock_position: DockPosition::Shown(DockAnchor::Right),
512 center_group: Default::default(),
513 dock_pane: Default::default(),
514 };
515
516
517 db.save_workspace(None, &workspace_3);
518 assert_eq!(
519 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
520 workspace_3
521 );
522
523 // Make sure that updating paths differently also works
524 workspace_3.workspace_id = (["/tmp3", "/tmp4", "/tmp2"]).into();
525 db.save_workspace(
526 Some((&["/tmp", "/tmp2"]).into()),
527 &workspace_3,
528 );
529 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
530 assert_eq!(
531 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
532 .unwrap(),
533 workspace_3
534 );
535
536
537 }
538
539 use crate::dock::DockPosition;
540 use crate::persistence::model::SerializedWorkspace;
541 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
542
543 fn default_workspace<P: AsRef<Path>>(
544 workspace_id: &[P],
545 dock_pane: SerializedPane,
546 center_group: &SerializedPaneGroup,
547 ) -> SerializedWorkspace {
548 SerializedWorkspace {
549 workspace_id: workspace_id.into(),
550 dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
551 center_group: center_group.clone(),
552 dock_pane,
553 }
554 }
555
556 #[test]
557 fn test_basic_dock_pane() {
558 env_logger::try_init().ok();
559
560 let db = WorkspaceDb(open_memory_db(Some("basic_dock_pane")));
561
562 let dock_pane = crate::persistence::model::SerializedPane::new(
563 vec![
564 SerializedItem::new("Terminal", 1),
565 SerializedItem::new("Terminal", 4),
566 SerializedItem::new("Terminal", 2),
567 SerializedItem::new("Terminal", 3),
568 ], false
569 );
570
571 let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default());
572
573 db.save_workspace(None, &workspace);
574
575 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
576
577 assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
578 }
579
580 #[test]
581 fn test_simple_split() {
582 env_logger::try_init().ok();
583
584 let db = WorkspaceDb(open_memory_db(Some("simple_split")));
585
586 // -----------------
587 // | 1,2 | 5,6 |
588 // | - - - | |
589 // | 3,4 | |
590 // -----------------
591 let center_pane = SerializedPaneGroup::Group {
592 axis: gpui::Axis::Horizontal,
593 children: vec![
594 SerializedPaneGroup::Group {
595 axis: gpui::Axis::Vertical,
596 children: vec![
597 SerializedPaneGroup::Pane(SerializedPane::new(
598 vec![
599 SerializedItem::new("Terminal", 1),
600 SerializedItem::new("Terminal", 2),
601 ],
602 false)),
603 SerializedPaneGroup::Pane(SerializedPane::new(vec![
604 SerializedItem::new("Terminal", 4),
605 SerializedItem::new("Terminal", 3),
606 ], true)),
607 ],
608 },
609 SerializedPaneGroup::Pane(SerializedPane::new(
610 vec![
611 SerializedItem::new("Terminal", 5),
612 SerializedItem::new("Terminal", 6),
613 ],
614 false)),
615 ],
616 };
617
618 let workspace = default_workspace(&["/tmp"], Default::default(), ¢er_pane);
619
620 db.save_workspace(None, &workspace);
621
622 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
623
624 assert_eq!(workspace.center_group, new_workspace.center_group);
625 }
626
627 #[test]
628 fn test_cleanup_panes() {
629 env_logger::try_init().ok();
630
631 let db = WorkspaceDb(open_memory_db(Some("test_cleanup_panes")));
632
633 let center_pane = SerializedPaneGroup::Group {
634 axis: gpui::Axis::Horizontal,
635 children: vec![
636 SerializedPaneGroup::Group {
637 axis: gpui::Axis::Vertical,
638 children: vec![
639 SerializedPaneGroup::Pane(SerializedPane::new(
640 vec![
641 SerializedItem::new("Terminal", 1),
642 SerializedItem::new("Terminal", 2),
643 ],
644 false)),
645 SerializedPaneGroup::Pane(SerializedPane::new(vec![
646 SerializedItem::new("Terminal", 4),
647 SerializedItem::new("Terminal", 3),
648 ], true)),
649 ],
650 },
651 SerializedPaneGroup::Pane(SerializedPane::new(
652 vec![
653 SerializedItem::new("Terminal", 5),
654 SerializedItem::new("Terminal", 6),
655 ],
656 false)),
657 ],
658 };
659
660 let id = &["/tmp"];
661
662 let mut workspace = default_workspace(id, Default::default(), ¢er_pane);
663
664 db.save_workspace(None, &workspace);
665
666 workspace.center_group = SerializedPaneGroup::Group {
667 axis: gpui::Axis::Vertical,
668 children: vec![
669 SerializedPaneGroup::Pane(SerializedPane::new(
670 vec![
671 SerializedItem::new("Terminal", 1),
672 SerializedItem::new("Terminal", 2),
673 ],
674 false)),
675 SerializedPaneGroup::Pane(SerializedPane::new(vec![
676 SerializedItem::new("Terminal", 4),
677 SerializedItem::new("Terminal", 3),
678 ], true)),
679 ],
680 };
681
682 db.save_workspace(None, &workspace);
683
684 let new_workspace = db.workspace_for_roots(id).unwrap();
685
686 assert_eq!(workspace.center_group, new_workspace.center_group);
687
688 }
689}