1#![allow(dead_code)]
2
3pub mod model;
4
5use std::path::Path;
6
7use anyhow::{anyhow, bail, Context, Result};
8use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
9use gpui::Axis;
10
11use util::{unzip_option, ResultExt};
12
13use crate::dock::DockPosition;
14use crate::WorkspaceId;
15
16use model::{
17 GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
18 WorkspaceLocation,
19};
20
21define_connection! {
22 pub static ref DB: WorkspaceDb<()> =
23 &[sql!(
24 CREATE TABLE workspaces(
25 workspace_id INTEGER PRIMARY KEY,
26 workspace_location BLOB UNIQUE,
27 dock_visible INTEGER, // Boolean
28 dock_anchor TEXT, // Enum: 'Bottom' / 'Right' / 'Expanded'
29 dock_pane INTEGER, // NULL indicates that we don't have a dock pane yet
30 left_sidebar_open INTEGER, //Boolean
31 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
32 FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
33 ) STRICT;
34
35 CREATE TABLE pane_groups(
36 group_id INTEGER PRIMARY KEY,
37 workspace_id INTEGER NOT NULL,
38 parent_group_id INTEGER, // NULL indicates that this is a root node
39 position INTEGER, // NULL indicates that this is a root node
40 axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
41 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
42 ON DELETE CASCADE
43 ON UPDATE CASCADE,
44 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
45 ) STRICT;
46
47 CREATE TABLE panes(
48 pane_id INTEGER PRIMARY KEY,
49 workspace_id INTEGER NOT NULL,
50 active INTEGER NOT NULL, // Boolean
51 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
52 ON DELETE CASCADE
53 ON UPDATE CASCADE
54 ) STRICT;
55
56 CREATE TABLE center_panes(
57 pane_id INTEGER PRIMARY KEY,
58 parent_group_id INTEGER, // NULL means that this is a root pane
59 position INTEGER, // NULL means that this is a root pane
60 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
61 ON DELETE CASCADE,
62 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
63 ) STRICT;
64
65 CREATE TABLE items(
66 item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
67 workspace_id INTEGER NOT NULL,
68 pane_id INTEGER NOT NULL,
69 kind TEXT NOT NULL,
70 position INTEGER NOT NULL,
71 active 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
82impl WorkspaceDb {
83 /// Returns a serialized workspace for the given worktree_roots. If the passed array
84 /// is empty, the most recent workspace is returned instead. If no workspace for the
85 /// passed roots is stored, returns none.
86 pub fn workspace_for_roots<P: AsRef<Path>>(
87 &self,
88 worktree_roots: &[P],
89 ) -> Option<SerializedWorkspace> {
90 let workspace_location: WorkspaceLocation = worktree_roots.into();
91
92 // Note that we re-assign the workspace_id here in case it's empty
93 // and we've grabbed the most recent workspace
94 let (workspace_id, workspace_location, left_sidebar_open, dock_position): (
95 WorkspaceId,
96 WorkspaceLocation,
97 bool,
98 DockPosition,
99 ) =
100 self.select_row_bound(sql!{
101 SELECT workspace_id, workspace_location, left_sidebar_open, dock_visible, dock_anchor
102 FROM workspaces
103 WHERE workspace_location = ?
104 })
105 .and_then(|mut prepared_statement| (prepared_statement)(&workspace_location))
106 .context("No workspaces found")
107 .warn_on_err()
108 .flatten()?;
109
110 Some(SerializedWorkspace {
111 id: workspace_id,
112 location: workspace_location.clone(),
113 dock_pane: self
114 .get_dock_pane(workspace_id)
115 .context("Getting dock pane")
116 .log_err()?,
117 center_group: self
118 .get_center_pane_group(workspace_id)
119 .context("Getting center group")
120 .log_err()?,
121 dock_position,
122 left_sidebar_open,
123 })
124 }
125
126 /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
127 /// that used this workspace previously
128 pub async fn save_workspace(&self, workspace: SerializedWorkspace) {
129 self.write(move |conn| {
130 conn.with_savepoint("update_worktrees", || {
131 // Clear out panes and pane_groups
132 conn.exec_bound(sql!(
133 UPDATE workspaces SET dock_pane = NULL WHERE workspace_id = ?1;
134 DELETE FROM pane_groups WHERE workspace_id = ?1;
135 DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
136 .expect("Clearing old panes");
137
138 conn.exec_bound(sql!(
139 DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?
140 ))?((&workspace.location, workspace.id.clone()))
141 .context("clearing out old locations")?;
142
143 // Upsert
144 conn.exec_bound(sql!(
145 INSERT INTO workspaces(
146 workspace_id,
147 workspace_location,
148 left_sidebar_open,
149 dock_visible,
150 dock_anchor,
151 timestamp
152 )
153 VALUES (?1, ?2, ?3, ?4, ?5, CURRENT_TIMESTAMP)
154 ON CONFLICT DO
155 UPDATE SET
156 workspace_location = ?2,
157 left_sidebar_open = ?3,
158 dock_visible = ?4,
159 dock_anchor = ?5,
160 timestamp = CURRENT_TIMESTAMP
161 ))?((
162 workspace.id,
163 &workspace.location,
164 workspace.left_sidebar_open,
165 workspace.dock_position,
166 ))
167 .context("Updating workspace")?;
168
169 // Save center pane group and dock pane
170 Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
171 .context("save pane group in save workspace")?;
172
173 let dock_id = Self::save_pane(conn, workspace.id, &workspace.dock_pane, None, true)
174 .context("save pane in save workspace")?;
175
176 // Complete workspace initialization
177 conn.exec_bound(sql!(
178 UPDATE workspaces
179 SET dock_pane = ?
180 WHERE workspace_id = ?
181 ))?((dock_id, workspace.id))
182 .context("Finishing initialization with dock pane")?;
183
184 Ok(())
185 })
186 .log_err();
187 })
188 .await;
189 }
190
191 query! {
192 pub async fn next_id() -> Result<WorkspaceId> {
193 INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
194 }
195 }
196
197 query! {
198 fn recent_workspaces() -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
199 SELECT workspace_id, workspace_location
200 FROM workspaces
201 WHERE workspace_location IS NOT NULL
202 ORDER BY timestamp DESC
203 }
204 }
205
206 query! {
207 async fn delete_stale_workspace(id: WorkspaceId) -> Result<()> {
208 DELETE FROM workspaces
209 WHERE workspace_id IS ?
210 }
211 }
212
213 // Returns the recent locations which are still valid on disk and deletes ones which no longer
214 // exist.
215 pub async fn recent_workspaces_on_disk(&self) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
216 let mut result = Vec::new();
217 let mut delete_tasks = Vec::new();
218 for (id, location) in self.recent_workspaces()? {
219 if location.paths().iter().all(|path| dbg!(path).exists()) {
220 result.push((id, location));
221 } else {
222 delete_tasks.push(self.delete_stale_workspace(id));
223 }
224 }
225
226 futures::future::join_all(delete_tasks).await;
227 Ok(result)
228 }
229
230 query! {
231 pub fn last_workspace() -> Result<Option<WorkspaceLocation>> {
232 SELECT workspace_location
233 FROM workspaces
234 WHERE workspace_location IS NOT NULL
235 ORDER BY timestamp DESC
236 LIMIT 1
237 }
238 }
239
240 fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
241 Ok(self
242 .get_pane_group(workspace_id, None)?
243 .into_iter()
244 .next()
245 .unwrap_or_else(|| {
246 SerializedPaneGroup::Pane(SerializedPane {
247 active: true,
248 children: vec![],
249 })
250 }))
251 }
252
253 fn get_pane_group(
254 &self,
255 workspace_id: WorkspaceId,
256 group_id: Option<GroupId>,
257 ) -> Result<Vec<SerializedPaneGroup>> {
258 type GroupKey = (Option<GroupId>, WorkspaceId);
259 type GroupOrPane = (Option<GroupId>, Option<Axis>, Option<PaneId>, Option<bool>);
260 self.select_bound::<GroupKey, GroupOrPane>(sql!(
261 SELECT group_id, axis, pane_id, active
262 FROM (SELECT
263 group_id,
264 axis,
265 NULL as pane_id,
266 NULL as active,
267 position,
268 parent_group_id,
269 workspace_id
270 FROM pane_groups
271 UNION
272 SELECT
273 NULL,
274 NULL,
275 center_panes.pane_id,
276 panes.active as active,
277 position,
278 parent_group_id,
279 panes.workspace_id as workspace_id
280 FROM center_panes
281 JOIN panes ON center_panes.pane_id = panes.pane_id)
282 WHERE parent_group_id IS ? AND workspace_id = ?
283 ORDER BY position
284 ))?((group_id, workspace_id))?
285 .into_iter()
286 .map(|(group_id, axis, pane_id, active)| {
287 if let Some((group_id, axis)) = group_id.zip(axis) {
288 Ok(SerializedPaneGroup::Group {
289 axis,
290 children: self.get_pane_group(workspace_id, Some(group_id))?,
291 })
292 } else if let Some((pane_id, active)) = pane_id.zip(active) {
293 Ok(SerializedPaneGroup::Pane(SerializedPane::new(
294 self.get_items(pane_id)?,
295 active,
296 )))
297 } else {
298 bail!("Pane Group Child was neither a pane group or a pane");
299 }
300 })
301 // Filter out panes and pane groups which don't have any children or items
302 .filter(|pane_group| match pane_group {
303 Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
304 Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
305 _ => true,
306 })
307 .collect::<Result<_>>()
308 }
309
310 fn save_pane_group(
311 conn: &Connection,
312 workspace_id: WorkspaceId,
313 pane_group: &SerializedPaneGroup,
314 parent: Option<(GroupId, usize)>,
315 ) -> Result<()> {
316 match pane_group {
317 SerializedPaneGroup::Group { axis, children } => {
318 let (parent_id, position) = unzip_option(parent);
319
320 let group_id = conn.select_row_bound::<_, i64>(sql!(
321 INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis)
322 VALUES (?, ?, ?, ?)
323 RETURNING group_id
324 ))?((workspace_id, parent_id, position, *axis))?
325 .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
326
327 for (position, group) in children.iter().enumerate() {
328 Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
329 }
330
331 Ok(())
332 }
333 SerializedPaneGroup::Pane(pane) => {
334 Self::save_pane(conn, workspace_id, &pane, parent, false)?;
335 Ok(())
336 }
337 }
338 }
339
340 fn get_dock_pane(&self, workspace_id: WorkspaceId) -> Result<SerializedPane> {
341 let (pane_id, active) = self.select_row_bound(sql!(
342 SELECT pane_id, active
343 FROM panes
344 WHERE pane_id = (SELECT dock_pane FROM workspaces WHERE workspace_id = ?)
345 ))?(workspace_id)?
346 .context("No dock pane for workspace")?;
347
348 Ok(SerializedPane::new(
349 self.get_items(pane_id).context("Reading items")?,
350 active,
351 ))
352 }
353
354 fn save_pane(
355 conn: &Connection,
356 workspace_id: WorkspaceId,
357 pane: &SerializedPane,
358 parent: Option<(GroupId, usize)>, // None indicates BOTH dock pane AND center_pane
359 dock: bool,
360 ) -> Result<PaneId> {
361 let pane_id = conn.select_row_bound::<_, i64>(sql!(
362 INSERT INTO panes(workspace_id, active)
363 VALUES (?, ?)
364 RETURNING pane_id
365 ))?((workspace_id, pane.active))?
366 .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
367
368 if !dock {
369 let (parent_id, order) = unzip_option(parent);
370 conn.exec_bound(sql!(
371 INSERT INTO center_panes(pane_id, parent_group_id, position)
372 VALUES (?, ?, ?)
373 ))?((pane_id, parent_id, order))?;
374 }
375
376 Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
377
378 Ok(pane_id)
379 }
380
381 fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
382 Ok(self.select_bound(sql!(
383 SELECT kind, item_id, active FROM items
384 WHERE pane_id = ?
385 ORDER BY position
386 ))?(pane_id)?)
387 }
388
389 fn save_items(
390 conn: &Connection,
391 workspace_id: WorkspaceId,
392 pane_id: PaneId,
393 items: &[SerializedItem],
394 ) -> Result<()> {
395 let mut insert = conn.exec_bound(sql!(
396 INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?)
397 )).context("Preparing insertion")?;
398 for (position, item) in items.iter().enumerate() {
399 insert((workspace_id, pane_id, position, item))?;
400 }
401
402 Ok(())
403 }
404
405 query! {
406 pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
407 UPDATE workspaces
408 SET timestamp = CURRENT_TIMESTAMP
409 WHERE workspace_id = ?
410 }
411 }
412}
413
414#[cfg(test)]
415mod tests {
416
417 use std::sync::Arc;
418
419 use db::open_test_db;
420 use settings::DockAnchor;
421
422 use super::*;
423
424 #[gpui::test]
425 async fn test_next_id_stability() {
426 env_logger::try_init().ok();
427
428 let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
429
430 db.write(|conn| {
431 conn.migrate(
432 "test_table",
433 &[sql!(
434 CREATE TABLE test_table(
435 text TEXT,
436 workspace_id INTEGER,
437 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
438 ON DELETE CASCADE
439 ) STRICT;
440 )],
441 )
442 .unwrap();
443 })
444 .await;
445
446 let id = db.next_id().await.unwrap();
447 // Assert the empty row got inserted
448 assert_eq!(
449 Some(id),
450 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
451 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
452 ))
453 .unwrap()(id)
454 .unwrap()
455 );
456
457 db.write(move |conn| {
458 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
459 .unwrap()(("test-text-1", id))
460 .unwrap()
461 })
462 .await;
463
464 let test_text_1 = db
465 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
466 .unwrap()(1)
467 .unwrap()
468 .unwrap();
469 assert_eq!(test_text_1, "test-text-1");
470 }
471
472 #[gpui::test]
473 async fn test_workspace_id_stability() {
474 env_logger::try_init().ok();
475
476 let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
477
478 db.write(|conn| {
479 conn.migrate(
480 "test_table",
481 &[sql!(
482 CREATE TABLE test_table(
483 text TEXT,
484 workspace_id INTEGER,
485 FOREIGN KEY(workspace_id)
486 REFERENCES workspaces(workspace_id)
487 ON DELETE CASCADE
488 ) STRICT;)],
489 )
490 })
491 .await
492 .unwrap();
493
494 let mut workspace_1 = SerializedWorkspace {
495 id: 1,
496 location: (["/tmp", "/tmp2"]).into(),
497 dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
498 center_group: Default::default(),
499 dock_pane: Default::default(),
500 left_sidebar_open: true,
501 };
502
503 let mut workspace_2 = SerializedWorkspace {
504 id: 2,
505 location: (["/tmp"]).into(),
506 dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
507 center_group: Default::default(),
508 dock_pane: Default::default(),
509 left_sidebar_open: false,
510 };
511
512 db.save_workspace(workspace_1.clone()).await;
513
514 db.write(|conn| {
515 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
516 .unwrap()(("test-text-1", 1))
517 .unwrap();
518 })
519 .await;
520
521 db.save_workspace(workspace_2.clone()).await;
522
523 db.write(|conn| {
524 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
525 .unwrap()(("test-text-2", 2))
526 .unwrap();
527 })
528 .await;
529
530 workspace_1.location = (["/tmp", "/tmp3"]).into();
531 db.save_workspace(workspace_1.clone()).await;
532 db.save_workspace(workspace_1).await;
533
534 workspace_2.dock_pane.children.push(SerializedItem {
535 kind: Arc::from("Test"),
536 item_id: 10,
537 active: true,
538 });
539 db.save_workspace(workspace_2).await;
540
541 let test_text_2 = db
542 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
543 .unwrap()(2)
544 .unwrap()
545 .unwrap();
546 assert_eq!(test_text_2, "test-text-2");
547
548 let test_text_1 = db
549 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
550 .unwrap()(1)
551 .unwrap()
552 .unwrap();
553 assert_eq!(test_text_1, "test-text-1");
554 }
555
556 #[gpui::test]
557 async fn test_full_workspace_serialization() {
558 env_logger::try_init().ok();
559
560 let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
561
562 let dock_pane = crate::persistence::model::SerializedPane {
563 children: vec![
564 SerializedItem::new("Terminal", 1, false),
565 SerializedItem::new("Terminal", 2, false),
566 SerializedItem::new("Terminal", 3, true),
567 SerializedItem::new("Terminal", 4, false),
568 ],
569 active: false,
570 };
571
572 // -----------------
573 // | 1,2 | 5,6 |
574 // | - - - | |
575 // | 3,4 | |
576 // -----------------
577 let center_group = SerializedPaneGroup::Group {
578 axis: gpui::Axis::Horizontal,
579 children: vec![
580 SerializedPaneGroup::Group {
581 axis: gpui::Axis::Vertical,
582 children: vec![
583 SerializedPaneGroup::Pane(SerializedPane::new(
584 vec![
585 SerializedItem::new("Terminal", 5, false),
586 SerializedItem::new("Terminal", 6, true),
587 ],
588 false,
589 )),
590 SerializedPaneGroup::Pane(SerializedPane::new(
591 vec![
592 SerializedItem::new("Terminal", 7, true),
593 SerializedItem::new("Terminal", 8, false),
594 ],
595 false,
596 )),
597 ],
598 },
599 SerializedPaneGroup::Pane(SerializedPane::new(
600 vec![
601 SerializedItem::new("Terminal", 9, false),
602 SerializedItem::new("Terminal", 10, true),
603 ],
604 false,
605 )),
606 ],
607 };
608
609 let workspace = SerializedWorkspace {
610 id: 5,
611 location: (["/tmp", "/tmp2"]).into(),
612 dock_position: DockPosition::Shown(DockAnchor::Bottom),
613 center_group,
614 dock_pane,
615 left_sidebar_open: true,
616 };
617
618 db.save_workspace(workspace.clone()).await;
619 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
620
621 assert_eq!(workspace, round_trip_workspace.unwrap());
622
623 // Test guaranteed duplicate IDs
624 db.save_workspace(workspace.clone()).await;
625 db.save_workspace(workspace.clone()).await;
626
627 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
628 assert_eq!(workspace, round_trip_workspace.unwrap());
629 }
630
631 #[gpui::test]
632 async fn test_workspace_assignment() {
633 env_logger::try_init().ok();
634
635 let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
636
637 let workspace_1 = SerializedWorkspace {
638 id: 1,
639 location: (["/tmp", "/tmp2"]).into(),
640 dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
641 center_group: Default::default(),
642 dock_pane: Default::default(),
643 left_sidebar_open: true,
644 };
645
646 let mut workspace_2 = SerializedWorkspace {
647 id: 2,
648 location: (["/tmp"]).into(),
649 dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
650 center_group: Default::default(),
651 dock_pane: Default::default(),
652 left_sidebar_open: false,
653 };
654
655 db.save_workspace(workspace_1.clone()).await;
656 db.save_workspace(workspace_2.clone()).await;
657
658 // Test that paths are treated as a set
659 assert_eq!(
660 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
661 workspace_1
662 );
663 assert_eq!(
664 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
665 workspace_1
666 );
667
668 // Make sure that other keys work
669 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
670 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
671
672 // Test 'mutate' case of updating a pre-existing id
673 workspace_2.location = (["/tmp", "/tmp2"]).into();
674
675 db.save_workspace(workspace_2.clone()).await;
676 assert_eq!(
677 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
678 workspace_2
679 );
680
681 // Test other mechanism for mutating
682 let mut workspace_3 = SerializedWorkspace {
683 id: 3,
684 location: (&["/tmp", "/tmp2"]).into(),
685 dock_position: DockPosition::Shown(DockAnchor::Right),
686 center_group: Default::default(),
687 dock_pane: Default::default(),
688 left_sidebar_open: false,
689 };
690
691 db.save_workspace(workspace_3.clone()).await;
692 assert_eq!(
693 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
694 workspace_3
695 );
696
697 // Make sure that updating paths differently also works
698 workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
699 db.save_workspace(workspace_3.clone()).await;
700 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
701 assert_eq!(
702 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
703 .unwrap(),
704 workspace_3
705 );
706 }
707
708 use crate::dock::DockPosition;
709 use crate::persistence::model::SerializedWorkspace;
710 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
711
712 fn default_workspace<P: AsRef<Path>>(
713 workspace_id: &[P],
714 dock_pane: SerializedPane,
715 center_group: &SerializedPaneGroup,
716 ) -> SerializedWorkspace {
717 SerializedWorkspace {
718 id: 4,
719 location: workspace_id.into(),
720 dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
721 center_group: center_group.clone(),
722 dock_pane,
723 left_sidebar_open: true,
724 }
725 }
726
727 #[gpui::test]
728 async fn test_basic_dock_pane() {
729 env_logger::try_init().ok();
730
731 let db = WorkspaceDb(open_test_db("basic_dock_pane").await);
732
733 let dock_pane = crate::persistence::model::SerializedPane::new(
734 vec![
735 SerializedItem::new("Terminal", 1, false),
736 SerializedItem::new("Terminal", 4, false),
737 SerializedItem::new("Terminal", 2, false),
738 SerializedItem::new("Terminal", 3, true),
739 ],
740 false,
741 );
742
743 let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default());
744
745 db.save_workspace(workspace.clone()).await;
746
747 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
748
749 assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
750 }
751
752 #[gpui::test]
753 async fn test_simple_split() {
754 env_logger::try_init().ok();
755
756 let db = WorkspaceDb(open_test_db("simple_split").await);
757
758 // -----------------
759 // | 1,2 | 5,6 |
760 // | - - - | |
761 // | 3,4 | |
762 // -----------------
763 let center_pane = SerializedPaneGroup::Group {
764 axis: gpui::Axis::Horizontal,
765 children: vec![
766 SerializedPaneGroup::Group {
767 axis: gpui::Axis::Vertical,
768 children: vec![
769 SerializedPaneGroup::Pane(SerializedPane::new(
770 vec![
771 SerializedItem::new("Terminal", 1, false),
772 SerializedItem::new("Terminal", 2, true),
773 ],
774 false,
775 )),
776 SerializedPaneGroup::Pane(SerializedPane::new(
777 vec![
778 SerializedItem::new("Terminal", 4, false),
779 SerializedItem::new("Terminal", 3, true),
780 ],
781 true,
782 )),
783 ],
784 },
785 SerializedPaneGroup::Pane(SerializedPane::new(
786 vec![
787 SerializedItem::new("Terminal", 5, true),
788 SerializedItem::new("Terminal", 6, false),
789 ],
790 false,
791 )),
792 ],
793 };
794
795 let workspace = default_workspace(&["/tmp"], Default::default(), ¢er_pane);
796
797 db.save_workspace(workspace.clone()).await;
798
799 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
800
801 assert_eq!(workspace.center_group, new_workspace.center_group);
802 }
803
804 #[gpui::test]
805 async fn test_cleanup_panes() {
806 env_logger::try_init().ok();
807
808 let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
809
810 let center_pane = SerializedPaneGroup::Group {
811 axis: gpui::Axis::Horizontal,
812 children: vec![
813 SerializedPaneGroup::Group {
814 axis: gpui::Axis::Vertical,
815 children: vec![
816 SerializedPaneGroup::Pane(SerializedPane::new(
817 vec![
818 SerializedItem::new("Terminal", 1, false),
819 SerializedItem::new("Terminal", 2, true),
820 ],
821 false,
822 )),
823 SerializedPaneGroup::Pane(SerializedPane::new(
824 vec![
825 SerializedItem::new("Terminal", 4, false),
826 SerializedItem::new("Terminal", 3, true),
827 ],
828 true,
829 )),
830 ],
831 },
832 SerializedPaneGroup::Pane(SerializedPane::new(
833 vec![
834 SerializedItem::new("Terminal", 5, false),
835 SerializedItem::new("Terminal", 6, true),
836 ],
837 false,
838 )),
839 ],
840 };
841
842 let id = &["/tmp"];
843
844 let mut workspace = default_workspace(id, Default::default(), ¢er_pane);
845
846 db.save_workspace(workspace.clone()).await;
847
848 workspace.center_group = SerializedPaneGroup::Group {
849 axis: gpui::Axis::Vertical,
850 children: vec![
851 SerializedPaneGroup::Pane(SerializedPane::new(
852 vec![
853 SerializedItem::new("Terminal", 1, false),
854 SerializedItem::new("Terminal", 2, true),
855 ],
856 false,
857 )),
858 SerializedPaneGroup::Pane(SerializedPane::new(
859 vec![
860 SerializedItem::new("Terminal", 4, true),
861 SerializedItem::new("Terminal", 3, false),
862 ],
863 true,
864 )),
865 ],
866 };
867
868 db.save_workspace(workspace.clone()).await;
869
870 let new_workspace = db.workspace_for_roots(id).unwrap();
871
872 assert_eq!(workspace.center_group, new_workspace.center_group);
873 }
874}