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