1pub mod model;
2
3use std::path::Path;
4
5use anyhow::{anyhow, bail, Context, Result};
6use client::DevServerProjectId;
7use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
8use gpui::{point, size, Axis, Bounds, WindowBounds};
9
10use sqlez::{
11 bindable::{Bind, Column, StaticColumnCount},
12 statement::Statement,
13};
14
15use util::ResultExt;
16use uuid::Uuid;
17
18use crate::WorkspaceId;
19
20use model::{
21 GroupId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
22 SerializedWorkspace,
23};
24
25use self::model::{
26 DockStructure, LocalPathsOrder, SerializedDevServerProject, SerializedWorkspaceLocation,
27};
28
29#[derive(Copy, Clone, Debug, PartialEq)]
30pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
31impl sqlez::bindable::StaticColumnCount for SerializedAxis {}
32impl sqlez::bindable::Bind for SerializedAxis {
33 fn bind(
34 &self,
35 statement: &sqlez::statement::Statement,
36 start_index: i32,
37 ) -> anyhow::Result<i32> {
38 match self.0 {
39 gpui::Axis::Horizontal => "Horizontal",
40 gpui::Axis::Vertical => "Vertical",
41 }
42 .bind(statement, start_index)
43 }
44}
45
46impl sqlez::bindable::Column for SerializedAxis {
47 fn column(
48 statement: &mut sqlez::statement::Statement,
49 start_index: i32,
50 ) -> anyhow::Result<(Self, i32)> {
51 String::column(statement, start_index).and_then(|(axis_text, next_index)| {
52 Ok((
53 match axis_text.as_str() {
54 "Horizontal" => Self(Axis::Horizontal),
55 "Vertical" => Self(Axis::Vertical),
56 _ => anyhow::bail!("Stored serialized item kind is incorrect"),
57 },
58 next_index,
59 ))
60 })
61 }
62}
63
64#[derive(Copy, Clone, Debug, PartialEq, Default)]
65pub(crate) struct SerializedWindowBounds(pub(crate) WindowBounds);
66
67impl StaticColumnCount for SerializedWindowBounds {
68 fn column_count() -> usize {
69 5
70 }
71}
72
73impl Bind for SerializedWindowBounds {
74 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
75 match self.0 {
76 WindowBounds::Windowed(bounds) => {
77 let next_index = statement.bind(&"Windowed", start_index)?;
78 statement.bind(
79 &(
80 SerializedDevicePixels(bounds.origin.x),
81 SerializedDevicePixels(bounds.origin.y),
82 SerializedDevicePixels(bounds.size.width),
83 SerializedDevicePixels(bounds.size.height),
84 ),
85 next_index,
86 )
87 }
88 WindowBounds::Maximized(bounds) => {
89 let next_index = statement.bind(&"Maximized", start_index)?;
90 statement.bind(
91 &(
92 SerializedDevicePixels(bounds.origin.x),
93 SerializedDevicePixels(bounds.origin.y),
94 SerializedDevicePixels(bounds.size.width),
95 SerializedDevicePixels(bounds.size.height),
96 ),
97 next_index,
98 )
99 }
100 WindowBounds::Fullscreen(bounds) => {
101 let next_index = statement.bind(&"FullScreen", start_index)?;
102 statement.bind(
103 &(
104 SerializedDevicePixels(bounds.origin.x),
105 SerializedDevicePixels(bounds.origin.y),
106 SerializedDevicePixels(bounds.size.width),
107 SerializedDevicePixels(bounds.size.height),
108 ),
109 next_index,
110 )
111 }
112 }
113 }
114}
115
116impl Column for SerializedWindowBounds {
117 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
118 let (window_state, next_index) = String::column(statement, start_index)?;
119 let status = match window_state.as_str() {
120 "Windowed" | "Fixed" => {
121 let ((x, y, width, height), _) = Column::column(statement, next_index)?;
122 let x: i32 = x;
123 let y: i32 = y;
124 let width: i32 = width;
125 let height: i32 = height;
126 SerializedWindowBounds(WindowBounds::Windowed(Bounds {
127 origin: point(x.into(), y.into()),
128 size: size(width.into(), height.into()),
129 }))
130 }
131 "Maximized" => {
132 let ((x, y, width, height), _) = Column::column(statement, next_index)?;
133 let x: i32 = x;
134 let y: i32 = y;
135 let width: i32 = width;
136 let height: i32 = height;
137 SerializedWindowBounds(WindowBounds::Maximized(Bounds {
138 origin: point(x.into(), y.into()),
139 size: size(width.into(), height.into()),
140 }))
141 }
142 "FullScreen" => {
143 let ((x, y, width, height), _) = Column::column(statement, next_index)?;
144 let x: i32 = x;
145 let y: i32 = y;
146 let width: i32 = width;
147 let height: i32 = height;
148 SerializedWindowBounds(WindowBounds::Fullscreen(Bounds {
149 origin: point(x.into(), y.into()),
150 size: size(width.into(), height.into()),
151 }))
152 }
153 _ => bail!("Window State did not have a valid string"),
154 };
155
156 Ok((status, next_index + 4))
157 }
158}
159
160#[derive(Clone, Debug, PartialEq)]
161struct SerializedDevicePixels(gpui::DevicePixels);
162impl sqlez::bindable::StaticColumnCount for SerializedDevicePixels {}
163
164impl sqlez::bindable::Bind for SerializedDevicePixels {
165 fn bind(
166 &self,
167 statement: &sqlez::statement::Statement,
168 start_index: i32,
169 ) -> anyhow::Result<i32> {
170 let this: i32 = self.0.into();
171 this.bind(statement, start_index)
172 }
173}
174
175define_connection! {
176 // Current schema shape using pseudo-rust syntax:
177 //
178 // workspaces(
179 // workspace_id: usize, // Primary key for workspaces
180 // local_paths: Bincode<Vec<PathBuf>>,
181 // local_paths_order: Bincode<Vec<usize>>,
182 // dock_visible: bool, // Deprecated
183 // dock_anchor: DockAnchor, // Deprecated
184 // dock_pane: Option<usize>, // Deprecated
185 // left_sidebar_open: boolean,
186 // timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
187 // window_state: String, // WindowBounds Discriminant
188 // window_x: Option<f32>, // WindowBounds::Fixed RectF x
189 // window_y: Option<f32>, // WindowBounds::Fixed RectF y
190 // window_width: Option<f32>, // WindowBounds::Fixed RectF width
191 // window_height: Option<f32>, // WindowBounds::Fixed RectF height
192 // display: Option<Uuid>, // Display id
193 // fullscreen: Option<bool>, // Is the window fullscreen?
194 // centered_layout: Option<bool>, // Is the Centered Layout mode activated?
195 // )
196 //
197 // pane_groups(
198 // group_id: usize, // Primary key for pane_groups
199 // workspace_id: usize, // References workspaces table
200 // parent_group_id: Option<usize>, // None indicates that this is the root node
201 // position: Optiopn<usize>, // None indicates that this is the root node
202 // axis: Option<Axis>, // 'Vertical', 'Horizontal'
203 // flexes: Option<Vec<f32>>, // A JSON array of floats
204 // )
205 //
206 // panes(
207 // pane_id: usize, // Primary key for panes
208 // workspace_id: usize, // References workspaces table
209 // active: bool,
210 // )
211 //
212 // center_panes(
213 // pane_id: usize, // Primary key for center_panes
214 // parent_group_id: Option<usize>, // References pane_groups. If none, this is the root
215 // position: Option<usize>, // None indicates this is the root
216 // )
217 //
218 // CREATE TABLE items(
219 // item_id: usize, // This is the item's view id, so this is not unique
220 // workspace_id: usize, // References workspaces table
221 // pane_id: usize, // References panes table
222 // kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global
223 // position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column
224 // active: bool, // Indicates if this item is the active one in the pane
225 // preview: bool // Indicates if this item is a preview item
226 // )
227 pub static ref DB: WorkspaceDb<()> =
228 &[sql!(
229 CREATE TABLE workspaces(
230 workspace_id INTEGER PRIMARY KEY,
231 workspace_location BLOB UNIQUE,
232 dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
233 dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
234 dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
235 left_sidebar_open INTEGER, // Boolean
236 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
237 FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
238 ) STRICT;
239
240 CREATE TABLE pane_groups(
241 group_id INTEGER PRIMARY KEY,
242 workspace_id INTEGER NOT NULL,
243 parent_group_id INTEGER, // NULL indicates that this is a root node
244 position INTEGER, // NULL indicates that this is a root node
245 axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
246 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
247 ON DELETE CASCADE
248 ON UPDATE CASCADE,
249 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
250 ) STRICT;
251
252 CREATE TABLE panes(
253 pane_id INTEGER PRIMARY KEY,
254 workspace_id INTEGER NOT NULL,
255 active INTEGER NOT NULL, // Boolean
256 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
257 ON DELETE CASCADE
258 ON UPDATE CASCADE
259 ) STRICT;
260
261 CREATE TABLE center_panes(
262 pane_id INTEGER PRIMARY KEY,
263 parent_group_id INTEGER, // NULL means that this is a root pane
264 position INTEGER, // NULL means that this is a root pane
265 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
266 ON DELETE CASCADE,
267 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
268 ) STRICT;
269
270 CREATE TABLE items(
271 item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
272 workspace_id INTEGER NOT NULL,
273 pane_id INTEGER NOT NULL,
274 kind TEXT NOT NULL,
275 position INTEGER NOT NULL,
276 active INTEGER NOT NULL,
277 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
278 ON DELETE CASCADE
279 ON UPDATE CASCADE,
280 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
281 ON DELETE CASCADE,
282 PRIMARY KEY(item_id, workspace_id)
283 ) STRICT;
284 ),
285 sql!(
286 ALTER TABLE workspaces ADD COLUMN window_state TEXT;
287 ALTER TABLE workspaces ADD COLUMN window_x REAL;
288 ALTER TABLE workspaces ADD COLUMN window_y REAL;
289 ALTER TABLE workspaces ADD COLUMN window_width REAL;
290 ALTER TABLE workspaces ADD COLUMN window_height REAL;
291 ALTER TABLE workspaces ADD COLUMN display BLOB;
292 ),
293 // Drop foreign key constraint from workspaces.dock_pane to panes table.
294 sql!(
295 CREATE TABLE workspaces_2(
296 workspace_id INTEGER PRIMARY KEY,
297 workspace_location BLOB UNIQUE,
298 dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
299 dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
300 dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
301 left_sidebar_open INTEGER, // Boolean
302 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
303 window_state TEXT,
304 window_x REAL,
305 window_y REAL,
306 window_width REAL,
307 window_height REAL,
308 display BLOB
309 ) STRICT;
310 INSERT INTO workspaces_2 SELECT * FROM workspaces;
311 DROP TABLE workspaces;
312 ALTER TABLE workspaces_2 RENAME TO workspaces;
313 ),
314 // Add panels related information
315 sql!(
316 ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
317 ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
318 ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
319 ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
320 ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
321 ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
322 ),
323 // Add panel zoom persistence
324 sql!(
325 ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
326 ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
327 ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
328 ),
329 // Add pane group flex data
330 sql!(
331 ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
332 ),
333 // Add fullscreen field to workspace
334 // Deprecated, `WindowBounds` holds the fullscreen state now.
335 // Preserving so users can downgrade Zed.
336 sql!(
337 ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
338 ),
339 // Add preview field to items
340 sql!(
341 ALTER TABLE items ADD COLUMN preview INTEGER; //bool
342 ),
343 // Add centered_layout field to workspace
344 sql!(
345 ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
346 ),
347 sql!(
348 CREATE TABLE remote_projects (
349 remote_project_id INTEGER NOT NULL UNIQUE,
350 path TEXT,
351 dev_server_name TEXT
352 );
353 ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
354 ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
355 ),
356 sql!(
357 DROP TABLE remote_projects;
358 CREATE TABLE dev_server_projects (
359 id INTEGER NOT NULL UNIQUE,
360 path TEXT,
361 dev_server_name TEXT
362 );
363 ALTER TABLE workspaces DROP COLUMN remote_project_id;
364 ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
365 ),
366 sql!(
367 ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
368 ),
369 ];
370}
371
372impl WorkspaceDb {
373 /// Returns a serialized workspace for the given worktree_roots. If the passed array
374 /// is empty, the most recent workspace is returned instead. If no workspace for the
375 /// passed roots is stored, returns none.
376 pub(crate) fn workspace_for_roots<P: AsRef<Path>>(
377 &self,
378 worktree_roots: &[P],
379 ) -> Option<SerializedWorkspace> {
380 let local_paths = LocalPaths::new(worktree_roots);
381
382 // Note that we re-assign the workspace_id here in case it's empty
383 // and we've grabbed the most recent workspace
384 let (
385 workspace_id,
386 local_paths,
387 local_paths_order,
388 dev_server_project_id,
389 window_bounds,
390 display,
391 centered_layout,
392 docks,
393 ): (
394 WorkspaceId,
395 Option<LocalPaths>,
396 Option<LocalPathsOrder>,
397 Option<u64>,
398 Option<SerializedWindowBounds>,
399 Option<Uuid>,
400 Option<bool>,
401 DockStructure,
402 ) = self
403 .select_row_bound(sql! {
404 SELECT
405 workspace_id,
406 local_paths,
407 local_paths_order,
408 dev_server_project_id,
409 window_state,
410 window_x,
411 window_y,
412 window_width,
413 window_height,
414 display,
415 centered_layout,
416 left_dock_visible,
417 left_dock_active_panel,
418 left_dock_zoom,
419 right_dock_visible,
420 right_dock_active_panel,
421 right_dock_zoom,
422 bottom_dock_visible,
423 bottom_dock_active_panel,
424 bottom_dock_zoom
425 FROM workspaces
426 WHERE local_paths = ?
427 })
428 .and_then(|mut prepared_statement| (prepared_statement)(&local_paths))
429 .context("No workspaces found")
430 .warn_on_err()
431 .flatten()?;
432
433 let location = if let Some(dev_server_project_id) = dev_server_project_id {
434 let dev_server_project: SerializedDevServerProject = self
435 .select_row_bound(sql! {
436 SELECT id, path, dev_server_name
437 FROM dev_server_projects
438 WHERE id = ?
439 })
440 .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id))
441 .context("No remote project found")
442 .warn_on_err()
443 .flatten()?;
444 SerializedWorkspaceLocation::DevServer(dev_server_project)
445 } else if let Some(local_paths) = local_paths {
446 match local_paths_order {
447 Some(order) => SerializedWorkspaceLocation::Local(local_paths, order),
448 None => {
449 let order = LocalPathsOrder::default_for_paths(&local_paths);
450 SerializedWorkspaceLocation::Local(local_paths, order)
451 }
452 }
453 } else {
454 return None;
455 };
456
457 Some(SerializedWorkspace {
458 id: workspace_id,
459 location,
460 center_group: self
461 .get_center_pane_group(workspace_id)
462 .context("Getting center group")
463 .log_err()?,
464 window_bounds,
465 centered_layout: centered_layout.unwrap_or(false),
466 display,
467 docks,
468 })
469 }
470
471 pub(crate) fn workspace_for_dev_server_project(
472 &self,
473 dev_server_project_id: DevServerProjectId,
474 ) -> Option<SerializedWorkspace> {
475 // Note that we re-assign the workspace_id here in case it's empty
476 // and we've grabbed the most recent workspace
477 let (
478 workspace_id,
479 local_paths,
480 local_paths_order,
481 dev_server_project_id,
482 window_bounds,
483 display,
484 centered_layout,
485 docks,
486 ): (
487 WorkspaceId,
488 Option<LocalPaths>,
489 Option<LocalPathsOrder>,
490 Option<u64>,
491 Option<SerializedWindowBounds>,
492 Option<Uuid>,
493 Option<bool>,
494 DockStructure,
495 ) = self
496 .select_row_bound(sql! {
497 SELECT
498 workspace_id,
499 local_paths,
500 local_paths_order,
501 dev_server_project_id,
502 window_state,
503 window_x,
504 window_y,
505 window_width,
506 window_height,
507 display,
508 centered_layout,
509 left_dock_visible,
510 left_dock_active_panel,
511 left_dock_zoom,
512 right_dock_visible,
513 right_dock_active_panel,
514 right_dock_zoom,
515 bottom_dock_visible,
516 bottom_dock_active_panel,
517 bottom_dock_zoom
518 FROM workspaces
519 WHERE dev_server_project_id = ?
520 })
521 .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id.0))
522 .context("No workspaces found")
523 .warn_on_err()
524 .flatten()?;
525
526 let location = if let Some(dev_server_project_id) = dev_server_project_id {
527 let dev_server_project: SerializedDevServerProject = self
528 .select_row_bound(sql! {
529 SELECT id, path, dev_server_name
530 FROM dev_server_projects
531 WHERE id = ?
532 })
533 .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id))
534 .context("No remote project found")
535 .warn_on_err()
536 .flatten()?;
537 SerializedWorkspaceLocation::DevServer(dev_server_project)
538 } else if let Some(local_paths) = local_paths {
539 match local_paths_order {
540 Some(order) => SerializedWorkspaceLocation::Local(local_paths, order),
541 None => {
542 let order = LocalPathsOrder::default_for_paths(&local_paths);
543 SerializedWorkspaceLocation::Local(local_paths, order)
544 }
545 }
546 } else {
547 return None;
548 };
549
550 Some(SerializedWorkspace {
551 id: workspace_id,
552 location,
553 center_group: self
554 .get_center_pane_group(workspace_id)
555 .context("Getting center group")
556 .log_err()?,
557 window_bounds,
558 centered_layout: centered_layout.unwrap_or(false),
559 display,
560 docks,
561 })
562 }
563
564 /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
565 /// that used this workspace previously
566 pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
567 self.write(move |conn| {
568 conn.with_savepoint("update_worktrees", || {
569 // Clear out panes and pane_groups
570 conn.exec_bound(sql!(
571 DELETE FROM pane_groups WHERE workspace_id = ?1;
572 DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
573 .context("Clearing old panes")?;
574
575 match workspace.location {
576 SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => {
577 conn.exec_bound(sql!(
578 DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ?
579 ))?((&local_paths, workspace.id))
580 .context("clearing out old locations")?;
581
582 // Upsert
583 conn.exec_bound(sql!(
584 INSERT INTO workspaces(
585 workspace_id,
586 local_paths,
587 local_paths_order,
588 left_dock_visible,
589 left_dock_active_panel,
590 left_dock_zoom,
591 right_dock_visible,
592 right_dock_active_panel,
593 right_dock_zoom,
594 bottom_dock_visible,
595 bottom_dock_active_panel,
596 bottom_dock_zoom,
597 timestamp
598 )
599 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, CURRENT_TIMESTAMP)
600 ON CONFLICT DO
601 UPDATE SET
602 local_paths = ?2,
603 local_paths_order = ?3,
604 left_dock_visible = ?4,
605 left_dock_active_panel = ?5,
606 left_dock_zoom = ?6,
607 right_dock_visible = ?7,
608 right_dock_active_panel = ?8,
609 right_dock_zoom = ?9,
610 bottom_dock_visible = ?10,
611 bottom_dock_active_panel = ?11,
612 bottom_dock_zoom = ?12,
613 timestamp = CURRENT_TIMESTAMP
614 ))?((workspace.id, &local_paths, &local_paths_order, workspace.docks))
615 .context("Updating workspace")?;
616 }
617 SerializedWorkspaceLocation::DevServer(dev_server_project) => {
618 conn.exec_bound(sql!(
619 DELETE FROM workspaces WHERE dev_server_project_id = ? AND workspace_id != ?
620 ))?((dev_server_project.id.0, workspace.id))
621 .context("clearing out old locations")?;
622
623 conn.exec_bound(sql!(
624 INSERT INTO dev_server_projects(
625 id,
626 path,
627 dev_server_name
628 ) VALUES (?1, ?2, ?3)
629 ON CONFLICT DO
630 UPDATE SET
631 path = ?2,
632 dev_server_name = ?3
633 ))?(&dev_server_project)?;
634
635 // Upsert
636 conn.exec_bound(sql!(
637 INSERT INTO workspaces(
638 workspace_id,
639 dev_server_project_id,
640 left_dock_visible,
641 left_dock_active_panel,
642 left_dock_zoom,
643 right_dock_visible,
644 right_dock_active_panel,
645 right_dock_zoom,
646 bottom_dock_visible,
647 bottom_dock_active_panel,
648 bottom_dock_zoom,
649 timestamp
650 )
651 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
652 ON CONFLICT DO
653 UPDATE SET
654 dev_server_project_id = ?2,
655 left_dock_visible = ?3,
656 left_dock_active_panel = ?4,
657 left_dock_zoom = ?5,
658 right_dock_visible = ?6,
659 right_dock_active_panel = ?7,
660 right_dock_zoom = ?8,
661 bottom_dock_visible = ?9,
662 bottom_dock_active_panel = ?10,
663 bottom_dock_zoom = ?11,
664 timestamp = CURRENT_TIMESTAMP
665 ))?((
666 workspace.id,
667 dev_server_project.id.0,
668 workspace.docks,
669 ))
670 .context("Updating workspace")?;
671 }
672 }
673
674 // Save center pane group
675 Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
676 .context("save pane group in save workspace")?;
677
678 Ok(())
679 })
680 .log_err();
681 })
682 .await;
683 }
684
685 query! {
686 pub async fn next_id() -> Result<WorkspaceId> {
687 INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
688 }
689 }
690
691 query! {
692 fn recent_workspaces() -> Result<Vec<(WorkspaceId, LocalPaths, Option<u64>)>> {
693 SELECT workspace_id, local_paths, dev_server_project_id
694 FROM workspaces
695 WHERE local_paths IS NOT NULL OR dev_server_project_id IS NOT NULL
696 ORDER BY timestamp DESC
697 }
698 }
699
700 query! {
701 fn dev_server_projects() -> Result<Vec<SerializedDevServerProject>> {
702 SELECT id, path, dev_server_name
703 FROM dev_server_projects
704 }
705 }
706
707 pub(crate) fn last_window(
708 &self,
709 ) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
710 let mut prepared_query =
711 self.select::<(Option<Uuid>, Option<SerializedWindowBounds>)>(sql!(
712 SELECT
713 display,
714 window_state, window_x, window_y, window_width, window_height
715 FROM workspaces
716 WHERE local_paths
717 IS NOT NULL
718 ORDER BY timestamp DESC
719 LIMIT 1
720 ))?;
721 let result = prepared_query()?;
722 Ok(result.into_iter().next().unwrap_or_else(|| (None, None)))
723 }
724
725 query! {
726 pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
727 DELETE FROM workspaces
728 WHERE workspace_id IS ?
729 }
730 }
731
732 pub async fn delete_workspace_by_dev_server_project_id(
733 &self,
734 id: DevServerProjectId,
735 ) -> Result<()> {
736 self.write(move |conn| {
737 conn.exec_bound(sql!(
738 DELETE FROM dev_server_projects WHERE id = ?
739 ))?(id.0)?;
740 conn.exec_bound(sql!(
741 DELETE FROM workspaces
742 WHERE dev_server_project_id IS ?
743 ))?(id.0)
744 })
745 .await
746 }
747
748 // Returns the recent locations which are still valid on disk and deletes ones which no longer
749 // exist.
750 pub async fn recent_workspaces_on_disk(
751 &self,
752 ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation)>> {
753 let mut result = Vec::new();
754 let mut delete_tasks = Vec::new();
755 let dev_server_projects = self.dev_server_projects()?;
756
757 for (id, location, dev_server_project_id) in self.recent_workspaces()? {
758 if let Some(dev_server_project_id) = dev_server_project_id.map(DevServerProjectId) {
759 if let Some(dev_server_project) = dev_server_projects
760 .iter()
761 .find(|rp| rp.id == dev_server_project_id)
762 {
763 result.push((id, dev_server_project.clone().into()));
764 } else {
765 delete_tasks.push(self.delete_workspace_by_id(id));
766 }
767 continue;
768 }
769
770 if location.paths().iter().all(|path| path.exists())
771 && location.paths().iter().any(|path| path.is_dir())
772 {
773 result.push((id, location.into()));
774 } else {
775 delete_tasks.push(self.delete_workspace_by_id(id));
776 }
777 }
778
779 futures::future::join_all(delete_tasks).await;
780 Ok(result)
781 }
782
783 pub async fn last_workspace(&self) -> Result<Option<LocalPaths>> {
784 Ok(self
785 .recent_workspaces_on_disk()
786 .await?
787 .into_iter()
788 .filter_map(|(_, location)| match location {
789 SerializedWorkspaceLocation::Local(local_paths, _) => Some(local_paths),
790 SerializedWorkspaceLocation::DevServer(_) => None,
791 })
792 .next())
793 }
794
795 fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
796 Ok(self
797 .get_pane_group(workspace_id, None)?
798 .into_iter()
799 .next()
800 .unwrap_or_else(|| {
801 SerializedPaneGroup::Pane(SerializedPane {
802 active: true,
803 children: vec![],
804 })
805 }))
806 }
807
808 fn get_pane_group(
809 &self,
810 workspace_id: WorkspaceId,
811 group_id: Option<GroupId>,
812 ) -> Result<Vec<SerializedPaneGroup>> {
813 type GroupKey = (Option<GroupId>, WorkspaceId);
814 type GroupOrPane = (
815 Option<GroupId>,
816 Option<SerializedAxis>,
817 Option<PaneId>,
818 Option<bool>,
819 Option<String>,
820 );
821 self.select_bound::<GroupKey, GroupOrPane>(sql!(
822 SELECT group_id, axis, pane_id, active, flexes
823 FROM (SELECT
824 group_id,
825 axis,
826 NULL as pane_id,
827 NULL as active,
828 position,
829 parent_group_id,
830 workspace_id,
831 flexes
832 FROM pane_groups
833 UNION
834 SELECT
835 NULL,
836 NULL,
837 center_panes.pane_id,
838 panes.active as active,
839 position,
840 parent_group_id,
841 panes.workspace_id as workspace_id,
842 NULL
843 FROM center_panes
844 JOIN panes ON center_panes.pane_id = panes.pane_id)
845 WHERE parent_group_id IS ? AND workspace_id = ?
846 ORDER BY position
847 ))?((group_id, workspace_id))?
848 .into_iter()
849 .map(|(group_id, axis, pane_id, active, flexes)| {
850 if let Some((group_id, axis)) = group_id.zip(axis) {
851 let flexes = flexes
852 .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
853 .transpose()?;
854
855 Ok(SerializedPaneGroup::Group {
856 axis,
857 children: self.get_pane_group(workspace_id, Some(group_id))?,
858 flexes,
859 })
860 } else if let Some((pane_id, active)) = pane_id.zip(active) {
861 Ok(SerializedPaneGroup::Pane(SerializedPane::new(
862 self.get_items(pane_id)?,
863 active,
864 )))
865 } else {
866 bail!("Pane Group Child was neither a pane group or a pane");
867 }
868 })
869 // Filter out panes and pane groups which don't have any children or items
870 .filter(|pane_group| match pane_group {
871 Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
872 Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
873 _ => true,
874 })
875 .collect::<Result<_>>()
876 }
877
878 fn save_pane_group(
879 conn: &Connection,
880 workspace_id: WorkspaceId,
881 pane_group: &SerializedPaneGroup,
882 parent: Option<(GroupId, usize)>,
883 ) -> Result<()> {
884 match pane_group {
885 SerializedPaneGroup::Group {
886 axis,
887 children,
888 flexes,
889 } => {
890 let (parent_id, position) = parent.unzip();
891
892 let flex_string = flexes
893 .as_ref()
894 .map(|flexes| serde_json::json!(flexes).to_string());
895
896 let group_id = conn.select_row_bound::<_, i64>(sql!(
897 INSERT INTO pane_groups(
898 workspace_id,
899 parent_group_id,
900 position,
901 axis,
902 flexes
903 )
904 VALUES (?, ?, ?, ?, ?)
905 RETURNING group_id
906 ))?((
907 workspace_id,
908 parent_id,
909 position,
910 *axis,
911 flex_string,
912 ))?
913 .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
914
915 for (position, group) in children.iter().enumerate() {
916 Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
917 }
918
919 Ok(())
920 }
921 SerializedPaneGroup::Pane(pane) => {
922 Self::save_pane(conn, workspace_id, pane, parent)?;
923 Ok(())
924 }
925 }
926 }
927
928 fn save_pane(
929 conn: &Connection,
930 workspace_id: WorkspaceId,
931 pane: &SerializedPane,
932 parent: Option<(GroupId, usize)>,
933 ) -> Result<PaneId> {
934 let pane_id = conn.select_row_bound::<_, i64>(sql!(
935 INSERT INTO panes(workspace_id, active)
936 VALUES (?, ?)
937 RETURNING pane_id
938 ))?((workspace_id, pane.active))?
939 .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
940
941 let (parent_id, order) = parent.unzip();
942 conn.exec_bound(sql!(
943 INSERT INTO center_panes(pane_id, parent_group_id, position)
944 VALUES (?, ?, ?)
945 ))?((pane_id, parent_id, order))?;
946
947 Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
948
949 Ok(pane_id)
950 }
951
952 fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
953 self.select_bound(sql!(
954 SELECT kind, item_id, active, preview FROM items
955 WHERE pane_id = ?
956 ORDER BY position
957 ))?(pane_id)
958 }
959
960 fn save_items(
961 conn: &Connection,
962 workspace_id: WorkspaceId,
963 pane_id: PaneId,
964 items: &[SerializedItem],
965 ) -> Result<()> {
966 let mut insert = conn.exec_bound(sql!(
967 INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
968 )).context("Preparing insertion")?;
969 for (position, item) in items.iter().enumerate() {
970 insert((workspace_id, pane_id, position, item))?;
971 }
972
973 Ok(())
974 }
975
976 query! {
977 pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
978 UPDATE workspaces
979 SET timestamp = CURRENT_TIMESTAMP
980 WHERE workspace_id = ?
981 }
982 }
983
984 query! {
985 pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
986 UPDATE workspaces
987 SET window_state = ?2,
988 window_x = ?3,
989 window_y = ?4,
990 window_width = ?5,
991 window_height = ?6,
992 display = ?7
993 WHERE workspace_id = ?1
994 }
995 }
996
997 query! {
998 pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
999 UPDATE workspaces
1000 SET centered_layout = ?2
1001 WHERE workspace_id = ?1
1002 }
1003 }
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008 use super::*;
1009 use db::open_test_db;
1010 use gpui;
1011
1012 #[gpui::test]
1013 async fn test_next_id_stability() {
1014 env_logger::try_init().ok();
1015
1016 let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
1017
1018 db.write(|conn| {
1019 conn.migrate(
1020 "test_table",
1021 &[sql!(
1022 CREATE TABLE test_table(
1023 text TEXT,
1024 workspace_id INTEGER,
1025 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1026 ON DELETE CASCADE
1027 ) STRICT;
1028 )],
1029 )
1030 .unwrap();
1031 })
1032 .await;
1033
1034 let id = db.next_id().await.unwrap();
1035 // Assert the empty row got inserted
1036 assert_eq!(
1037 Some(id),
1038 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
1039 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
1040 ))
1041 .unwrap()(id)
1042 .unwrap()
1043 );
1044
1045 db.write(move |conn| {
1046 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1047 .unwrap()(("test-text-1", id))
1048 .unwrap()
1049 })
1050 .await;
1051
1052 let test_text_1 = db
1053 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1054 .unwrap()(1)
1055 .unwrap()
1056 .unwrap();
1057 assert_eq!(test_text_1, "test-text-1");
1058 }
1059
1060 #[gpui::test]
1061 async fn test_workspace_id_stability() {
1062 env_logger::try_init().ok();
1063
1064 let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
1065
1066 db.write(|conn| {
1067 conn.migrate(
1068 "test_table",
1069 &[sql!(
1070 CREATE TABLE test_table(
1071 text TEXT,
1072 workspace_id INTEGER,
1073 FOREIGN KEY(workspace_id)
1074 REFERENCES workspaces(workspace_id)
1075 ON DELETE CASCADE
1076 ) STRICT;)],
1077 )
1078 })
1079 .await
1080 .unwrap();
1081
1082 let mut workspace_1 = SerializedWorkspace {
1083 id: WorkspaceId(1),
1084 location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
1085 center_group: Default::default(),
1086 window_bounds: Default::default(),
1087 display: Default::default(),
1088 docks: Default::default(),
1089 centered_layout: false,
1090 };
1091
1092 let workspace_2 = SerializedWorkspace {
1093 id: WorkspaceId(2),
1094 location: LocalPaths::new(["/tmp"]).into(),
1095 center_group: Default::default(),
1096 window_bounds: Default::default(),
1097 display: Default::default(),
1098 docks: Default::default(),
1099 centered_layout: false,
1100 };
1101
1102 db.save_workspace(workspace_1.clone()).await;
1103
1104 db.write(|conn| {
1105 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1106 .unwrap()(("test-text-1", 1))
1107 .unwrap();
1108 })
1109 .await;
1110
1111 db.save_workspace(workspace_2.clone()).await;
1112
1113 db.write(|conn| {
1114 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1115 .unwrap()(("test-text-2", 2))
1116 .unwrap();
1117 })
1118 .await;
1119
1120 workspace_1.location = LocalPaths::new(["/tmp", "/tmp3"]).into();
1121 db.save_workspace(workspace_1.clone()).await;
1122 db.save_workspace(workspace_1).await;
1123 db.save_workspace(workspace_2).await;
1124
1125 let test_text_2 = db
1126 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1127 .unwrap()(2)
1128 .unwrap()
1129 .unwrap();
1130 assert_eq!(test_text_2, "test-text-2");
1131
1132 let test_text_1 = db
1133 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1134 .unwrap()(1)
1135 .unwrap()
1136 .unwrap();
1137 assert_eq!(test_text_1, "test-text-1");
1138 }
1139
1140 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
1141 SerializedPaneGroup::Group {
1142 axis: SerializedAxis(axis),
1143 flexes: None,
1144 children,
1145 }
1146 }
1147
1148 #[gpui::test]
1149 async fn test_full_workspace_serialization() {
1150 env_logger::try_init().ok();
1151
1152 let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
1153
1154 // -----------------
1155 // | 1,2 | 5,6 |
1156 // | - - - | |
1157 // | 3,4 | |
1158 // -----------------
1159 let center_group = group(
1160 Axis::Horizontal,
1161 vec![
1162 group(
1163 Axis::Vertical,
1164 vec![
1165 SerializedPaneGroup::Pane(SerializedPane::new(
1166 vec![
1167 SerializedItem::new("Terminal", 5, false, false),
1168 SerializedItem::new("Terminal", 6, true, false),
1169 ],
1170 false,
1171 )),
1172 SerializedPaneGroup::Pane(SerializedPane::new(
1173 vec![
1174 SerializedItem::new("Terminal", 7, true, false),
1175 SerializedItem::new("Terminal", 8, false, false),
1176 ],
1177 false,
1178 )),
1179 ],
1180 ),
1181 SerializedPaneGroup::Pane(SerializedPane::new(
1182 vec![
1183 SerializedItem::new("Terminal", 9, false, false),
1184 SerializedItem::new("Terminal", 10, true, false),
1185 ],
1186 false,
1187 )),
1188 ],
1189 );
1190
1191 let workspace = SerializedWorkspace {
1192 id: WorkspaceId(5),
1193 location: SerializedWorkspaceLocation::Local(
1194 LocalPaths::new(["/tmp", "/tmp2"]),
1195 LocalPathsOrder::new([1, 0]),
1196 ),
1197 center_group,
1198 window_bounds: Default::default(),
1199 display: Default::default(),
1200 docks: Default::default(),
1201 centered_layout: false,
1202 };
1203
1204 db.save_workspace(workspace.clone()).await;
1205
1206 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
1207 assert_eq!(workspace, round_trip_workspace.unwrap());
1208
1209 // Test guaranteed duplicate IDs
1210 db.save_workspace(workspace.clone()).await;
1211 db.save_workspace(workspace.clone()).await;
1212
1213 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
1214 assert_eq!(workspace, round_trip_workspace.unwrap());
1215 }
1216
1217 #[gpui::test]
1218 async fn test_workspace_assignment() {
1219 env_logger::try_init().ok();
1220
1221 let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
1222
1223 let workspace_1 = SerializedWorkspace {
1224 id: WorkspaceId(1),
1225 location: SerializedWorkspaceLocation::Local(
1226 LocalPaths::new(["/tmp", "/tmp2"]),
1227 LocalPathsOrder::new([0, 1]),
1228 ),
1229 center_group: Default::default(),
1230 window_bounds: Default::default(),
1231 display: Default::default(),
1232 docks: Default::default(),
1233 centered_layout: false,
1234 };
1235
1236 let mut workspace_2 = SerializedWorkspace {
1237 id: WorkspaceId(2),
1238 location: LocalPaths::new(["/tmp"]).into(),
1239 center_group: Default::default(),
1240 window_bounds: Default::default(),
1241 display: Default::default(),
1242 docks: Default::default(),
1243 centered_layout: false,
1244 };
1245
1246 db.save_workspace(workspace_1.clone()).await;
1247 db.save_workspace(workspace_2.clone()).await;
1248
1249 // Test that paths are treated as a set
1250 assert_eq!(
1251 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1252 workspace_1
1253 );
1254 assert_eq!(
1255 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
1256 workspace_1
1257 );
1258
1259 // Make sure that other keys work
1260 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
1261 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
1262
1263 // Test 'mutate' case of updating a pre-existing id
1264 workspace_2.location = LocalPaths::new(["/tmp", "/tmp2"]).into();
1265
1266 db.save_workspace(workspace_2.clone()).await;
1267 assert_eq!(
1268 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1269 workspace_2
1270 );
1271
1272 // Test other mechanism for mutating
1273 let mut workspace_3 = SerializedWorkspace {
1274 id: WorkspaceId(3),
1275 location: SerializedWorkspaceLocation::Local(
1276 LocalPaths::new(&["/tmp", "/tmp2"]),
1277 LocalPathsOrder::new([1, 0]),
1278 ),
1279 center_group: Default::default(),
1280 window_bounds: Default::default(),
1281 display: Default::default(),
1282 docks: Default::default(),
1283 centered_layout: false,
1284 };
1285
1286 db.save_workspace(workspace_3.clone()).await;
1287 assert_eq!(
1288 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1289 workspace_3
1290 );
1291
1292 // Make sure that updating paths differently also works
1293 workspace_3.location = LocalPaths::new(["/tmp3", "/tmp4", "/tmp2"]).into();
1294 db.save_workspace(workspace_3.clone()).await;
1295 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
1296 assert_eq!(
1297 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
1298 .unwrap(),
1299 workspace_3
1300 );
1301 }
1302
1303 use crate::persistence::model::SerializedWorkspace;
1304 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
1305
1306 fn default_workspace<P: AsRef<Path>>(
1307 workspace_id: &[P],
1308 center_group: &SerializedPaneGroup,
1309 ) -> SerializedWorkspace {
1310 SerializedWorkspace {
1311 id: WorkspaceId(4),
1312 location: LocalPaths::new(workspace_id).into(),
1313 center_group: center_group.clone(),
1314 window_bounds: Default::default(),
1315 display: Default::default(),
1316 docks: Default::default(),
1317 centered_layout: false,
1318 }
1319 }
1320
1321 #[gpui::test]
1322 async fn test_simple_split() {
1323 env_logger::try_init().ok();
1324
1325 let db = WorkspaceDb(open_test_db("simple_split").await);
1326
1327 // -----------------
1328 // | 1,2 | 5,6 |
1329 // | - - - | |
1330 // | 3,4 | |
1331 // -----------------
1332 let center_pane = group(
1333 Axis::Horizontal,
1334 vec![
1335 group(
1336 Axis::Vertical,
1337 vec![
1338 SerializedPaneGroup::Pane(SerializedPane::new(
1339 vec![
1340 SerializedItem::new("Terminal", 1, false, false),
1341 SerializedItem::new("Terminal", 2, true, false),
1342 ],
1343 false,
1344 )),
1345 SerializedPaneGroup::Pane(SerializedPane::new(
1346 vec![
1347 SerializedItem::new("Terminal", 4, false, false),
1348 SerializedItem::new("Terminal", 3, true, false),
1349 ],
1350 true,
1351 )),
1352 ],
1353 ),
1354 SerializedPaneGroup::Pane(SerializedPane::new(
1355 vec![
1356 SerializedItem::new("Terminal", 5, true, false),
1357 SerializedItem::new("Terminal", 6, false, false),
1358 ],
1359 false,
1360 )),
1361 ],
1362 );
1363
1364 let workspace = default_workspace(&["/tmp"], ¢er_pane);
1365
1366 db.save_workspace(workspace.clone()).await;
1367
1368 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
1369
1370 assert_eq!(workspace.center_group, new_workspace.center_group);
1371 }
1372
1373 #[gpui::test]
1374 async fn test_cleanup_panes() {
1375 env_logger::try_init().ok();
1376
1377 let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
1378
1379 let center_pane = group(
1380 Axis::Horizontal,
1381 vec![
1382 group(
1383 Axis::Vertical,
1384 vec![
1385 SerializedPaneGroup::Pane(SerializedPane::new(
1386 vec![
1387 SerializedItem::new("Terminal", 1, false, false),
1388 SerializedItem::new("Terminal", 2, true, false),
1389 ],
1390 false,
1391 )),
1392 SerializedPaneGroup::Pane(SerializedPane::new(
1393 vec![
1394 SerializedItem::new("Terminal", 4, false, false),
1395 SerializedItem::new("Terminal", 3, true, false),
1396 ],
1397 true,
1398 )),
1399 ],
1400 ),
1401 SerializedPaneGroup::Pane(SerializedPane::new(
1402 vec![
1403 SerializedItem::new("Terminal", 5, false, false),
1404 SerializedItem::new("Terminal", 6, true, false),
1405 ],
1406 false,
1407 )),
1408 ],
1409 );
1410
1411 let id = &["/tmp"];
1412
1413 let mut workspace = default_workspace(id, ¢er_pane);
1414
1415 db.save_workspace(workspace.clone()).await;
1416
1417 workspace.center_group = group(
1418 Axis::Vertical,
1419 vec![
1420 SerializedPaneGroup::Pane(SerializedPane::new(
1421 vec![
1422 SerializedItem::new("Terminal", 1, false, false),
1423 SerializedItem::new("Terminal", 2, true, false),
1424 ],
1425 false,
1426 )),
1427 SerializedPaneGroup::Pane(SerializedPane::new(
1428 vec![
1429 SerializedItem::new("Terminal", 4, true, false),
1430 SerializedItem::new("Terminal", 3, false, false),
1431 ],
1432 true,
1433 )),
1434 ],
1435 );
1436
1437 db.save_workspace(workspace.clone()).await;
1438
1439 let new_workspace = db.workspace_for_roots(id).unwrap();
1440
1441 assert_eq!(workspace.center_group, new_workspace.center_group);
1442 }
1443}