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