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