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