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