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