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