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