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