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