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 = serde_json::json!(flexes).to_string();
447 let group_id = conn.select_row_bound::<_, i64>(sql!(
448 INSERT INTO pane_groups(
449 workspace_id,
450 parent_group_id,
451 position,
452 axis,
453 flexes
454 )
455 VALUES (?, ?, ?, ?, ?)
456 RETURNING group_id
457 ))?((
458 workspace_id,
459 parent_id,
460 position,
461 *axis,
462 flex_string,
463 ))?
464 .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
465
466 for (position, group) in children.iter().enumerate() {
467 Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
468 }
469
470 Ok(())
471 }
472 SerializedPaneGroup::Pane(pane) => {
473 Self::save_pane(conn, workspace_id, &pane, parent)?;
474 Ok(())
475 }
476 }
477 }
478
479 fn save_pane(
480 conn: &Connection,
481 workspace_id: WorkspaceId,
482 pane: &SerializedPane,
483 parent: Option<(GroupId, usize)>,
484 ) -> Result<PaneId> {
485 let pane_id = conn.select_row_bound::<_, i64>(sql!(
486 INSERT INTO panes(workspace_id, active)
487 VALUES (?, ?)
488 RETURNING pane_id
489 ))?((workspace_id, pane.active))?
490 .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
491
492 let (parent_id, order) = unzip_option(parent);
493 conn.exec_bound(sql!(
494 INSERT INTO center_panes(pane_id, parent_group_id, position)
495 VALUES (?, ?, ?)
496 ))?((pane_id, parent_id, order))?;
497
498 Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
499
500 Ok(pane_id)
501 }
502
503 fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
504 Ok(self.select_bound(sql!(
505 SELECT kind, item_id, active FROM items
506 WHERE pane_id = ?
507 ORDER BY position
508 ))?(pane_id)?)
509 }
510
511 fn save_items(
512 conn: &Connection,
513 workspace_id: WorkspaceId,
514 pane_id: PaneId,
515 items: &[SerializedItem],
516 ) -> Result<()> {
517 let mut insert = conn.exec_bound(sql!(
518 INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?)
519 )).context("Preparing insertion")?;
520 for (position, item) in items.iter().enumerate() {
521 insert((workspace_id, pane_id, position, item))?;
522 }
523
524 Ok(())
525 }
526
527 query! {
528 pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
529 UPDATE workspaces
530 SET timestamp = CURRENT_TIMESTAMP
531 WHERE workspace_id = ?
532 }
533 }
534
535 query! {
536 pub async fn set_window_bounds(workspace_id: WorkspaceId, bounds: WindowBounds, display: Uuid) -> Result<()> {
537 UPDATE workspaces
538 SET window_state = ?2,
539 window_x = ?3,
540 window_y = ?4,
541 window_width = ?5,
542 window_height = ?6,
543 display = ?7
544 WHERE workspace_id = ?1
545 }
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552 use db::open_test_db;
553
554 #[gpui::test]
555 async fn test_next_id_stability() {
556 env_logger::try_init().ok();
557
558 let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
559
560 db.write(|conn| {
561 conn.migrate(
562 "test_table",
563 &[sql!(
564 CREATE TABLE test_table(
565 text TEXT,
566 workspace_id INTEGER,
567 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
568 ON DELETE CASCADE
569 ) STRICT;
570 )],
571 )
572 .unwrap();
573 })
574 .await;
575
576 let id = db.next_id().await.unwrap();
577 // Assert the empty row got inserted
578 assert_eq!(
579 Some(id),
580 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
581 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
582 ))
583 .unwrap()(id)
584 .unwrap()
585 );
586
587 db.write(move |conn| {
588 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
589 .unwrap()(("test-text-1", id))
590 .unwrap()
591 })
592 .await;
593
594 let test_text_1 = db
595 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
596 .unwrap()(1)
597 .unwrap()
598 .unwrap();
599 assert_eq!(test_text_1, "test-text-1");
600 }
601
602 #[gpui::test]
603 async fn test_workspace_id_stability() {
604 env_logger::try_init().ok();
605
606 let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
607
608 db.write(|conn| {
609 conn.migrate(
610 "test_table",
611 &[sql!(
612 CREATE TABLE test_table(
613 text TEXT,
614 workspace_id INTEGER,
615 FOREIGN KEY(workspace_id)
616 REFERENCES workspaces(workspace_id)
617 ON DELETE CASCADE
618 ) STRICT;)],
619 )
620 })
621 .await
622 .unwrap();
623
624 let mut workspace_1 = SerializedWorkspace {
625 id: 1,
626 location: (["/tmp", "/tmp2"]).into(),
627 center_group: Default::default(),
628 bounds: Default::default(),
629 display: Default::default(),
630 docks: Default::default(),
631 };
632
633 let workspace_2 = SerializedWorkspace {
634 id: 2,
635 location: (["/tmp"]).into(),
636 center_group: Default::default(),
637 bounds: Default::default(),
638 display: Default::default(),
639 docks: Default::default(),
640 };
641
642 db.save_workspace(workspace_1.clone()).await;
643
644 db.write(|conn| {
645 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
646 .unwrap()(("test-text-1", 1))
647 .unwrap();
648 })
649 .await;
650
651 db.save_workspace(workspace_2.clone()).await;
652
653 db.write(|conn| {
654 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
655 .unwrap()(("test-text-2", 2))
656 .unwrap();
657 })
658 .await;
659
660 workspace_1.location = (["/tmp", "/tmp3"]).into();
661 db.save_workspace(workspace_1.clone()).await;
662 db.save_workspace(workspace_1).await;
663 db.save_workspace(workspace_2).await;
664
665 let test_text_2 = db
666 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
667 .unwrap()(2)
668 .unwrap()
669 .unwrap();
670 assert_eq!(test_text_2, "test-text-2");
671
672 let test_text_1 = db
673 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
674 .unwrap()(1)
675 .unwrap()
676 .unwrap();
677 assert_eq!(test_text_1, "test-text-1");
678 }
679
680 fn group(axis: gpui::Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
681 SerializedPaneGroup::Group {
682 axis,
683 flexes: None,
684 children,
685 }
686 }
687
688 #[gpui::test]
689 async fn test_full_workspace_serialization() {
690 env_logger::try_init().ok();
691
692 let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
693
694 // -----------------
695 // | 1,2 | 5,6 |
696 // | - - - | |
697 // | 3,4 | |
698 // -----------------
699 let center_group = group(
700 gpui::Axis::Horizontal,
701 vec![
702 group(
703 gpui::Axis::Vertical,
704 vec![
705 SerializedPaneGroup::Pane(SerializedPane::new(
706 vec![
707 SerializedItem::new("Terminal", 5, false),
708 SerializedItem::new("Terminal", 6, true),
709 ],
710 false,
711 )),
712 SerializedPaneGroup::Pane(SerializedPane::new(
713 vec![
714 SerializedItem::new("Terminal", 7, true),
715 SerializedItem::new("Terminal", 8, false),
716 ],
717 false,
718 )),
719 ],
720 ),
721 SerializedPaneGroup::Pane(SerializedPane::new(
722 vec![
723 SerializedItem::new("Terminal", 9, false),
724 SerializedItem::new("Terminal", 10, true),
725 ],
726 false,
727 )),
728 ],
729 );
730
731 let workspace = SerializedWorkspace {
732 id: 5,
733 location: (["/tmp", "/tmp2"]).into(),
734 center_group,
735 bounds: Default::default(),
736 display: Default::default(),
737 docks: Default::default(),
738 };
739
740 db.save_workspace(workspace.clone()).await;
741 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
742
743 assert_eq!(workspace, round_trip_workspace.unwrap());
744
745 // Test guaranteed duplicate IDs
746 db.save_workspace(workspace.clone()).await;
747 db.save_workspace(workspace.clone()).await;
748
749 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
750 assert_eq!(workspace, round_trip_workspace.unwrap());
751 }
752
753 #[gpui::test]
754 async fn test_workspace_assignment() {
755 env_logger::try_init().ok();
756
757 let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
758
759 let workspace_1 = SerializedWorkspace {
760 id: 1,
761 location: (["/tmp", "/tmp2"]).into(),
762 center_group: Default::default(),
763 bounds: Default::default(),
764 display: Default::default(),
765 docks: Default::default(),
766 };
767
768 let mut workspace_2 = SerializedWorkspace {
769 id: 2,
770 location: (["/tmp"]).into(),
771 center_group: Default::default(),
772 bounds: Default::default(),
773 display: Default::default(),
774 docks: Default::default(),
775 };
776
777 db.save_workspace(workspace_1.clone()).await;
778 db.save_workspace(workspace_2.clone()).await;
779
780 // Test that paths are treated as a set
781 assert_eq!(
782 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
783 workspace_1
784 );
785 assert_eq!(
786 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
787 workspace_1
788 );
789
790 // Make sure that other keys work
791 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
792 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
793
794 // Test 'mutate' case of updating a pre-existing id
795 workspace_2.location = (["/tmp", "/tmp2"]).into();
796
797 db.save_workspace(workspace_2.clone()).await;
798 assert_eq!(
799 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
800 workspace_2
801 );
802
803 // Test other mechanism for mutating
804 let mut workspace_3 = SerializedWorkspace {
805 id: 3,
806 location: (&["/tmp", "/tmp2"]).into(),
807 center_group: Default::default(),
808 bounds: Default::default(),
809 display: Default::default(),
810 docks: Default::default(),
811 };
812
813 db.save_workspace(workspace_3.clone()).await;
814 assert_eq!(
815 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
816 workspace_3
817 );
818
819 // Make sure that updating paths differently also works
820 workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
821 db.save_workspace(workspace_3.clone()).await;
822 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
823 assert_eq!(
824 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
825 .unwrap(),
826 workspace_3
827 );
828 }
829
830 use crate::persistence::model::SerializedWorkspace;
831 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
832
833 fn default_workspace<P: AsRef<Path>>(
834 workspace_id: &[P],
835 center_group: &SerializedPaneGroup,
836 ) -> SerializedWorkspace {
837 SerializedWorkspace {
838 id: 4,
839 location: workspace_id.into(),
840 center_group: center_group.clone(),
841 bounds: Default::default(),
842 display: Default::default(),
843 docks: Default::default(),
844 }
845 }
846
847 #[gpui::test]
848 async fn test_simple_split() {
849 env_logger::try_init().ok();
850
851 let db = WorkspaceDb(open_test_db("simple_split").await);
852
853 // -----------------
854 // | 1,2 | 5,6 |
855 // | - - - | |
856 // | 3,4 | |
857 // -----------------
858 let center_pane = group(
859 gpui::Axis::Horizontal,
860 vec![
861 group(
862 gpui::Axis::Vertical,
863 vec![
864 SerializedPaneGroup::Pane(SerializedPane::new(
865 vec![
866 SerializedItem::new("Terminal", 1, false),
867 SerializedItem::new("Terminal", 2, true),
868 ],
869 false,
870 )),
871 SerializedPaneGroup::Pane(SerializedPane::new(
872 vec![
873 SerializedItem::new("Terminal", 4, false),
874 SerializedItem::new("Terminal", 3, true),
875 ],
876 true,
877 )),
878 ],
879 ),
880 SerializedPaneGroup::Pane(SerializedPane::new(
881 vec![
882 SerializedItem::new("Terminal", 5, true),
883 SerializedItem::new("Terminal", 6, false),
884 ],
885 false,
886 )),
887 ],
888 );
889
890 let workspace = default_workspace(&["/tmp"], ¢er_pane);
891
892 db.save_workspace(workspace.clone()).await;
893
894 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
895
896 assert_eq!(workspace.center_group, new_workspace.center_group);
897 }
898
899 #[gpui::test]
900 async fn test_cleanup_panes() {
901 env_logger::try_init().ok();
902
903 let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
904
905 let center_pane = group(
906 gpui::Axis::Horizontal,
907 vec![
908 group(
909 gpui::Axis::Vertical,
910 vec![
911 SerializedPaneGroup::Pane(SerializedPane::new(
912 vec![
913 SerializedItem::new("Terminal", 1, false),
914 SerializedItem::new("Terminal", 2, true),
915 ],
916 false,
917 )),
918 SerializedPaneGroup::Pane(SerializedPane::new(
919 vec![
920 SerializedItem::new("Terminal", 4, false),
921 SerializedItem::new("Terminal", 3, true),
922 ],
923 true,
924 )),
925 ],
926 ),
927 SerializedPaneGroup::Pane(SerializedPane::new(
928 vec![
929 SerializedItem::new("Terminal", 5, false),
930 SerializedItem::new("Terminal", 6, true),
931 ],
932 false,
933 )),
934 ],
935 );
936
937 let id = &["/tmp"];
938
939 let mut workspace = default_workspace(id, ¢er_pane);
940
941 db.save_workspace(workspace.clone()).await;
942
943 workspace.center_group = group(
944 gpui::Axis::Vertical,
945 vec![
946 SerializedPaneGroup::Pane(SerializedPane::new(
947 vec![
948 SerializedItem::new("Terminal", 1, false),
949 SerializedItem::new("Terminal", 2, true),
950 ],
951 false,
952 )),
953 SerializedPaneGroup::Pane(SerializedPane::new(
954 vec![
955 SerializedItem::new("Terminal", 4, true),
956 SerializedItem::new("Terminal", 3, false),
957 ],
958 true,
959 )),
960 ],
961 );
962
963 db.save_workspace(workspace.clone()).await;
964
965 let new_workspace = db.workspace_for_roots(id).unwrap();
966
967 assert_eq!(workspace.center_group, new_workspace.center_group);
968 }
969}