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 .expect("Clearing old panes");
357
358 conn.exec_bound(sql!(
359 DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?
360 ))?((&workspace.location, workspace.id))
361 .context("clearing out old locations")?;
362
363 // Upsert
364 conn.exec_bound(sql!(
365 INSERT INTO workspaces(
366 workspace_id,
367 workspace_location,
368 left_dock_visible,
369 left_dock_active_panel,
370 left_dock_zoom,
371 right_dock_visible,
372 right_dock_active_panel,
373 right_dock_zoom,
374 bottom_dock_visible,
375 bottom_dock_active_panel,
376 bottom_dock_zoom,
377 timestamp
378 )
379 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
380 ON CONFLICT DO
381 UPDATE SET
382 workspace_location = ?2,
383 left_dock_visible = ?3,
384 left_dock_active_panel = ?4,
385 left_dock_zoom = ?5,
386 right_dock_visible = ?6,
387 right_dock_active_panel = ?7,
388 right_dock_zoom = ?8,
389 bottom_dock_visible = ?9,
390 bottom_dock_active_panel = ?10,
391 bottom_dock_zoom = ?11,
392 timestamp = CURRENT_TIMESTAMP
393 ))?((workspace.id, &workspace.location, workspace.docks))
394 .context("Updating workspace")?;
395
396 // Save center pane group
397 Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
398 .context("save pane group in save workspace")?;
399
400 Ok(())
401 })
402 .log_err();
403 })
404 .await;
405 }
406
407 query! {
408 pub async fn next_id() -> Result<WorkspaceId> {
409 INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
410 }
411 }
412
413 query! {
414 fn recent_workspaces() -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
415 SELECT workspace_id, workspace_location
416 FROM workspaces
417 WHERE workspace_location IS NOT NULL
418 ORDER BY timestamp DESC
419 }
420 }
421
422 query! {
423 pub fn last_window() -> Result<(Option<Uuid>, Option<SerializedWindowsBounds>, Option<bool>)> {
424 SELECT display, window_state, window_x, window_y, window_width, window_height, fullscreen
425 FROM workspaces
426 WHERE workspace_location IS NOT NULL
427 ORDER BY timestamp DESC
428 LIMIT 1
429 }
430 }
431
432 query! {
433 pub async fn delete_workspace_by_id(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_workspace_by_id(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 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 query! {
670 pub(crate) async fn set_fullscreen(workspace_id: WorkspaceId, fullscreen: bool) -> Result<()> {
671 UPDATE workspaces
672 SET fullscreen = ?2
673 WHERE workspace_id = ?1
674 }
675 }
676}
677
678#[cfg(test)]
679mod tests {
680 use super::*;
681 use db::open_test_db;
682 use gpui;
683
684 #[gpui::test]
685 async fn test_next_id_stability() {
686 env_logger::try_init().ok();
687
688 let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
689
690 db.write(|conn| {
691 conn.migrate(
692 "test_table",
693 &[sql!(
694 CREATE TABLE test_table(
695 text TEXT,
696 workspace_id INTEGER,
697 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
698 ON DELETE CASCADE
699 ) STRICT;
700 )],
701 )
702 .unwrap();
703 })
704 .await;
705
706 let id = db.next_id().await.unwrap();
707 // Assert the empty row got inserted
708 assert_eq!(
709 Some(id),
710 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
711 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
712 ))
713 .unwrap()(id)
714 .unwrap()
715 );
716
717 db.write(move |conn| {
718 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
719 .unwrap()(("test-text-1", id))
720 .unwrap()
721 })
722 .await;
723
724 let test_text_1 = db
725 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
726 .unwrap()(1)
727 .unwrap()
728 .unwrap();
729 assert_eq!(test_text_1, "test-text-1");
730 }
731
732 #[gpui::test]
733 async fn test_workspace_id_stability() {
734 env_logger::try_init().ok();
735
736 let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
737
738 db.write(|conn| {
739 conn.migrate(
740 "test_table",
741 &[sql!(
742 CREATE TABLE test_table(
743 text TEXT,
744 workspace_id INTEGER,
745 FOREIGN KEY(workspace_id)
746 REFERENCES workspaces(workspace_id)
747 ON DELETE CASCADE
748 ) STRICT;)],
749 )
750 })
751 .await
752 .unwrap();
753
754 let mut workspace_1 = SerializedWorkspace {
755 id: WorkspaceId(1),
756 location: (["/tmp", "/tmp2"]).into(),
757 center_group: Default::default(),
758 bounds: Default::default(),
759 display: Default::default(),
760 docks: Default::default(),
761 fullscreen: false,
762 };
763
764 let workspace_2 = SerializedWorkspace {
765 id: WorkspaceId(2),
766 location: (["/tmp"]).into(),
767 center_group: Default::default(),
768 bounds: Default::default(),
769 display: Default::default(),
770 docks: Default::default(),
771 fullscreen: false,
772 };
773
774 db.save_workspace(workspace_1.clone()).await;
775
776 db.write(|conn| {
777 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
778 .unwrap()(("test-text-1", 1))
779 .unwrap();
780 })
781 .await;
782
783 db.save_workspace(workspace_2.clone()).await;
784
785 db.write(|conn| {
786 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
787 .unwrap()(("test-text-2", 2))
788 .unwrap();
789 })
790 .await;
791
792 workspace_1.location = (["/tmp", "/tmp3"]).into();
793 db.save_workspace(workspace_1.clone()).await;
794 db.save_workspace(workspace_1).await;
795 db.save_workspace(workspace_2).await;
796
797 let test_text_2 = db
798 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
799 .unwrap()(2)
800 .unwrap()
801 .unwrap();
802 assert_eq!(test_text_2, "test-text-2");
803
804 let test_text_1 = db
805 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
806 .unwrap()(1)
807 .unwrap()
808 .unwrap();
809 assert_eq!(test_text_1, "test-text-1");
810 }
811
812 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
813 SerializedPaneGroup::Group {
814 axis: SerializedAxis(axis),
815 flexes: None,
816 children,
817 }
818 }
819
820 #[gpui::test]
821 async fn test_full_workspace_serialization() {
822 env_logger::try_init().ok();
823
824 let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
825
826 // -----------------
827 // | 1,2 | 5,6 |
828 // | - - - | |
829 // | 3,4 | |
830 // -----------------
831 let center_group = group(
832 Axis::Horizontal,
833 vec![
834 group(
835 Axis::Vertical,
836 vec![
837 SerializedPaneGroup::Pane(SerializedPane::new(
838 vec![
839 SerializedItem::new("Terminal", 5, false),
840 SerializedItem::new("Terminal", 6, true),
841 ],
842 false,
843 )),
844 SerializedPaneGroup::Pane(SerializedPane::new(
845 vec![
846 SerializedItem::new("Terminal", 7, true),
847 SerializedItem::new("Terminal", 8, false),
848 ],
849 false,
850 )),
851 ],
852 ),
853 SerializedPaneGroup::Pane(SerializedPane::new(
854 vec![
855 SerializedItem::new("Terminal", 9, false),
856 SerializedItem::new("Terminal", 10, true),
857 ],
858 false,
859 )),
860 ],
861 );
862
863 let workspace = SerializedWorkspace {
864 id: WorkspaceId(5),
865 location: (["/tmp", "/tmp2"]).into(),
866 center_group,
867 bounds: Default::default(),
868 display: Default::default(),
869 docks: Default::default(),
870 fullscreen: false,
871 };
872
873 db.save_workspace(workspace.clone()).await;
874 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
875
876 assert_eq!(workspace, round_trip_workspace.unwrap());
877
878 // Test guaranteed duplicate IDs
879 db.save_workspace(workspace.clone()).await;
880 db.save_workspace(workspace.clone()).await;
881
882 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
883 assert_eq!(workspace, round_trip_workspace.unwrap());
884 }
885
886 #[gpui::test]
887 async fn test_workspace_assignment() {
888 env_logger::try_init().ok();
889
890 let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
891
892 let workspace_1 = SerializedWorkspace {
893 id: WorkspaceId(1),
894 location: (["/tmp", "/tmp2"]).into(),
895 center_group: Default::default(),
896 bounds: Default::default(),
897 display: Default::default(),
898 docks: Default::default(),
899 fullscreen: false,
900 };
901
902 let mut workspace_2 = SerializedWorkspace {
903 id: WorkspaceId(2),
904 location: (["/tmp"]).into(),
905 center_group: Default::default(),
906 bounds: Default::default(),
907 display: Default::default(),
908 docks: Default::default(),
909 fullscreen: false,
910 };
911
912 db.save_workspace(workspace_1.clone()).await;
913 db.save_workspace(workspace_2.clone()).await;
914
915 // Test that paths are treated as a set
916 assert_eq!(
917 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
918 workspace_1
919 );
920 assert_eq!(
921 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
922 workspace_1
923 );
924
925 // Make sure that other keys work
926 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
927 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
928
929 // Test 'mutate' case of updating a pre-existing id
930 workspace_2.location = (["/tmp", "/tmp2"]).into();
931
932 db.save_workspace(workspace_2.clone()).await;
933 assert_eq!(
934 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
935 workspace_2
936 );
937
938 // Test other mechanism for mutating
939 let mut workspace_3 = SerializedWorkspace {
940 id: WorkspaceId(3),
941 location: (&["/tmp", "/tmp2"]).into(),
942 center_group: Default::default(),
943 bounds: Default::default(),
944 display: Default::default(),
945 docks: Default::default(),
946 fullscreen: false,
947 };
948
949 db.save_workspace(workspace_3.clone()).await;
950 assert_eq!(
951 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
952 workspace_3
953 );
954
955 // Make sure that updating paths differently also works
956 workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
957 db.save_workspace(workspace_3.clone()).await;
958 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
959 assert_eq!(
960 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
961 .unwrap(),
962 workspace_3
963 );
964 }
965
966 use crate::persistence::model::SerializedWorkspace;
967 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
968
969 fn default_workspace<P: AsRef<Path>>(
970 workspace_id: &[P],
971 center_group: &SerializedPaneGroup,
972 ) -> SerializedWorkspace {
973 SerializedWorkspace {
974 id: WorkspaceId(4),
975 location: workspace_id.into(),
976 center_group: center_group.clone(),
977 bounds: Default::default(),
978 display: Default::default(),
979 docks: Default::default(),
980 fullscreen: false,
981 }
982 }
983
984 #[gpui::test]
985 async fn test_simple_split() {
986 env_logger::try_init().ok();
987
988 let db = WorkspaceDb(open_test_db("simple_split").await);
989
990 // -----------------
991 // | 1,2 | 5,6 |
992 // | - - - | |
993 // | 3,4 | |
994 // -----------------
995 let center_pane = group(
996 Axis::Horizontal,
997 vec![
998 group(
999 Axis::Vertical,
1000 vec![
1001 SerializedPaneGroup::Pane(SerializedPane::new(
1002 vec![
1003 SerializedItem::new("Terminal", 1, false),
1004 SerializedItem::new("Terminal", 2, true),
1005 ],
1006 false,
1007 )),
1008 SerializedPaneGroup::Pane(SerializedPane::new(
1009 vec![
1010 SerializedItem::new("Terminal", 4, false),
1011 SerializedItem::new("Terminal", 3, true),
1012 ],
1013 true,
1014 )),
1015 ],
1016 ),
1017 SerializedPaneGroup::Pane(SerializedPane::new(
1018 vec![
1019 SerializedItem::new("Terminal", 5, true),
1020 SerializedItem::new("Terminal", 6, false),
1021 ],
1022 false,
1023 )),
1024 ],
1025 );
1026
1027 let workspace = default_workspace(&["/tmp"], ¢er_pane);
1028
1029 db.save_workspace(workspace.clone()).await;
1030
1031 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
1032
1033 assert_eq!(workspace.center_group, new_workspace.center_group);
1034 }
1035
1036 #[gpui::test]
1037 async fn test_cleanup_panes() {
1038 env_logger::try_init().ok();
1039
1040 let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
1041
1042 let center_pane = group(
1043 Axis::Horizontal,
1044 vec![
1045 group(
1046 Axis::Vertical,
1047 vec![
1048 SerializedPaneGroup::Pane(SerializedPane::new(
1049 vec![
1050 SerializedItem::new("Terminal", 1, false),
1051 SerializedItem::new("Terminal", 2, true),
1052 ],
1053 false,
1054 )),
1055 SerializedPaneGroup::Pane(SerializedPane::new(
1056 vec![
1057 SerializedItem::new("Terminal", 4, false),
1058 SerializedItem::new("Terminal", 3, true),
1059 ],
1060 true,
1061 )),
1062 ],
1063 ),
1064 SerializedPaneGroup::Pane(SerializedPane::new(
1065 vec![
1066 SerializedItem::new("Terminal", 5, false),
1067 SerializedItem::new("Terminal", 6, true),
1068 ],
1069 false,
1070 )),
1071 ],
1072 );
1073
1074 let id = &["/tmp"];
1075
1076 let mut workspace = default_workspace(id, ¢er_pane);
1077
1078 db.save_workspace(workspace.clone()).await;
1079
1080 workspace.center_group = group(
1081 Axis::Vertical,
1082 vec![
1083 SerializedPaneGroup::Pane(SerializedPane::new(
1084 vec![
1085 SerializedItem::new("Terminal", 1, false),
1086 SerializedItem::new("Terminal", 2, true),
1087 ],
1088 false,
1089 )),
1090 SerializedPaneGroup::Pane(SerializedPane::new(
1091 vec![
1092 SerializedItem::new("Terminal", 4, true),
1093 SerializedItem::new("Terminal", 3, false),
1094 ],
1095 true,
1096 )),
1097 ],
1098 );
1099
1100 db.save_workspace(workspace.clone()).await;
1101
1102 let new_workspace = db.workspace_for_roots(id).unwrap();
1103
1104 assert_eq!(workspace.center_group, new_workspace.center_group);
1105 }
1106}