1#![allow(dead_code)]
2
3pub mod model;
4
5use std::path::Path;
6
7use anyhow::{anyhow, bail, Context, Result};
8use db2::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
9use gpui::WindowBounds;
10
11use util::{unzip_option, ResultExt};
12use uuid::Uuid;
13
14use crate::{Axis, 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 db2::open_test_db;
556 use gpui;
557
558 #[gpui::test]
559 async fn test_next_id_stability() {
560 env_logger::try_init().ok();
561
562 let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
563
564 db.write(|conn| {
565 conn.migrate(
566 "test_table",
567 &[sql!(
568 CREATE TABLE test_table(
569 text TEXT,
570 workspace_id INTEGER,
571 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
572 ON DELETE CASCADE
573 ) STRICT;
574 )],
575 )
576 .unwrap();
577 })
578 .await;
579
580 let id = db.next_id().await.unwrap();
581 // Assert the empty row got inserted
582 assert_eq!(
583 Some(id),
584 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
585 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
586 ))
587 .unwrap()(id)
588 .unwrap()
589 );
590
591 db.write(move |conn| {
592 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
593 .unwrap()(("test-text-1", id))
594 .unwrap()
595 })
596 .await;
597
598 let test_text_1 = db
599 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
600 .unwrap()(1)
601 .unwrap()
602 .unwrap();
603 assert_eq!(test_text_1, "test-text-1");
604 }
605
606 #[gpui::test]
607 async fn test_workspace_id_stability() {
608 env_logger::try_init().ok();
609
610 let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
611
612 db.write(|conn| {
613 conn.migrate(
614 "test_table",
615 &[sql!(
616 CREATE TABLE test_table(
617 text TEXT,
618 workspace_id INTEGER,
619 FOREIGN KEY(workspace_id)
620 REFERENCES workspaces(workspace_id)
621 ON DELETE CASCADE
622 ) STRICT;)],
623 )
624 })
625 .await
626 .unwrap();
627
628 let mut workspace_1 = SerializedWorkspace {
629 id: 1,
630 location: (["/tmp", "/tmp2"]).into(),
631 center_group: Default::default(),
632 bounds: Default::default(),
633 display: Default::default(),
634 docks: Default::default(),
635 };
636
637 let workspace_2 = SerializedWorkspace {
638 id: 2,
639 location: (["/tmp"]).into(),
640 center_group: Default::default(),
641 bounds: Default::default(),
642 display: Default::default(),
643 docks: Default::default(),
644 };
645
646 db.save_workspace(workspace_1.clone()).await;
647
648 db.write(|conn| {
649 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
650 .unwrap()(("test-text-1", 1))
651 .unwrap();
652 })
653 .await;
654
655 db.save_workspace(workspace_2.clone()).await;
656
657 db.write(|conn| {
658 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
659 .unwrap()(("test-text-2", 2))
660 .unwrap();
661 })
662 .await;
663
664 workspace_1.location = (["/tmp", "/tmp3"]).into();
665 db.save_workspace(workspace_1.clone()).await;
666 db.save_workspace(workspace_1).await;
667 db.save_workspace(workspace_2).await;
668
669 let test_text_2 = db
670 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
671 .unwrap()(2)
672 .unwrap()
673 .unwrap();
674 assert_eq!(test_text_2, "test-text-2");
675
676 let test_text_1 = db
677 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
678 .unwrap()(1)
679 .unwrap()
680 .unwrap();
681 assert_eq!(test_text_1, "test-text-1");
682 }
683
684 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
685 SerializedPaneGroup::Group {
686 axis,
687 flexes: None,
688 children,
689 }
690 }
691
692 #[gpui::test]
693 async fn test_full_workspace_serialization() {
694 env_logger::try_init().ok();
695
696 let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
697
698 // -----------------
699 // | 1,2 | 5,6 |
700 // | - - - | |
701 // | 3,4 | |
702 // -----------------
703 let center_group = group(
704 Axis::Horizontal,
705 vec![
706 group(
707 Axis::Vertical,
708 vec![
709 SerializedPaneGroup::Pane(SerializedPane::new(
710 vec![
711 SerializedItem::new("Terminal", 5, false),
712 SerializedItem::new("Terminal", 6, true),
713 ],
714 false,
715 )),
716 SerializedPaneGroup::Pane(SerializedPane::new(
717 vec![
718 SerializedItem::new("Terminal", 7, true),
719 SerializedItem::new("Terminal", 8, false),
720 ],
721 false,
722 )),
723 ],
724 ),
725 SerializedPaneGroup::Pane(SerializedPane::new(
726 vec![
727 SerializedItem::new("Terminal", 9, false),
728 SerializedItem::new("Terminal", 10, true),
729 ],
730 false,
731 )),
732 ],
733 );
734
735 let workspace = SerializedWorkspace {
736 id: 5,
737 location: (["/tmp", "/tmp2"]).into(),
738 center_group,
739 bounds: Default::default(),
740 display: Default::default(),
741 docks: Default::default(),
742 };
743
744 db.save_workspace(workspace.clone()).await;
745 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
746
747 assert_eq!(workspace, round_trip_workspace.unwrap());
748
749 // Test guaranteed duplicate IDs
750 db.save_workspace(workspace.clone()).await;
751 db.save_workspace(workspace.clone()).await;
752
753 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
754 assert_eq!(workspace, round_trip_workspace.unwrap());
755 }
756
757 #[gpui::test]
758 async fn test_workspace_assignment() {
759 env_logger::try_init().ok();
760
761 let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
762
763 let workspace_1 = SerializedWorkspace {
764 id: 1,
765 location: (["/tmp", "/tmp2"]).into(),
766 center_group: Default::default(),
767 bounds: Default::default(),
768 display: Default::default(),
769 docks: Default::default(),
770 };
771
772 let mut workspace_2 = SerializedWorkspace {
773 id: 2,
774 location: (["/tmp"]).into(),
775 center_group: Default::default(),
776 bounds: Default::default(),
777 display: Default::default(),
778 docks: Default::default(),
779 };
780
781 db.save_workspace(workspace_1.clone()).await;
782 db.save_workspace(workspace_2.clone()).await;
783
784 // Test that paths are treated as a set
785 assert_eq!(
786 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
787 workspace_1
788 );
789 assert_eq!(
790 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
791 workspace_1
792 );
793
794 // Make sure that other keys work
795 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
796 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
797
798 // Test 'mutate' case of updating a pre-existing id
799 workspace_2.location = (["/tmp", "/tmp2"]).into();
800
801 db.save_workspace(workspace_2.clone()).await;
802 assert_eq!(
803 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
804 workspace_2
805 );
806
807 // Test other mechanism for mutating
808 let mut workspace_3 = SerializedWorkspace {
809 id: 3,
810 location: (&["/tmp", "/tmp2"]).into(),
811 center_group: Default::default(),
812 bounds: Default::default(),
813 display: Default::default(),
814 docks: Default::default(),
815 };
816
817 db.save_workspace(workspace_3.clone()).await;
818 assert_eq!(
819 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
820 workspace_3
821 );
822
823 // Make sure that updating paths differently also works
824 workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
825 db.save_workspace(workspace_3.clone()).await;
826 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
827 assert_eq!(
828 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
829 .unwrap(),
830 workspace_3
831 );
832 }
833
834 use crate::persistence::model::SerializedWorkspace;
835 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
836
837 fn default_workspace<P: AsRef<Path>>(
838 workspace_id: &[P],
839 center_group: &SerializedPaneGroup,
840 ) -> SerializedWorkspace {
841 SerializedWorkspace {
842 id: 4,
843 location: workspace_id.into(),
844 center_group: center_group.clone(),
845 bounds: Default::default(),
846 display: Default::default(),
847 docks: Default::default(),
848 }
849 }
850
851 #[gpui::test]
852 async fn test_simple_split() {
853 env_logger::try_init().ok();
854
855 let db = WorkspaceDb(open_test_db("simple_split").await);
856
857 // -----------------
858 // | 1,2 | 5,6 |
859 // | - - - | |
860 // | 3,4 | |
861 // -----------------
862 let center_pane = group(
863 Axis::Horizontal,
864 vec![
865 group(
866 Axis::Vertical,
867 vec![
868 SerializedPaneGroup::Pane(SerializedPane::new(
869 vec![
870 SerializedItem::new("Terminal", 1, false),
871 SerializedItem::new("Terminal", 2, true),
872 ],
873 false,
874 )),
875 SerializedPaneGroup::Pane(SerializedPane::new(
876 vec![
877 SerializedItem::new("Terminal", 4, false),
878 SerializedItem::new("Terminal", 3, true),
879 ],
880 true,
881 )),
882 ],
883 ),
884 SerializedPaneGroup::Pane(SerializedPane::new(
885 vec![
886 SerializedItem::new("Terminal", 5, true),
887 SerializedItem::new("Terminal", 6, false),
888 ],
889 false,
890 )),
891 ],
892 );
893
894 let workspace = default_workspace(&["/tmp"], ¢er_pane);
895
896 db.save_workspace(workspace.clone()).await;
897
898 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
899
900 assert_eq!(workspace.center_group, new_workspace.center_group);
901 }
902
903 #[gpui::test]
904 async fn test_cleanup_panes() {
905 env_logger::try_init().ok();
906
907 let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
908
909 let center_pane = group(
910 Axis::Horizontal,
911 vec![
912 group(
913 Axis::Vertical,
914 vec![
915 SerializedPaneGroup::Pane(SerializedPane::new(
916 vec![
917 SerializedItem::new("Terminal", 1, false),
918 SerializedItem::new("Terminal", 2, true),
919 ],
920 false,
921 )),
922 SerializedPaneGroup::Pane(SerializedPane::new(
923 vec![
924 SerializedItem::new("Terminal", 4, false),
925 SerializedItem::new("Terminal", 3, true),
926 ],
927 true,
928 )),
929 ],
930 ),
931 SerializedPaneGroup::Pane(SerializedPane::new(
932 vec![
933 SerializedItem::new("Terminal", 5, false),
934 SerializedItem::new("Terminal", 6, true),
935 ],
936 false,
937 )),
938 ],
939 );
940
941 let id = &["/tmp"];
942
943 let mut workspace = default_workspace(id, ¢er_pane);
944
945 db.save_workspace(workspace.clone()).await;
946
947 workspace.center_group = group(
948 Axis::Vertical,
949 vec![
950 SerializedPaneGroup::Pane(SerializedPane::new(
951 vec![
952 SerializedItem::new("Terminal", 1, false),
953 SerializedItem::new("Terminal", 2, true),
954 ],
955 false,
956 )),
957 SerializedPaneGroup::Pane(SerializedPane::new(
958 vec![
959 SerializedItem::new("Terminal", 4, true),
960 SerializedItem::new("Terminal", 3, false),
961 ],
962 true,
963 )),
964 ],
965 );
966
967 db.save_workspace(workspace.clone()).await;
968
969 let new_workspace = db.workspace_for_roots(id).unwrap();
970
971 assert_eq!(workspace.center_group, new_workspace.center_group);
972 }
973}