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