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| path.exists())
220 && location.paths().iter().any(|path| path.is_dir())
221 {
222 result.push((id, location));
223 } else {
224 delete_tasks.push(self.delete_stale_workspace(id));
225 }
226 }
227
228 futures::future::join_all(delete_tasks).await;
229 Ok(result)
230 }
231
232 pub async fn last_workspace(&self) -> Result<Option<WorkspaceLocation>> {
233 Ok(self
234 .recent_workspaces_on_disk()
235 .await?
236 .into_iter()
237 .next()
238 .map(|(_, location)| location))
239 }
240
241 fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
242 Ok(self
243 .get_pane_group(workspace_id, None)?
244 .into_iter()
245 .next()
246 .unwrap_or_else(|| {
247 SerializedPaneGroup::Pane(SerializedPane {
248 active: true,
249 children: vec![],
250 })
251 }))
252 }
253
254 fn get_pane_group(
255 &self,
256 workspace_id: WorkspaceId,
257 group_id: Option<GroupId>,
258 ) -> Result<Vec<SerializedPaneGroup>> {
259 type GroupKey = (Option<GroupId>, WorkspaceId);
260 type GroupOrPane = (Option<GroupId>, Option<Axis>, Option<PaneId>, Option<bool>);
261 self.select_bound::<GroupKey, GroupOrPane>(sql!(
262 SELECT group_id, axis, pane_id, active
263 FROM (SELECT
264 group_id,
265 axis,
266 NULL as pane_id,
267 NULL as active,
268 position,
269 parent_group_id,
270 workspace_id
271 FROM pane_groups
272 UNION
273 SELECT
274 NULL,
275 NULL,
276 center_panes.pane_id,
277 panes.active as active,
278 position,
279 parent_group_id,
280 panes.workspace_id as workspace_id
281 FROM center_panes
282 JOIN panes ON center_panes.pane_id = panes.pane_id)
283 WHERE parent_group_id IS ? AND workspace_id = ?
284 ORDER BY position
285 ))?((group_id, workspace_id))?
286 .into_iter()
287 .map(|(group_id, axis, pane_id, active)| {
288 if let Some((group_id, axis)) = group_id.zip(axis) {
289 Ok(SerializedPaneGroup::Group {
290 axis,
291 children: self.get_pane_group(workspace_id, Some(group_id))?,
292 })
293 } else if let Some((pane_id, active)) = pane_id.zip(active) {
294 Ok(SerializedPaneGroup::Pane(SerializedPane::new(
295 self.get_items(pane_id)?,
296 active,
297 )))
298 } else {
299 bail!("Pane Group Child was neither a pane group or a pane");
300 }
301 })
302 // Filter out panes and pane groups which don't have any children or items
303 .filter(|pane_group| match pane_group {
304 Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
305 Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
306 _ => true,
307 })
308 .collect::<Result<_>>()
309 }
310
311 fn save_pane_group(
312 conn: &Connection,
313 workspace_id: WorkspaceId,
314 pane_group: &SerializedPaneGroup,
315 parent: Option<(GroupId, usize)>,
316 ) -> Result<()> {
317 match pane_group {
318 SerializedPaneGroup::Group { axis, children } => {
319 let (parent_id, position) = unzip_option(parent);
320
321 let group_id = conn.select_row_bound::<_, i64>(sql!(
322 INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis)
323 VALUES (?, ?, ?, ?)
324 RETURNING group_id
325 ))?((workspace_id, parent_id, position, *axis))?
326 .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
327
328 for (position, group) in children.iter().enumerate() {
329 Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
330 }
331
332 Ok(())
333 }
334 SerializedPaneGroup::Pane(pane) => {
335 Self::save_pane(conn, workspace_id, &pane, parent, false)?;
336 Ok(())
337 }
338 }
339 }
340
341 fn get_dock_pane(&self, workspace_id: WorkspaceId) -> Result<SerializedPane> {
342 let (pane_id, active) = self.select_row_bound(sql!(
343 SELECT pane_id, active
344 FROM panes
345 WHERE pane_id = (SELECT dock_pane FROM workspaces WHERE workspace_id = ?)
346 ))?(workspace_id)?
347 .context("No dock pane for workspace")?;
348
349 Ok(SerializedPane::new(
350 self.get_items(pane_id).context("Reading items")?,
351 active,
352 ))
353 }
354
355 fn save_pane(
356 conn: &Connection,
357 workspace_id: WorkspaceId,
358 pane: &SerializedPane,
359 parent: Option<(GroupId, usize)>, // None indicates BOTH dock pane AND center_pane
360 dock: bool,
361 ) -> Result<PaneId> {
362 let pane_id = conn.select_row_bound::<_, i64>(sql!(
363 INSERT INTO panes(workspace_id, active)
364 VALUES (?, ?)
365 RETURNING pane_id
366 ))?((workspace_id, pane.active))?
367 .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
368
369 if !dock {
370 let (parent_id, order) = unzip_option(parent);
371 conn.exec_bound(sql!(
372 INSERT INTO center_panes(pane_id, parent_group_id, position)
373 VALUES (?, ?, ?)
374 ))?((pane_id, parent_id, order))?;
375 }
376
377 Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
378
379 Ok(pane_id)
380 }
381
382 fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
383 Ok(self.select_bound(sql!(
384 SELECT kind, item_id, active FROM items
385 WHERE pane_id = ?
386 ORDER BY position
387 ))?(pane_id)?)
388 }
389
390 fn save_items(
391 conn: &Connection,
392 workspace_id: WorkspaceId,
393 pane_id: PaneId,
394 items: &[SerializedItem],
395 ) -> Result<()> {
396 let mut insert = conn.exec_bound(sql!(
397 INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?)
398 )).context("Preparing insertion")?;
399 for (position, item) in items.iter().enumerate() {
400 insert((workspace_id, pane_id, position, item))?;
401 }
402
403 Ok(())
404 }
405
406 query! {
407 pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
408 UPDATE workspaces
409 SET timestamp = CURRENT_TIMESTAMP
410 WHERE workspace_id = ?
411 }
412 }
413}
414
415#[cfg(test)]
416mod tests {
417
418 use std::sync::Arc;
419
420 use db::open_test_db;
421 use settings::DockAnchor;
422
423 use super::*;
424
425 #[gpui::test]
426 async fn test_next_id_stability() {
427 env_logger::try_init().ok();
428
429 let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
430
431 db.write(|conn| {
432 conn.migrate(
433 "test_table",
434 &[sql!(
435 CREATE TABLE test_table(
436 text TEXT,
437 workspace_id INTEGER,
438 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
439 ON DELETE CASCADE
440 ) STRICT;
441 )],
442 )
443 .unwrap();
444 })
445 .await;
446
447 let id = db.next_id().await.unwrap();
448 // Assert the empty row got inserted
449 assert_eq!(
450 Some(id),
451 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
452 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
453 ))
454 .unwrap()(id)
455 .unwrap()
456 );
457
458 db.write(move |conn| {
459 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
460 .unwrap()(("test-text-1", id))
461 .unwrap()
462 })
463 .await;
464
465 let test_text_1 = db
466 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
467 .unwrap()(1)
468 .unwrap()
469 .unwrap();
470 assert_eq!(test_text_1, "test-text-1");
471 }
472
473 #[gpui::test]
474 async fn test_workspace_id_stability() {
475 env_logger::try_init().ok();
476
477 let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
478
479 db.write(|conn| {
480 conn.migrate(
481 "test_table",
482 &[sql!(
483 CREATE TABLE test_table(
484 text TEXT,
485 workspace_id INTEGER,
486 FOREIGN KEY(workspace_id)
487 REFERENCES workspaces(workspace_id)
488 ON DELETE CASCADE
489 ) STRICT;)],
490 )
491 })
492 .await
493 .unwrap();
494
495 let mut workspace_1 = SerializedWorkspace {
496 id: 1,
497 location: (["/tmp", "/tmp2"]).into(),
498 dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
499 center_group: Default::default(),
500 dock_pane: Default::default(),
501 left_sidebar_open: true,
502 };
503
504 let mut workspace_2 = SerializedWorkspace {
505 id: 2,
506 location: (["/tmp"]).into(),
507 dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
508 center_group: Default::default(),
509 dock_pane: Default::default(),
510 left_sidebar_open: false,
511 };
512
513 db.save_workspace(workspace_1.clone()).await;
514
515 db.write(|conn| {
516 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
517 .unwrap()(("test-text-1", 1))
518 .unwrap();
519 })
520 .await;
521
522 db.save_workspace(workspace_2.clone()).await;
523
524 db.write(|conn| {
525 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
526 .unwrap()(("test-text-2", 2))
527 .unwrap();
528 })
529 .await;
530
531 workspace_1.location = (["/tmp", "/tmp3"]).into();
532 db.save_workspace(workspace_1.clone()).await;
533 db.save_workspace(workspace_1).await;
534
535 workspace_2.dock_pane.children.push(SerializedItem {
536 kind: Arc::from("Test"),
537 item_id: 10,
538 active: true,
539 });
540 db.save_workspace(workspace_2).await;
541
542 let test_text_2 = db
543 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
544 .unwrap()(2)
545 .unwrap()
546 .unwrap();
547 assert_eq!(test_text_2, "test-text-2");
548
549 let test_text_1 = db
550 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
551 .unwrap()(1)
552 .unwrap()
553 .unwrap();
554 assert_eq!(test_text_1, "test-text-1");
555 }
556
557 #[gpui::test]
558 async fn test_full_workspace_serialization() {
559 env_logger::try_init().ok();
560
561 let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
562
563 let dock_pane = crate::persistence::model::SerializedPane {
564 children: vec![
565 SerializedItem::new("Terminal", 1, false),
566 SerializedItem::new("Terminal", 2, false),
567 SerializedItem::new("Terminal", 3, true),
568 SerializedItem::new("Terminal", 4, false),
569 ],
570 active: false,
571 };
572
573 // -----------------
574 // | 1,2 | 5,6 |
575 // | - - - | |
576 // | 3,4 | |
577 // -----------------
578 let center_group = SerializedPaneGroup::Group {
579 axis: gpui::Axis::Horizontal,
580 children: vec![
581 SerializedPaneGroup::Group {
582 axis: gpui::Axis::Vertical,
583 children: vec![
584 SerializedPaneGroup::Pane(SerializedPane::new(
585 vec![
586 SerializedItem::new("Terminal", 5, false),
587 SerializedItem::new("Terminal", 6, true),
588 ],
589 false,
590 )),
591 SerializedPaneGroup::Pane(SerializedPane::new(
592 vec![
593 SerializedItem::new("Terminal", 7, true),
594 SerializedItem::new("Terminal", 8, false),
595 ],
596 false,
597 )),
598 ],
599 },
600 SerializedPaneGroup::Pane(SerializedPane::new(
601 vec![
602 SerializedItem::new("Terminal", 9, false),
603 SerializedItem::new("Terminal", 10, true),
604 ],
605 false,
606 )),
607 ],
608 };
609
610 let workspace = SerializedWorkspace {
611 id: 5,
612 location: (["/tmp", "/tmp2"]).into(),
613 dock_position: DockPosition::Shown(DockAnchor::Bottom),
614 center_group,
615 dock_pane,
616 left_sidebar_open: true,
617 };
618
619 db.save_workspace(workspace.clone()).await;
620 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
621
622 assert_eq!(workspace, round_trip_workspace.unwrap());
623
624 // Test guaranteed duplicate IDs
625 db.save_workspace(workspace.clone()).await;
626 db.save_workspace(workspace.clone()).await;
627
628 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
629 assert_eq!(workspace, round_trip_workspace.unwrap());
630 }
631
632 #[gpui::test]
633 async fn test_workspace_assignment() {
634 env_logger::try_init().ok();
635
636 let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
637
638 let workspace_1 = SerializedWorkspace {
639 id: 1,
640 location: (["/tmp", "/tmp2"]).into(),
641 dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
642 center_group: Default::default(),
643 dock_pane: Default::default(),
644 left_sidebar_open: true,
645 };
646
647 let mut workspace_2 = SerializedWorkspace {
648 id: 2,
649 location: (["/tmp"]).into(),
650 dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
651 center_group: Default::default(),
652 dock_pane: Default::default(),
653 left_sidebar_open: false,
654 };
655
656 db.save_workspace(workspace_1.clone()).await;
657 db.save_workspace(workspace_2.clone()).await;
658
659 // Test that paths are treated as a set
660 assert_eq!(
661 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
662 workspace_1
663 );
664 assert_eq!(
665 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
666 workspace_1
667 );
668
669 // Make sure that other keys work
670 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
671 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
672
673 // Test 'mutate' case of updating a pre-existing id
674 workspace_2.location = (["/tmp", "/tmp2"]).into();
675
676 db.save_workspace(workspace_2.clone()).await;
677 assert_eq!(
678 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
679 workspace_2
680 );
681
682 // Test other mechanism for mutating
683 let mut workspace_3 = SerializedWorkspace {
684 id: 3,
685 location: (&["/tmp", "/tmp2"]).into(),
686 dock_position: DockPosition::Shown(DockAnchor::Right),
687 center_group: Default::default(),
688 dock_pane: Default::default(),
689 left_sidebar_open: false,
690 };
691
692 db.save_workspace(workspace_3.clone()).await;
693 assert_eq!(
694 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
695 workspace_3
696 );
697
698 // Make sure that updating paths differently also works
699 workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
700 db.save_workspace(workspace_3.clone()).await;
701 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
702 assert_eq!(
703 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
704 .unwrap(),
705 workspace_3
706 );
707 }
708
709 use crate::dock::DockPosition;
710 use crate::persistence::model::SerializedWorkspace;
711 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
712
713 fn default_workspace<P: AsRef<Path>>(
714 workspace_id: &[P],
715 dock_pane: SerializedPane,
716 center_group: &SerializedPaneGroup,
717 ) -> SerializedWorkspace {
718 SerializedWorkspace {
719 id: 4,
720 location: workspace_id.into(),
721 dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
722 center_group: center_group.clone(),
723 dock_pane,
724 left_sidebar_open: true,
725 }
726 }
727
728 #[gpui::test]
729 async fn test_basic_dock_pane() {
730 env_logger::try_init().ok();
731
732 let db = WorkspaceDb(open_test_db("basic_dock_pane").await);
733
734 let dock_pane = crate::persistence::model::SerializedPane::new(
735 vec![
736 SerializedItem::new("Terminal", 1, false),
737 SerializedItem::new("Terminal", 4, false),
738 SerializedItem::new("Terminal", 2, false),
739 SerializedItem::new("Terminal", 3, true),
740 ],
741 false,
742 );
743
744 let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default());
745
746 db.save_workspace(workspace.clone()).await;
747
748 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
749
750 assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
751 }
752
753 #[gpui::test]
754 async fn test_simple_split() {
755 env_logger::try_init().ok();
756
757 let db = WorkspaceDb(open_test_db("simple_split").await);
758
759 // -----------------
760 // | 1,2 | 5,6 |
761 // | - - - | |
762 // | 3,4 | |
763 // -----------------
764 let center_pane = SerializedPaneGroup::Group {
765 axis: gpui::Axis::Horizontal,
766 children: vec![
767 SerializedPaneGroup::Group {
768 axis: gpui::Axis::Vertical,
769 children: vec![
770 SerializedPaneGroup::Pane(SerializedPane::new(
771 vec![
772 SerializedItem::new("Terminal", 1, false),
773 SerializedItem::new("Terminal", 2, true),
774 ],
775 false,
776 )),
777 SerializedPaneGroup::Pane(SerializedPane::new(
778 vec![
779 SerializedItem::new("Terminal", 4, false),
780 SerializedItem::new("Terminal", 3, true),
781 ],
782 true,
783 )),
784 ],
785 },
786 SerializedPaneGroup::Pane(SerializedPane::new(
787 vec![
788 SerializedItem::new("Terminal", 5, true),
789 SerializedItem::new("Terminal", 6, false),
790 ],
791 false,
792 )),
793 ],
794 };
795
796 let workspace = default_workspace(&["/tmp"], Default::default(), ¢er_pane);
797
798 db.save_workspace(workspace.clone()).await;
799
800 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
801
802 assert_eq!(workspace.center_group, new_workspace.center_group);
803 }
804
805 #[gpui::test]
806 async fn test_cleanup_panes() {
807 env_logger::try_init().ok();
808
809 let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
810
811 let center_pane = SerializedPaneGroup::Group {
812 axis: gpui::Axis::Horizontal,
813 children: vec![
814 SerializedPaneGroup::Group {
815 axis: gpui::Axis::Vertical,
816 children: vec![
817 SerializedPaneGroup::Pane(SerializedPane::new(
818 vec![
819 SerializedItem::new("Terminal", 1, false),
820 SerializedItem::new("Terminal", 2, true),
821 ],
822 false,
823 )),
824 SerializedPaneGroup::Pane(SerializedPane::new(
825 vec![
826 SerializedItem::new("Terminal", 4, false),
827 SerializedItem::new("Terminal", 3, true),
828 ],
829 true,
830 )),
831 ],
832 },
833 SerializedPaneGroup::Pane(SerializedPane::new(
834 vec![
835 SerializedItem::new("Terminal", 5, false),
836 SerializedItem::new("Terminal", 6, true),
837 ],
838 false,
839 )),
840 ],
841 };
842
843 let id = &["/tmp"];
844
845 let mut workspace = default_workspace(id, Default::default(), ¢er_pane);
846
847 db.save_workspace(workspace.clone()).await;
848
849 workspace.center_group = SerializedPaneGroup::Group {
850 axis: gpui::Axis::Vertical,
851 children: vec![
852 SerializedPaneGroup::Pane(SerializedPane::new(
853 vec![
854 SerializedItem::new("Terminal", 1, false),
855 SerializedItem::new("Terminal", 2, true),
856 ],
857 false,
858 )),
859 SerializedPaneGroup::Pane(SerializedPane::new(
860 vec![
861 SerializedItem::new("Terminal", 4, true),
862 SerializedItem::new("Terminal", 3, false),
863 ],
864 true,
865 )),
866 ],
867 };
868
869 db.save_workspace(workspace.clone()).await;
870
871 let new_workspace = db.workspace_for_roots(id).unwrap();
872
873 assert_eq!(workspace.center_group, new_workspace.center_group);
874 }
875}