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