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_children(workspace_id, None)?
215 .into_iter()
216 .next()
217 .context("No center pane group")
218 }
219
220 fn get_pane_group_children(
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_children(
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 .collect::<Result<_>>()
269 }
270
271 pub(crate) fn save_pane_group(
272 &self,
273 workspace_id: &WorkspaceId,
274 pane_group: &SerializedPaneGroup,
275 parent: Option<(GroupId, usize)>,
276 ) -> Result<()> {
277 match pane_group {
278 SerializedPaneGroup::Group { axis, children } => {
279 let (parent_id, position) = unzip_option(parent);
280
281 let group_id = self.select_row_bound::<_, i64>(indoc!{"
282 INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis)
283 VALUES (?, ?, ?, ?)
284 RETURNING group_id"})?
285 ((workspace_id, parent_id, position, *axis))?
286 .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
287
288 for (position, group) in children.iter().enumerate() {
289 self.save_pane_group(workspace_id, group, Some((group_id, position)))?
290 }
291 Ok(())
292 }
293 SerializedPaneGroup::Pane(pane) => {
294 self.save_pane(workspace_id, &pane, parent, false)?;
295 Ok(())
296 },
297 }
298 }
299
300 pub(crate) fn get_dock_pane(&self, workspace_id: &WorkspaceId) -> Result<SerializedPane> {
301 let (pane_id, active) = self.select_row_bound(indoc! {"
302 SELECT pane_id, active
303 FROM panes
304 WHERE pane_id = (SELECT dock_pane FROM workspaces WHERE workspace_id = ?)"})?(
305 workspace_id,
306 )?
307 .context("No dock pane for workspace")?;
308
309 Ok(SerializedPane::new(
310 self.get_items(pane_id).context("Reading items")?,
311 active
312 ))
313 }
314
315 pub(crate) fn save_pane(
316 &self,
317 workspace_id: &WorkspaceId,
318 pane: &SerializedPane,
319 parent: Option<(GroupId, usize)>, // None indicates BOTH dock pane AND center_pane
320 dock: bool,
321 ) -> Result<PaneId> {
322 let pane_id = self.select_row_bound::<_, i64>(indoc!{"
323 INSERT INTO panes(workspace_id, active)
324 VALUES (?, ?)
325 RETURNING pane_id"},
326 )?((workspace_id, pane.active))?
327 .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
328
329 if !dock {
330 let (parent_id, order) = unzip_option(parent);
331 self.exec_bound(indoc! {"
332 INSERT INTO center_panes(pane_id, parent_group_id, position)
333 VALUES (?, ?, ?)"})?((
334 pane_id, parent_id, order
335 ))?;
336 }
337
338 self.save_items(workspace_id, pane_id, &pane.children)
339 .context("Saving items")?;
340
341 Ok(pane_id)
342 }
343
344
345
346 pub(crate) fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
347 Ok(self.select_bound(indoc! {"
348 SELECT kind, item_id FROM items
349 WHERE pane_id = ?
350 ORDER BY position"})?(pane_id)?)
351 }
352
353 pub(crate) fn save_items(
354 &self,
355 workspace_id: &WorkspaceId,
356 pane_id: PaneId,
357 items: &[SerializedItem],
358 ) -> Result<()> {
359 let mut insert = self.exec_bound(
360 "INSERT INTO items(workspace_id, pane_id, position, kind, item_id) VALUES (?, ?, ?, ?, ?)",
361 ).context("Preparing insertion")?;
362 for (position, item) in items.iter().enumerate() {
363 insert((workspace_id, pane_id, position, item))?;
364 }
365
366 Ok(())
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use db::{open_memory_db, write_db_to};
373 use settings::DockAnchor;
374
375 use super::*;
376
377 #[test]
378 fn test_full_workspace_serialization() {
379 env_logger::try_init().ok();
380
381 let db = WorkspaceDb(open_memory_db(Some("test_full_workspace_serialization")));
382
383 let dock_pane = crate::persistence::model::SerializedPane {
384
385 children: vec![
386 SerializedItem::new("Terminal", 1),
387 SerializedItem::new("Terminal", 2),
388 SerializedItem::new("Terminal", 3),
389 SerializedItem::new("Terminal", 4),
390
391 ],
392 active: false
393 };
394
395 // -----------------
396 // | 1,2 | 5,6 |
397 // | - - - | |
398 // | 3,4 | |
399 // -----------------
400 let center_group = SerializedPaneGroup::Group {
401 axis: gpui::Axis::Horizontal,
402 children: vec![
403 SerializedPaneGroup::Group {
404 axis: gpui::Axis::Vertical,
405 children: vec![
406 SerializedPaneGroup::Pane(SerializedPane::new(
407 vec![
408 SerializedItem::new("Terminal", 5),
409 SerializedItem::new("Terminal", 6),
410 ],
411 false)
412 ),
413 SerializedPaneGroup::Pane(SerializedPane::new(
414 vec![
415 SerializedItem::new("Terminal", 7),
416 SerializedItem::new("Terminal", 8),
417 ],
418 false,
419 )),
420 ],
421 },
422 SerializedPaneGroup::Pane(SerializedPane::new(
423 vec![
424 SerializedItem::new("Terminal", 9),
425 SerializedItem::new("Terminal", 10),
426
427 ],
428 false,
429 )),
430 ],
431 };
432
433 let workspace = SerializedWorkspace {
434 workspace_id: (["/tmp", "/tmp2"]).into(),
435 dock_position: DockPosition::Shown(DockAnchor::Bottom),
436 center_group,
437 dock_pane,
438 };
439
440 db.save_workspace(None, &workspace);
441 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
442
443 assert_eq!(workspace, round_trip_workspace.unwrap());
444
445 // Test guaranteed duplicate IDs
446 db.save_workspace(None, &workspace);
447 db.save_workspace(None, &workspace);
448
449 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
450 assert_eq!(workspace, round_trip_workspace.unwrap());
451
452
453 }
454
455 #[test]
456 fn test_workspace_assignment() {
457 env_logger::try_init().ok();
458
459 let db = WorkspaceDb(open_memory_db(Some("test_basic_functionality")));
460
461 let workspace_1 = SerializedWorkspace {
462 workspace_id: (["/tmp", "/tmp2"]).into(),
463 dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
464 center_group: Default::default(),
465 dock_pane: Default::default(),
466 };
467
468 let mut workspace_2 = SerializedWorkspace {
469 workspace_id: (["/tmp"]).into(),
470 dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
471 center_group: Default::default(),
472 dock_pane: Default::default(),
473 };
474
475 db.save_workspace(None, &workspace_1);
476 db.save_workspace(None, &workspace_2);
477
478 // Test that paths are treated as a set
479 assert_eq!(
480 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
481 workspace_1
482 );
483 assert_eq!(
484 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
485 workspace_1
486 );
487
488 // Make sure that other keys work
489 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
490 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
491
492 // Test 'mutate' case of updating a pre-existing id
493 workspace_2.workspace_id = (["/tmp", "/tmp2"]).into();
494 db.save_workspace(Some((&["/tmp"]).into()), &workspace_2);
495 assert_eq!(
496 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
497 workspace_2
498 );
499
500 // Test other mechanism for mutating
501 let mut workspace_3 = SerializedWorkspace {
502 workspace_id: (&["/tmp", "/tmp2"]).into(),
503 dock_position: DockPosition::Shown(DockAnchor::Right),
504 center_group: Default::default(),
505 dock_pane: Default::default(),
506 };
507
508
509 db.save_workspace(None, &workspace_3);
510 assert_eq!(
511 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
512 workspace_3
513 );
514
515 // Make sure that updating paths differently also works
516 workspace_3.workspace_id = (["/tmp3", "/tmp4", "/tmp2"]).into();
517 db.save_workspace(
518 Some((&["/tmp", "/tmp2"]).into()),
519 &workspace_3,
520 );
521 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
522 assert_eq!(
523 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
524 .unwrap(),
525 workspace_3
526 );
527
528
529 }
530
531 use crate::dock::DockPosition;
532 use crate::persistence::model::SerializedWorkspace;
533 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
534
535 fn default_workspace<P: AsRef<Path>>(
536 workspace_id: &[P],
537 dock_pane: SerializedPane,
538 center_group: &SerializedPaneGroup,
539 ) -> SerializedWorkspace {
540 SerializedWorkspace {
541 workspace_id: workspace_id.into(),
542 dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
543 center_group: center_group.clone(),
544 dock_pane,
545 }
546 }
547
548 #[test]
549 fn test_basic_dock_pane() {
550 env_logger::try_init().ok();
551
552 let db = WorkspaceDb(open_memory_db(Some("basic_dock_pane")));
553
554 let dock_pane = crate::persistence::model::SerializedPane::new(
555 vec![
556 SerializedItem::new("Terminal", 1),
557 SerializedItem::new("Terminal", 4),
558 SerializedItem::new("Terminal", 2),
559 SerializedItem::new("Terminal", 3),
560 ], false
561 );
562
563 let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default());
564
565 db.save_workspace(None, &workspace);
566 write_db_to(&db, "dest.db").unwrap();
567 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
568
569 assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
570 }
571
572 #[test]
573 fn test_simple_split() {
574 env_logger::try_init().ok();
575
576 let db = WorkspaceDb(open_memory_db(Some("simple_split")));
577
578 // -----------------
579 // | 1,2 | 5,6 |
580 // | - - - | |
581 // | 3,4 | |
582 // -----------------
583 let center_pane = SerializedPaneGroup::Group {
584 axis: gpui::Axis::Horizontal,
585 children: vec![
586 SerializedPaneGroup::Group {
587 axis: gpui::Axis::Vertical,
588 children: vec![
589 SerializedPaneGroup::Pane(SerializedPane::new(
590 vec![
591 SerializedItem::new("Terminal", 1),
592 SerializedItem::new("Terminal", 2),
593 ],
594 false)),
595 SerializedPaneGroup::Pane(SerializedPane::new(vec![
596 SerializedItem::new("Terminal", 4),
597 SerializedItem::new("Terminal", 3),
598 ], true)),
599 ],
600 },
601 SerializedPaneGroup::Pane(SerializedPane::new(
602 vec![
603 SerializedItem::new("Terminal", 5),
604 SerializedItem::new("Terminal", 6),
605 ],
606 false)),
607 ],
608 };
609
610 let workspace = default_workspace(&["/tmp"], Default::default(), ¢er_pane);
611
612 db.save_workspace(None, &workspace);
613
614 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
615
616 assert_eq!(workspace.center_group, new_workspace.center_group);
617 }
618
619 #[test]
620 fn test_cleanup_panes() {
621 env_logger::try_init().ok();
622
623 let db = WorkspaceDb(open_memory_db(Some("test_cleanup_panes")));
624
625 let center_pane = SerializedPaneGroup::Group {
626 axis: gpui::Axis::Horizontal,
627 children: vec![
628 SerializedPaneGroup::Group {
629 axis: gpui::Axis::Vertical,
630 children: vec![
631 SerializedPaneGroup::Pane(SerializedPane::new(
632 vec![
633 SerializedItem::new("Terminal", 1),
634 SerializedItem::new("Terminal", 2),
635 ],
636 false)),
637 SerializedPaneGroup::Pane(SerializedPane::new(vec![
638 SerializedItem::new("Terminal", 4),
639 SerializedItem::new("Terminal", 3),
640 ], true)),
641 ],
642 },
643 SerializedPaneGroup::Pane(SerializedPane::new(
644 vec![
645 SerializedItem::new("Terminal", 5),
646 SerializedItem::new("Terminal", 6),
647 ],
648 false)),
649 ],
650 };
651
652 let id = &["/tmp"];
653
654 let mut workspace = default_workspace(id, Default::default(), ¢er_pane);
655
656 db.save_workspace(None, &workspace);
657
658 workspace.center_group = SerializedPaneGroup::Group {
659 axis: gpui::Axis::Vertical,
660 children: vec![
661 SerializedPaneGroup::Pane(SerializedPane::new(
662 vec![
663 SerializedItem::new("Terminal", 1),
664 SerializedItem::new("Terminal", 2),
665 ],
666 false)),
667 SerializedPaneGroup::Pane(SerializedPane::new(vec![
668 SerializedItem::new("Terminal", 4),
669 SerializedItem::new("Terminal", 3),
670 ], true)),
671 ],
672 };
673
674 db.save_workspace(None, &workspace);
675
676 let new_workspace = db.workspace_for_roots(id).unwrap();
677
678 assert_eq!(workspace.center_group, new_workspace.center_group);
679
680 }
681}