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