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