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