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