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