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::{platform::WindowBounds, Axis};
10
11use util::{unzip_option, ResultExt};
12use uuid::Uuid;
13
14use crate::WorkspaceId;
15
16use model::{
17 GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
18 WorkspaceLocation,
19};
20
21define_connection! {
22 // Current schema shape using pseudo-rust syntax:
23 //
24 // workspaces(
25 // workspace_id: usize, // Primary key for workspaces
26 // workspace_location: Bincode<Vec<PathBuf>>,
27 // dock_visible: bool, // Deprecated
28 // dock_anchor: DockAnchor, // Deprecated
29 // dock_pane: Option<usize>, // Deprecated
30 // left_sidebar_open: boolean,
31 // timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
32 // window_state: String, // WindowBounds Discriminant
33 // window_x: Option<f32>, // WindowBounds::Fixed RectF x
34 // window_y: Option<f32>, // WindowBounds::Fixed RectF y
35 // window_width: Option<f32>, // WindowBounds::Fixed RectF width
36 // window_height: Option<f32>, // WindowBounds::Fixed RectF height
37 // display: Option<Uuid>, // Display id
38 // )
39 //
40 // pane_groups(
41 // group_id: usize, // Primary key for pane_groups
42 // workspace_id: usize, // References workspaces table
43 // parent_group_id: Option<usize>, // None indicates that this is the root node
44 // position: Optiopn<usize>, // None indicates that this is the root node
45 // axis: Option<Axis>, // 'Vertical', 'Horizontal'
46 // )
47 //
48 // panes(
49 // pane_id: usize, // Primary key for panes
50 // workspace_id: usize, // References workspaces table
51 // active: bool,
52 // )
53 //
54 // center_panes(
55 // pane_id: usize, // Primary key for center_panes
56 // parent_group_id: Option<usize>, // References pane_groups. If none, this is the root
57 // position: Option<usize>, // None indicates this is the root
58 // )
59 //
60 // CREATE TABLE items(
61 // item_id: usize, // This is the item's view id, so this is not unique
62 // workspace_id: usize, // References workspaces table
63 // pane_id: usize, // References panes table
64 // kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global
65 // position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column
66 // active: bool, // Indicates if this item is the active one in the pane
67 // )
68 pub static ref DB: WorkspaceDb<()> =
69 &[sql!(
70 CREATE TABLE workspaces(
71 workspace_id INTEGER PRIMARY KEY,
72 workspace_location BLOB UNIQUE,
73 dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
74 dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
75 dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
76 left_sidebar_open INTEGER, // Boolean
77 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
78 FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
79 ) STRICT;
80
81 CREATE TABLE pane_groups(
82 group_id INTEGER PRIMARY KEY,
83 workspace_id INTEGER NOT NULL,
84 parent_group_id INTEGER, // NULL indicates that this is a root node
85 position INTEGER, // NULL indicates that this is a root node
86 axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
87 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
88 ON DELETE CASCADE
89 ON UPDATE CASCADE,
90 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
91 ) STRICT;
92
93 CREATE TABLE panes(
94 pane_id INTEGER PRIMARY KEY,
95 workspace_id INTEGER NOT NULL,
96 active INTEGER NOT NULL, // Boolean
97 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
98 ON DELETE CASCADE
99 ON UPDATE CASCADE
100 ) STRICT;
101
102 CREATE TABLE center_panes(
103 pane_id INTEGER PRIMARY KEY,
104 parent_group_id INTEGER, // NULL means that this is a root pane
105 position INTEGER, // NULL means that this is a root pane
106 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
107 ON DELETE CASCADE,
108 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
109 ) STRICT;
110
111 CREATE TABLE items(
112 item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
113 workspace_id INTEGER NOT NULL,
114 pane_id INTEGER NOT NULL,
115 kind TEXT NOT NULL,
116 position INTEGER NOT NULL,
117 active INTEGER NOT NULL,
118 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
119 ON DELETE CASCADE
120 ON UPDATE CASCADE,
121 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
122 ON DELETE CASCADE,
123 PRIMARY KEY(item_id, workspace_id)
124 ) STRICT;
125 ),
126 sql!(
127 ALTER TABLE workspaces ADD COLUMN window_state TEXT;
128 ALTER TABLE workspaces ADD COLUMN window_x REAL;
129 ALTER TABLE workspaces ADD COLUMN window_y REAL;
130 ALTER TABLE workspaces ADD COLUMN window_width REAL;
131 ALTER TABLE workspaces ADD COLUMN window_height REAL;
132 ALTER TABLE workspaces ADD COLUMN display BLOB;
133 )];
134}
135
136impl WorkspaceDb {
137 /// Returns a serialized workspace for the given worktree_roots. If the passed array
138 /// is empty, the most recent workspace is returned instead. If no workspace for the
139 /// passed roots is stored, returns none.
140 pub fn workspace_for_roots<P: AsRef<Path>>(
141 &self,
142 worktree_roots: &[P],
143 ) -> Option<SerializedWorkspace> {
144 let workspace_location: WorkspaceLocation = worktree_roots.into();
145
146 // Note that we re-assign the workspace_id here in case it's empty
147 // and we've grabbed the most recent workspace
148 let (workspace_id, workspace_location, left_sidebar_open, bounds, display): (
149 WorkspaceId,
150 WorkspaceLocation,
151 bool,
152 Option<WindowBounds>,
153 Option<Uuid>,
154 ) = self
155 .select_row_bound(sql! {
156 SELECT
157 workspace_id,
158 workspace_location,
159 left_sidebar_open,
160 window_state,
161 window_x,
162 window_y,
163 window_width,
164 window_height,
165 display
166 FROM workspaces
167 WHERE workspace_location = ?
168 })
169 .and_then(|mut prepared_statement| (prepared_statement)(&workspace_location))
170 .context("No workspaces found")
171 .warn_on_err()
172 .flatten()?;
173
174 Some(SerializedWorkspace {
175 id: workspace_id,
176 location: workspace_location.clone(),
177 center_group: self
178 .get_center_pane_group(workspace_id)
179 .context("Getting center group")
180 .log_err()?,
181 left_sidebar_open,
182 bounds,
183 display,
184 })
185 }
186
187 /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
188 /// that used this workspace previously
189 pub async fn save_workspace(&self, workspace: SerializedWorkspace) {
190 self.write(move |conn| {
191 conn.with_savepoint("update_worktrees", || {
192 // Clear out panes and pane_groups
193 conn.exec_bound(sql!(
194 DELETE FROM pane_groups WHERE workspace_id = ?1;
195 DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
196 .expect("Clearing old panes");
197
198 conn.exec_bound(sql!(
199 DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?
200 ))?((&workspace.location, workspace.id.clone()))
201 .context("clearing out old locations")?;
202
203 // Upsert
204 conn.exec_bound(sql!(
205 INSERT INTO workspaces(
206 workspace_id,
207 workspace_location,
208 left_sidebar_open,
209 timestamp
210 )
211 VALUES (?1, ?2, ?3, CURRENT_TIMESTAMP)
212 ON CONFLICT DO
213 UPDATE SET
214 workspace_location = ?2,
215 left_sidebar_open = ?3,
216 timestamp = CURRENT_TIMESTAMP
217 ))?((workspace.id, &workspace.location))
218 .context("Updating workspace")?;
219
220 // Save center pane group
221 Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
222 .context("save pane group in save workspace")?;
223
224 Ok(())
225 })
226 .log_err();
227 })
228 .await;
229 }
230
231 query! {
232 pub async fn next_id() -> Result<WorkspaceId> {
233 INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
234 }
235 }
236
237 query! {
238 fn recent_workspaces() -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
239 SELECT workspace_id, workspace_location
240 FROM workspaces
241 WHERE workspace_location IS NOT NULL
242 ORDER BY timestamp DESC
243 }
244 }
245
246 query! {
247 async fn delete_stale_workspace(id: WorkspaceId) -> Result<()> {
248 DELETE FROM workspaces
249 WHERE workspace_id IS ?
250 }
251 }
252
253 // Returns the recent locations which are still valid on disk and deletes ones which no longer
254 // exist.
255 pub async fn recent_workspaces_on_disk(&self) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
256 let mut result = Vec::new();
257 let mut delete_tasks = Vec::new();
258 for (id, location) in self.recent_workspaces()? {
259 if location.paths().iter().all(|path| path.exists())
260 && location.paths().iter().any(|path| path.is_dir())
261 {
262 result.push((id, location));
263 } else {
264 delete_tasks.push(self.delete_stale_workspace(id));
265 }
266 }
267
268 futures::future::join_all(delete_tasks).await;
269 Ok(result)
270 }
271
272 pub async fn last_workspace(&self) -> Result<Option<WorkspaceLocation>> {
273 Ok(self
274 .recent_workspaces_on_disk()
275 .await?
276 .into_iter()
277 .next()
278 .map(|(_, location)| location))
279 }
280
281 fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
282 Ok(self
283 .get_pane_group(workspace_id, None)?
284 .into_iter()
285 .next()
286 .unwrap_or_else(|| {
287 SerializedPaneGroup::Pane(SerializedPane {
288 active: true,
289 children: vec![],
290 })
291 }))
292 }
293
294 fn get_pane_group(
295 &self,
296 workspace_id: WorkspaceId,
297 group_id: Option<GroupId>,
298 ) -> Result<Vec<SerializedPaneGroup>> {
299 type GroupKey = (Option<GroupId>, WorkspaceId);
300 type GroupOrPane = (Option<GroupId>, Option<Axis>, Option<PaneId>, Option<bool>);
301 self.select_bound::<GroupKey, GroupOrPane>(sql!(
302 SELECT group_id, axis, pane_id, active
303 FROM (SELECT
304 group_id,
305 axis,
306 NULL as pane_id,
307 NULL as active,
308 position,
309 parent_group_id,
310 workspace_id
311 FROM pane_groups
312 UNION
313 SELECT
314 NULL,
315 NULL,
316 center_panes.pane_id,
317 panes.active as active,
318 position,
319 parent_group_id,
320 panes.workspace_id as workspace_id
321 FROM center_panes
322 JOIN panes ON center_panes.pane_id = panes.pane_id)
323 WHERE parent_group_id IS ? AND workspace_id = ?
324 ORDER BY position
325 ))?((group_id, workspace_id))?
326 .into_iter()
327 .map(|(group_id, axis, pane_id, active)| {
328 if let Some((group_id, axis)) = group_id.zip(axis) {
329 Ok(SerializedPaneGroup::Group {
330 axis,
331 children: self.get_pane_group(workspace_id, Some(group_id))?,
332 })
333 } else if let Some((pane_id, active)) = pane_id.zip(active) {
334 Ok(SerializedPaneGroup::Pane(SerializedPane::new(
335 self.get_items(pane_id)?,
336 active,
337 )))
338 } else {
339 bail!("Pane Group Child was neither a pane group or a pane");
340 }
341 })
342 // Filter out panes and pane groups which don't have any children or items
343 .filter(|pane_group| match pane_group {
344 Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
345 Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
346 _ => true,
347 })
348 .collect::<Result<_>>()
349 }
350
351 fn save_pane_group(
352 conn: &Connection,
353 workspace_id: WorkspaceId,
354 pane_group: &SerializedPaneGroup,
355 parent: Option<(GroupId, usize)>,
356 ) -> Result<()> {
357 match pane_group {
358 SerializedPaneGroup::Group { axis, children } => {
359 let (parent_id, position) = unzip_option(parent);
360
361 let group_id = conn.select_row_bound::<_, i64>(sql!(
362 INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis)
363 VALUES (?, ?, ?, ?)
364 RETURNING group_id
365 ))?((workspace_id, parent_id, position, *axis))?
366 .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
367
368 for (position, group) in children.iter().enumerate() {
369 Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
370 }
371
372 Ok(())
373 }
374 SerializedPaneGroup::Pane(pane) => {
375 Self::save_pane(conn, workspace_id, &pane, parent)?;
376 Ok(())
377 }
378 }
379 }
380
381 fn save_pane(
382 conn: &Connection,
383 workspace_id: WorkspaceId,
384 pane: &SerializedPane,
385 parent: Option<(GroupId, usize)>,
386 ) -> Result<PaneId> {
387 let pane_id = conn.select_row_bound::<_, i64>(sql!(
388 INSERT INTO panes(workspace_id, active)
389 VALUES (?, ?)
390 RETURNING pane_id
391 ))?((workspace_id, pane.active))?
392 .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
393
394 let (parent_id, order) = unzip_option(parent);
395 conn.exec_bound(sql!(
396 INSERT INTO center_panes(pane_id, parent_group_id, position)
397 VALUES (?, ?, ?)
398 ))?((pane_id, parent_id, order))?;
399
400 Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
401
402 Ok(pane_id)
403 }
404
405 fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
406 Ok(self.select_bound(sql!(
407 SELECT kind, item_id, active FROM items
408 WHERE pane_id = ?
409 ORDER BY position
410 ))?(pane_id)?)
411 }
412
413 fn save_items(
414 conn: &Connection,
415 workspace_id: WorkspaceId,
416 pane_id: PaneId,
417 items: &[SerializedItem],
418 ) -> Result<()> {
419 let mut insert = conn.exec_bound(sql!(
420 INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?)
421 )).context("Preparing insertion")?;
422 for (position, item) in items.iter().enumerate() {
423 insert((workspace_id, pane_id, position, item))?;
424 }
425
426 Ok(())
427 }
428
429 query! {
430 pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
431 UPDATE workspaces
432 SET timestamp = CURRENT_TIMESTAMP
433 WHERE workspace_id = ?
434 }
435 }
436
437 query! {
438 pub async fn set_window_bounds(workspace_id: WorkspaceId, bounds: WindowBounds, display: Uuid) -> Result<()> {
439 UPDATE workspaces
440 SET window_state = ?2,
441 window_x = ?3,
442 window_y = ?4,
443 window_width = ?5,
444 window_height = ?6,
445 display = ?7
446 WHERE workspace_id = ?1
447 }
448 }
449}
450
451#[cfg(test)]
452mod tests {
453
454 use db::open_test_db;
455
456 use super::*;
457
458 #[gpui::test]
459 async fn test_next_id_stability() {
460 env_logger::try_init().ok();
461
462 let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
463
464 db.write(|conn| {
465 conn.migrate(
466 "test_table",
467 &[sql!(
468 CREATE TABLE test_table(
469 text TEXT,
470 workspace_id INTEGER,
471 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
472 ON DELETE CASCADE
473 ) STRICT;
474 )],
475 )
476 .unwrap();
477 })
478 .await;
479
480 let id = db.next_id().await.unwrap();
481 // Assert the empty row got inserted
482 assert_eq!(
483 Some(id),
484 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
485 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
486 ))
487 .unwrap()(id)
488 .unwrap()
489 );
490
491 db.write(move |conn| {
492 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
493 .unwrap()(("test-text-1", id))
494 .unwrap()
495 })
496 .await;
497
498 let test_text_1 = db
499 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
500 .unwrap()(1)
501 .unwrap()
502 .unwrap();
503 assert_eq!(test_text_1, "test-text-1");
504 }
505
506 #[gpui::test]
507 async fn test_workspace_id_stability() {
508 env_logger::try_init().ok();
509
510 let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
511
512 db.write(|conn| {
513 conn.migrate(
514 "test_table",
515 &[sql!(
516 CREATE TABLE test_table(
517 text TEXT,
518 workspace_id INTEGER,
519 FOREIGN KEY(workspace_id)
520 REFERENCES workspaces(workspace_id)
521 ON DELETE CASCADE
522 ) STRICT;)],
523 )
524 })
525 .await
526 .unwrap();
527
528 let mut workspace_1 = SerializedWorkspace {
529 id: 1,
530 location: (["/tmp", "/tmp2"]).into(),
531 center_group: Default::default(),
532 left_sidebar_open: true,
533 bounds: Default::default(),
534 display: Default::default(),
535 };
536
537 let mut _workspace_2 = SerializedWorkspace {
538 id: 2,
539 location: (["/tmp"]).into(),
540 center_group: Default::default(),
541 left_sidebar_open: false,
542 bounds: Default::default(),
543 display: Default::default(),
544 };
545
546 db.save_workspace(workspace_1.clone()).await;
547
548 db.write(|conn| {
549 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
550 .unwrap()(("test-text-1", 1))
551 .unwrap();
552 })
553 .await;
554
555 db.save_workspace(_workspace_2.clone()).await;
556
557 db.write(|conn| {
558 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
559 .unwrap()(("test-text-2", 2))
560 .unwrap();
561 })
562 .await;
563
564 workspace_1.location = (["/tmp", "/tmp3"]).into();
565 db.save_workspace(workspace_1.clone()).await;
566 db.save_workspace(workspace_1).await;
567
568 todo!();
569 // workspace_2.dock_pane.children.push(SerializedItem {
570 // kind: Arc::from("Test"),
571 // item_id: 10,
572 // active: true,
573 // });
574 // db.save_workspace(workspace_2).await;
575
576 // let test_text_2 = db
577 // .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
578 // .unwrap()(2)
579 // .unwrap()
580 // .unwrap();
581 // assert_eq!(test_text_2, "test-text-2");
582
583 // let test_text_1 = db
584 // .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
585 // .unwrap()(1)
586 // .unwrap()
587 // .unwrap();
588 // assert_eq!(test_text_1, "test-text-1");
589 }
590
591 #[gpui::test]
592 async fn test_full_workspace_serialization() {
593 env_logger::try_init().ok();
594
595 let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
596
597 // -----------------
598 // | 1,2 | 5,6 |
599 // | - - - | |
600 // | 3,4 | |
601 // -----------------
602 let center_group = SerializedPaneGroup::Group {
603 axis: gpui::Axis::Horizontal,
604 children: vec![
605 SerializedPaneGroup::Group {
606 axis: gpui::Axis::Vertical,
607 children: vec![
608 SerializedPaneGroup::Pane(SerializedPane::new(
609 vec![
610 SerializedItem::new("Terminal", 5, false),
611 SerializedItem::new("Terminal", 6, true),
612 ],
613 false,
614 )),
615 SerializedPaneGroup::Pane(SerializedPane::new(
616 vec![
617 SerializedItem::new("Terminal", 7, true),
618 SerializedItem::new("Terminal", 8, false),
619 ],
620 false,
621 )),
622 ],
623 },
624 SerializedPaneGroup::Pane(SerializedPane::new(
625 vec![
626 SerializedItem::new("Terminal", 9, false),
627 SerializedItem::new("Terminal", 10, true),
628 ],
629 false,
630 )),
631 ],
632 };
633
634 let workspace = SerializedWorkspace {
635 id: 5,
636 location: (["/tmp", "/tmp2"]).into(),
637 center_group,
638 left_sidebar_open: true,
639 bounds: Default::default(),
640 display: Default::default(),
641 };
642
643 db.save_workspace(workspace.clone()).await;
644 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
645
646 assert_eq!(workspace, round_trip_workspace.unwrap());
647
648 // Test guaranteed duplicate IDs
649 db.save_workspace(workspace.clone()).await;
650 db.save_workspace(workspace.clone()).await;
651
652 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
653 assert_eq!(workspace, round_trip_workspace.unwrap());
654 }
655
656 #[gpui::test]
657 async fn test_workspace_assignment() {
658 env_logger::try_init().ok();
659
660 let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
661
662 let workspace_1 = SerializedWorkspace {
663 id: 1,
664 location: (["/tmp", "/tmp2"]).into(),
665 center_group: Default::default(),
666 left_sidebar_open: true,
667 bounds: Default::default(),
668 display: Default::default(),
669 };
670
671 let mut workspace_2 = SerializedWorkspace {
672 id: 2,
673 location: (["/tmp"]).into(),
674 center_group: Default::default(),
675 left_sidebar_open: false,
676 bounds: Default::default(),
677 display: Default::default(),
678 };
679
680 db.save_workspace(workspace_1.clone()).await;
681 db.save_workspace(workspace_2.clone()).await;
682
683 // Test that paths are treated as a set
684 assert_eq!(
685 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
686 workspace_1
687 );
688 assert_eq!(
689 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
690 workspace_1
691 );
692
693 // Make sure that other keys work
694 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
695 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
696
697 // Test 'mutate' case of updating a pre-existing id
698 workspace_2.location = (["/tmp", "/tmp2"]).into();
699
700 db.save_workspace(workspace_2.clone()).await;
701 assert_eq!(
702 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
703 workspace_2
704 );
705
706 // Test other mechanism for mutating
707 let mut workspace_3 = SerializedWorkspace {
708 id: 3,
709 location: (&["/tmp", "/tmp2"]).into(),
710 center_group: Default::default(),
711 left_sidebar_open: false,
712 bounds: Default::default(),
713 display: Default::default(),
714 };
715
716 db.save_workspace(workspace_3.clone()).await;
717 assert_eq!(
718 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
719 workspace_3
720 );
721
722 // Make sure that updating paths differently also works
723 workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
724 db.save_workspace(workspace_3.clone()).await;
725 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
726 assert_eq!(
727 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
728 .unwrap(),
729 workspace_3
730 );
731 }
732
733 use crate::persistence::model::SerializedWorkspace;
734 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
735
736 fn default_workspace<P: AsRef<Path>>(
737 workspace_id: &[P],
738 center_group: &SerializedPaneGroup,
739 ) -> SerializedWorkspace {
740 SerializedWorkspace {
741 id: 4,
742 location: workspace_id.into(),
743 center_group: center_group.clone(),
744 left_sidebar_open: true,
745 bounds: Default::default(),
746 display: Default::default(),
747 }
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"], ¢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, ¢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}