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
501 use db::open_test_db;
502
503 use super::*;
504
505 #[gpui::test]
506 async fn test_next_id_stability() {
507 env_logger::try_init().ok();
508
509 let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
510
511 db.write(|conn| {
512 conn.migrate(
513 "test_table",
514 &[sql!(
515 CREATE TABLE test_table(
516 text TEXT,
517 workspace_id INTEGER,
518 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
519 ON DELETE CASCADE
520 ) STRICT;
521 )],
522 )
523 .unwrap();
524 })
525 .await;
526
527 let id = db.next_id().await.unwrap();
528 // Assert the empty row got inserted
529 assert_eq!(
530 Some(id),
531 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
532 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
533 ))
534 .unwrap()(id)
535 .unwrap()
536 );
537
538 db.write(move |conn| {
539 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
540 .unwrap()(("test-text-1", id))
541 .unwrap()
542 })
543 .await;
544
545 let test_text_1 = db
546 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
547 .unwrap()(1)
548 .unwrap()
549 .unwrap();
550 assert_eq!(test_text_1, "test-text-1");
551 }
552
553 #[gpui::test]
554 async fn test_workspace_id_stability() {
555 env_logger::try_init().ok();
556
557 let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
558
559 db.write(|conn| {
560 conn.migrate(
561 "test_table",
562 &[sql!(
563 CREATE TABLE test_table(
564 text TEXT,
565 workspace_id INTEGER,
566 FOREIGN KEY(workspace_id)
567 REFERENCES workspaces(workspace_id)
568 ON DELETE CASCADE
569 ) STRICT;)],
570 )
571 })
572 .await
573 .unwrap();
574
575 let mut workspace_1 = SerializedWorkspace {
576 id: 1,
577 location: (["/tmp", "/tmp2"]).into(),
578 center_group: Default::default(),
579 bounds: Default::default(),
580 display: Default::default(),
581 docks: Default::default(),
582 };
583
584 let mut _workspace_2 = SerializedWorkspace {
585 id: 2,
586 location: (["/tmp"]).into(),
587 center_group: Default::default(),
588 bounds: Default::default(),
589 display: Default::default(),
590 docks: Default::default(),
591 };
592
593 db.save_workspace(workspace_1.clone()).await;
594
595 db.write(|conn| {
596 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
597 .unwrap()(("test-text-1", 1))
598 .unwrap();
599 })
600 .await;
601
602 db.save_workspace(_workspace_2.clone()).await;
603
604 db.write(|conn| {
605 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
606 .unwrap()(("test-text-2", 2))
607 .unwrap();
608 })
609 .await;
610
611 workspace_1.location = (["/tmp", "/tmp3"]).into();
612 db.save_workspace(workspace_1.clone()).await;
613 db.save_workspace(workspace_1).await;
614
615 todo!();
616 // workspace_2.dock_pane.children.push(SerializedItem {
617 // kind: Arc::from("Test"),
618 // item_id: 10,
619 // active: true,
620 // });
621 // db.save_workspace(workspace_2).await;
622
623 // let test_text_2 = db
624 // .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
625 // .unwrap()(2)
626 // .unwrap()
627 // .unwrap();
628 // assert_eq!(test_text_2, "test-text-2");
629
630 // let test_text_1 = db
631 // .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
632 // .unwrap()(1)
633 // .unwrap()
634 // .unwrap();
635 // assert_eq!(test_text_1, "test-text-1");
636 }
637
638 #[gpui::test]
639 async fn test_full_workspace_serialization() {
640 env_logger::try_init().ok();
641
642 let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
643
644 // -----------------
645 // | 1,2 | 5,6 |
646 // | - - - | |
647 // | 3,4 | |
648 // -----------------
649 let center_group = SerializedPaneGroup::Group {
650 axis: gpui::Axis::Horizontal,
651 children: vec![
652 SerializedPaneGroup::Group {
653 axis: gpui::Axis::Vertical,
654 children: vec![
655 SerializedPaneGroup::Pane(SerializedPane::new(
656 vec![
657 SerializedItem::new("Terminal", 5, false),
658 SerializedItem::new("Terminal", 6, true),
659 ],
660 false,
661 )),
662 SerializedPaneGroup::Pane(SerializedPane::new(
663 vec![
664 SerializedItem::new("Terminal", 7, true),
665 SerializedItem::new("Terminal", 8, false),
666 ],
667 false,
668 )),
669 ],
670 },
671 SerializedPaneGroup::Pane(SerializedPane::new(
672 vec![
673 SerializedItem::new("Terminal", 9, false),
674 SerializedItem::new("Terminal", 10, true),
675 ],
676 false,
677 )),
678 ],
679 };
680
681 let workspace = SerializedWorkspace {
682 id: 5,
683 location: (["/tmp", "/tmp2"]).into(),
684 center_group,
685 bounds: Default::default(),
686 display: Default::default(),
687 docks: Default::default(),
688 };
689
690 db.save_workspace(workspace.clone()).await;
691 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
692
693 assert_eq!(workspace, round_trip_workspace.unwrap());
694
695 // Test guaranteed duplicate IDs
696 db.save_workspace(workspace.clone()).await;
697 db.save_workspace(workspace.clone()).await;
698
699 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
700 assert_eq!(workspace, round_trip_workspace.unwrap());
701 }
702
703 #[gpui::test]
704 async fn test_workspace_assignment() {
705 env_logger::try_init().ok();
706
707 let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
708
709 let workspace_1 = SerializedWorkspace {
710 id: 1,
711 location: (["/tmp", "/tmp2"]).into(),
712 center_group: Default::default(),
713 bounds: Default::default(),
714 display: Default::default(),
715 docks: Default::default(),
716 };
717
718 let mut workspace_2 = SerializedWorkspace {
719 id: 2,
720 location: (["/tmp"]).into(),
721 center_group: Default::default(),
722 bounds: Default::default(),
723 display: Default::default(),
724 docks: Default::default(),
725 };
726
727 db.save_workspace(workspace_1.clone()).await;
728 db.save_workspace(workspace_2.clone()).await;
729
730 // Test that paths are treated as a set
731 assert_eq!(
732 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
733 workspace_1
734 );
735 assert_eq!(
736 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
737 workspace_1
738 );
739
740 // Make sure that other keys work
741 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
742 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
743
744 // Test 'mutate' case of updating a pre-existing id
745 workspace_2.location = (["/tmp", "/tmp2"]).into();
746
747 db.save_workspace(workspace_2.clone()).await;
748 assert_eq!(
749 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
750 workspace_2
751 );
752
753 // Test other mechanism for mutating
754 let mut workspace_3 = SerializedWorkspace {
755 id: 3,
756 location: (&["/tmp", "/tmp2"]).into(),
757 center_group: Default::default(),
758 bounds: Default::default(),
759 display: Default::default(),
760 docks: Default::default(),
761 };
762
763 db.save_workspace(workspace_3.clone()).await;
764 assert_eq!(
765 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
766 workspace_3
767 );
768
769 // Make sure that updating paths differently also works
770 workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
771 db.save_workspace(workspace_3.clone()).await;
772 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
773 assert_eq!(
774 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
775 .unwrap(),
776 workspace_3
777 );
778 }
779
780 use crate::persistence::model::SerializedWorkspace;
781 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
782
783 fn default_workspace<P: AsRef<Path>>(
784 workspace_id: &[P],
785 center_group: &SerializedPaneGroup,
786 ) -> SerializedWorkspace {
787 SerializedWorkspace {
788 id: 4,
789 location: workspace_id.into(),
790 center_group: center_group.clone(),
791 bounds: Default::default(),
792 display: Default::default(),
793 docks: Default::default(),
794 }
795 }
796
797 #[gpui::test]
798 async fn test_simple_split() {
799 env_logger::try_init().ok();
800
801 let db = WorkspaceDb(open_test_db("simple_split").await);
802
803 // -----------------
804 // | 1,2 | 5,6 |
805 // | - - - | |
806 // | 3,4 | |
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, true),
833 SerializedItem::new("Terminal", 6, false),
834 ],
835 false,
836 )),
837 ],
838 };
839
840 let workspace = default_workspace(&["/tmp"], ¢er_pane);
841
842 db.save_workspace(workspace.clone()).await;
843
844 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
845
846 assert_eq!(workspace.center_group, new_workspace.center_group);
847 }
848
849 #[gpui::test]
850 async fn test_cleanup_panes() {
851 env_logger::try_init().ok();
852
853 let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
854
855 let center_pane = SerializedPaneGroup::Group {
856 axis: gpui::Axis::Horizontal,
857 children: vec![
858 SerializedPaneGroup::Group {
859 axis: gpui::Axis::Vertical,
860 children: vec![
861 SerializedPaneGroup::Pane(SerializedPane::new(
862 vec![
863 SerializedItem::new("Terminal", 1, false),
864 SerializedItem::new("Terminal", 2, true),
865 ],
866 false,
867 )),
868 SerializedPaneGroup::Pane(SerializedPane::new(
869 vec![
870 SerializedItem::new("Terminal", 4, false),
871 SerializedItem::new("Terminal", 3, true),
872 ],
873 true,
874 )),
875 ],
876 },
877 SerializedPaneGroup::Pane(SerializedPane::new(
878 vec![
879 SerializedItem::new("Terminal", 5, false),
880 SerializedItem::new("Terminal", 6, true),
881 ],
882 false,
883 )),
884 ],
885 };
886
887 let id = &["/tmp"];
888
889 let mut workspace = default_workspace(id, ¢er_pane);
890
891 db.save_workspace(workspace.clone()).await;
892
893 workspace.center_group = SerializedPaneGroup::Group {
894 axis: gpui::Axis::Vertical,
895 children: vec![
896 SerializedPaneGroup::Pane(SerializedPane::new(
897 vec![
898 SerializedItem::new("Terminal", 1, false),
899 SerializedItem::new("Terminal", 2, true),
900 ],
901 false,
902 )),
903 SerializedPaneGroup::Pane(SerializedPane::new(
904 vec![
905 SerializedItem::new("Terminal", 4, true),
906 SerializedItem::new("Terminal", 3, false),
907 ],
908 true,
909 )),
910 ],
911 };
912
913 db.save_workspace(workspace.clone()).await;
914
915 let new_workspace = db.workspace_for_roots(id).unwrap();
916
917 assert_eq!(workspace.center_group, new_workspace.center_group);
918 }
919}