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, WindowId};
9
10use remote::ssh_session::SshProjectId;
11use sqlez::{
12 bindable::{Bind, Column, StaticColumnCount},
13 statement::Statement,
14};
15
16use ui::px;
17use util::{maybe, ResultExt};
18use uuid::Uuid;
19
20use crate::WorkspaceId;
21
22use model::{
23 GroupId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
24 SerializedSshProject, SerializedWorkspace,
25};
26
27use self::model::{
28 DockStructure, LocalPathsOrder, SerializedDevServerProject, SerializedWorkspaceLocation,
29};
30
31#[derive(Copy, Clone, Debug, PartialEq)]
32pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
33impl sqlez::bindable::StaticColumnCount for SerializedAxis {}
34impl sqlez::bindable::Bind for SerializedAxis {
35 fn bind(
36 &self,
37 statement: &sqlez::statement::Statement,
38 start_index: i32,
39 ) -> anyhow::Result<i32> {
40 match self.0 {
41 gpui::Axis::Horizontal => "Horizontal",
42 gpui::Axis::Vertical => "Vertical",
43 }
44 .bind(statement, start_index)
45 }
46}
47
48impl sqlez::bindable::Column for SerializedAxis {
49 fn column(
50 statement: &mut sqlez::statement::Statement,
51 start_index: i32,
52 ) -> anyhow::Result<(Self, i32)> {
53 String::column(statement, start_index).and_then(|(axis_text, next_index)| {
54 Ok((
55 match axis_text.as_str() {
56 "Horizontal" => Self(Axis::Horizontal),
57 "Vertical" => Self(Axis::Vertical),
58 _ => anyhow::bail!("Stored serialized item kind is incorrect"),
59 },
60 next_index,
61 ))
62 })
63 }
64}
65
66#[derive(Copy, Clone, Debug, PartialEq, Default)]
67pub(crate) struct SerializedWindowBounds(pub(crate) WindowBounds);
68
69impl StaticColumnCount for SerializedWindowBounds {
70 fn column_count() -> usize {
71 5
72 }
73}
74
75impl Bind for SerializedWindowBounds {
76 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
77 match self.0 {
78 WindowBounds::Windowed(bounds) => {
79 let next_index = statement.bind(&"Windowed", start_index)?;
80 statement.bind(
81 &(
82 SerializedPixels(bounds.origin.x),
83 SerializedPixels(bounds.origin.y),
84 SerializedPixels(bounds.size.width),
85 SerializedPixels(bounds.size.height),
86 ),
87 next_index,
88 )
89 }
90 WindowBounds::Maximized(bounds) => {
91 let next_index = statement.bind(&"Maximized", start_index)?;
92 statement.bind(
93 &(
94 SerializedPixels(bounds.origin.x),
95 SerializedPixels(bounds.origin.y),
96 SerializedPixels(bounds.size.width),
97 SerializedPixels(bounds.size.height),
98 ),
99 next_index,
100 )
101 }
102 WindowBounds::Fullscreen(bounds) => {
103 let next_index = statement.bind(&"FullScreen", start_index)?;
104 statement.bind(
105 &(
106 SerializedPixels(bounds.origin.x),
107 SerializedPixels(bounds.origin.y),
108 SerializedPixels(bounds.size.width),
109 SerializedPixels(bounds.size.height),
110 ),
111 next_index,
112 )
113 }
114 }
115 }
116}
117
118impl Column for SerializedWindowBounds {
119 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
120 let (window_state, next_index) = String::column(statement, start_index)?;
121 let ((x, y, width, height), _): ((i32, i32, i32, i32), _) =
122 Column::column(statement, next_index)?;
123 let bounds = Bounds {
124 origin: point(px(x as f32), px(y as f32)),
125 size: size(px(width as f32), px(height as f32)),
126 };
127
128 let status = match window_state.as_str() {
129 "Windowed" | "Fixed" => SerializedWindowBounds(WindowBounds::Windowed(bounds)),
130 "Maximized" => SerializedWindowBounds(WindowBounds::Maximized(bounds)),
131 "FullScreen" => SerializedWindowBounds(WindowBounds::Fullscreen(bounds)),
132 _ => bail!("Window State did not have a valid string"),
133 };
134
135 Ok((status, next_index + 4))
136 }
137}
138
139#[derive(Clone, Debug, PartialEq)]
140struct SerializedPixels(gpui::Pixels);
141impl sqlez::bindable::StaticColumnCount for SerializedPixels {}
142
143impl sqlez::bindable::Bind for SerializedPixels {
144 fn bind(
145 &self,
146 statement: &sqlez::statement::Statement,
147 start_index: i32,
148 ) -> anyhow::Result<i32> {
149 let this: i32 = self.0 .0 as i32;
150 this.bind(statement, start_index)
151 }
152}
153
154define_connection! {
155 // Current schema shape using pseudo-rust syntax:
156 //
157 // workspaces(
158 // workspace_id: usize, // Primary key for workspaces
159 // local_paths: Bincode<Vec<PathBuf>>,
160 // local_paths_order: Bincode<Vec<usize>>,
161 // dock_visible: bool, // Deprecated
162 // dock_anchor: DockAnchor, // Deprecated
163 // dock_pane: Option<usize>, // Deprecated
164 // left_sidebar_open: boolean,
165 // timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
166 // window_state: String, // WindowBounds Discriminant
167 // window_x: Option<f32>, // WindowBounds::Fixed RectF x
168 // window_y: Option<f32>, // WindowBounds::Fixed RectF y
169 // window_width: Option<f32>, // WindowBounds::Fixed RectF width
170 // window_height: Option<f32>, // WindowBounds::Fixed RectF height
171 // display: Option<Uuid>, // Display id
172 // fullscreen: Option<bool>, // Is the window fullscreen?
173 // centered_layout: Option<bool>, // Is the Centered Layout mode activated?
174 // session_id: Option<String>, // Session id
175 // window_id: Option<u64>, // Window Id
176 // )
177 //
178 // pane_groups(
179 // group_id: usize, // Primary key for pane_groups
180 // workspace_id: usize, // References workspaces table
181 // parent_group_id: Option<usize>, // None indicates that this is the root node
182 // position: Optiopn<usize>, // None indicates that this is the root node
183 // axis: Option<Axis>, // 'Vertical', 'Horizontal'
184 // flexes: Option<Vec<f32>>, // A JSON array of floats
185 // )
186 //
187 // panes(
188 // pane_id: usize, // Primary key for panes
189 // workspace_id: usize, // References workspaces table
190 // active: bool,
191 // )
192 //
193 // center_panes(
194 // pane_id: usize, // Primary key for center_panes
195 // parent_group_id: Option<usize>, // References pane_groups. If none, this is the root
196 // position: Option<usize>, // None indicates this is the root
197 // )
198 //
199 // CREATE TABLE items(
200 // item_id: usize, // This is the item's view id, so this is not unique
201 // workspace_id: usize, // References workspaces table
202 // pane_id: usize, // References panes table
203 // kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global
204 // position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column
205 // active: bool, // Indicates if this item is the active one in the pane
206 // preview: bool // Indicates if this item is a preview item
207 // )
208 pub static ref DB: WorkspaceDb<()> =
209 &[sql!(
210 CREATE TABLE workspaces(
211 workspace_id INTEGER PRIMARY KEY,
212 workspace_location BLOB UNIQUE,
213 dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
214 dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
215 dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
216 left_sidebar_open INTEGER, // Boolean
217 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
218 FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
219 ) STRICT;
220
221 CREATE TABLE pane_groups(
222 group_id INTEGER PRIMARY KEY,
223 workspace_id INTEGER NOT NULL,
224 parent_group_id INTEGER, // NULL indicates that this is a root node
225 position INTEGER, // NULL indicates that this is a root node
226 axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
227 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
228 ON DELETE CASCADE
229 ON UPDATE CASCADE,
230 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
231 ) STRICT;
232
233 CREATE TABLE panes(
234 pane_id INTEGER PRIMARY KEY,
235 workspace_id INTEGER NOT NULL,
236 active INTEGER NOT NULL, // Boolean
237 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
238 ON DELETE CASCADE
239 ON UPDATE CASCADE
240 ) STRICT;
241
242 CREATE TABLE center_panes(
243 pane_id INTEGER PRIMARY KEY,
244 parent_group_id INTEGER, // NULL means that this is a root pane
245 position INTEGER, // NULL means that this is a root pane
246 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
247 ON DELETE CASCADE,
248 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
249 ) STRICT;
250
251 CREATE TABLE items(
252 item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
253 workspace_id INTEGER NOT NULL,
254 pane_id INTEGER NOT NULL,
255 kind TEXT NOT NULL,
256 position INTEGER NOT NULL,
257 active INTEGER NOT NULL,
258 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
259 ON DELETE CASCADE
260 ON UPDATE CASCADE,
261 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
262 ON DELETE CASCADE,
263 PRIMARY KEY(item_id, workspace_id)
264 ) STRICT;
265 ),
266 sql!(
267 ALTER TABLE workspaces ADD COLUMN window_state TEXT;
268 ALTER TABLE workspaces ADD COLUMN window_x REAL;
269 ALTER TABLE workspaces ADD COLUMN window_y REAL;
270 ALTER TABLE workspaces ADD COLUMN window_width REAL;
271 ALTER TABLE workspaces ADD COLUMN window_height REAL;
272 ALTER TABLE workspaces ADD COLUMN display BLOB;
273 ),
274 // Drop foreign key constraint from workspaces.dock_pane to panes table.
275 sql!(
276 CREATE TABLE workspaces_2(
277 workspace_id INTEGER PRIMARY KEY,
278 workspace_location BLOB UNIQUE,
279 dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
280 dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
281 dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
282 left_sidebar_open INTEGER, // Boolean
283 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
284 window_state TEXT,
285 window_x REAL,
286 window_y REAL,
287 window_width REAL,
288 window_height REAL,
289 display BLOB
290 ) STRICT;
291 INSERT INTO workspaces_2 SELECT * FROM workspaces;
292 DROP TABLE workspaces;
293 ALTER TABLE workspaces_2 RENAME TO workspaces;
294 ),
295 // Add panels related information
296 sql!(
297 ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
298 ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
299 ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
300 ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
301 ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
302 ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
303 ),
304 // Add panel zoom persistence
305 sql!(
306 ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
307 ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
308 ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
309 ),
310 // Add pane group flex data
311 sql!(
312 ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
313 ),
314 // Add fullscreen field to workspace
315 // Deprecated, `WindowBounds` holds the fullscreen state now.
316 // Preserving so users can downgrade Zed.
317 sql!(
318 ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
319 ),
320 // Add preview field to items
321 sql!(
322 ALTER TABLE items ADD COLUMN preview INTEGER; //bool
323 ),
324 // Add centered_layout field to workspace
325 sql!(
326 ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
327 ),
328 sql!(
329 CREATE TABLE remote_projects (
330 remote_project_id INTEGER NOT NULL UNIQUE,
331 path TEXT,
332 dev_server_name TEXT
333 );
334 ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
335 ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
336 ),
337 sql!(
338 DROP TABLE remote_projects;
339 CREATE TABLE dev_server_projects (
340 id INTEGER NOT NULL UNIQUE,
341 path TEXT,
342 dev_server_name TEXT
343 );
344 ALTER TABLE workspaces DROP COLUMN remote_project_id;
345 ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
346 ),
347 sql!(
348 ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
349 ),
350 sql!(
351 ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
352 ),
353 sql!(
354 ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
355 ),
356 sql!(
357 ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
358 ),
359 sql!(
360 CREATE TABLE ssh_projects (
361 id INTEGER PRIMARY KEY,
362 host TEXT NOT NULL,
363 port INTEGER,
364 path TEXT NOT NULL,
365 user TEXT
366 );
367 ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
368 ),
369 sql!(
370 ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
371 ),
372 ];
373}
374
375impl WorkspaceDb {
376 /// Returns a serialized workspace for the given worktree_roots. If the passed array
377 /// is empty, the most recent workspace is returned instead. If no workspace for the
378 /// passed roots is stored, returns none.
379 pub(crate) fn workspace_for_roots<P: AsRef<Path>>(
380 &self,
381 worktree_roots: &[P],
382 ) -> Option<SerializedWorkspace> {
383 // paths are sorted before db interactions to ensure that the order of the paths
384 // doesn't affect the workspace selection for existing workspaces
385 let local_paths = LocalPaths::new(worktree_roots);
386
387 // Note that we re-assign the workspace_id here in case it's empty
388 // and we've grabbed the most recent workspace
389 let (
390 workspace_id,
391 local_paths,
392 local_paths_order,
393 window_bounds,
394 display,
395 centered_layout,
396 docks,
397 window_id,
398 ): (
399 WorkspaceId,
400 Option<LocalPaths>,
401 Option<LocalPathsOrder>,
402 Option<SerializedWindowBounds>,
403 Option<Uuid>,
404 Option<bool>,
405 DockStructure,
406 Option<u64>,
407 ) = self
408 .select_row_bound(sql! {
409 SELECT
410 workspace_id,
411 local_paths,
412 local_paths_order,
413 window_state,
414 window_x,
415 window_y,
416 window_width,
417 window_height,
418 display,
419 centered_layout,
420 left_dock_visible,
421 left_dock_active_panel,
422 left_dock_zoom,
423 right_dock_visible,
424 right_dock_active_panel,
425 right_dock_zoom,
426 bottom_dock_visible,
427 bottom_dock_active_panel,
428 bottom_dock_zoom,
429 window_id
430 FROM workspaces
431 WHERE local_paths = ?
432 })
433 .and_then(|mut prepared_statement| (prepared_statement)(&local_paths))
434 .context("No workspaces found")
435 .warn_on_err()
436 .flatten()?;
437
438 let local_paths = local_paths?;
439 let location = match local_paths_order {
440 Some(order) => SerializedWorkspaceLocation::Local(local_paths, order),
441 None => {
442 let order = LocalPathsOrder::default_for_paths(&local_paths);
443 SerializedWorkspaceLocation::Local(local_paths, order)
444 }
445 };
446
447 Some(SerializedWorkspace {
448 id: workspace_id,
449 location,
450 center_group: self
451 .get_center_pane_group(workspace_id)
452 .context("Getting center group")
453 .log_err()?,
454 window_bounds,
455 centered_layout: centered_layout.unwrap_or(false),
456 display,
457 docks,
458 session_id: None,
459 window_id,
460 })
461 }
462
463 pub(crate) fn workspace_for_dev_server_project(
464 &self,
465 dev_server_project_id: DevServerProjectId,
466 ) -> Option<SerializedWorkspace> {
467 // Note that we re-assign the workspace_id here in case it's empty
468 // and we've grabbed the most recent workspace
469 let (
470 workspace_id,
471 dev_server_project_id,
472 window_bounds,
473 display,
474 centered_layout,
475 docks,
476 window_id,
477 ): (
478 WorkspaceId,
479 Option<u64>,
480 Option<SerializedWindowBounds>,
481 Option<Uuid>,
482 Option<bool>,
483 DockStructure,
484 Option<u64>,
485 ) = self
486 .select_row_bound(sql! {
487 SELECT
488 workspace_id,
489 dev_server_project_id,
490 window_state,
491 window_x,
492 window_y,
493 window_width,
494 window_height,
495 display,
496 centered_layout,
497 left_dock_visible,
498 left_dock_active_panel,
499 left_dock_zoom,
500 right_dock_visible,
501 right_dock_active_panel,
502 right_dock_zoom,
503 bottom_dock_visible,
504 bottom_dock_active_panel,
505 bottom_dock_zoom,
506 window_id
507 FROM workspaces
508 WHERE dev_server_project_id = ?
509 })
510 .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id.0))
511 .context("No workspaces found")
512 .warn_on_err()
513 .flatten()?;
514
515 let dev_server_project_id = dev_server_project_id?;
516
517 let dev_server_project: SerializedDevServerProject = self
518 .select_row_bound(sql! {
519 SELECT id, path, dev_server_name
520 FROM dev_server_projects
521 WHERE id = ?
522 })
523 .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id))
524 .context("No remote project found")
525 .warn_on_err()
526 .flatten()?;
527
528 let location = SerializedWorkspaceLocation::DevServer(dev_server_project);
529
530 Some(SerializedWorkspace {
531 id: workspace_id,
532 location,
533 center_group: self
534 .get_center_pane_group(workspace_id)
535 .context("Getting center group")
536 .log_err()?,
537 window_bounds,
538 centered_layout: centered_layout.unwrap_or(false),
539 display,
540 docks,
541 session_id: None,
542 window_id,
543 })
544 }
545
546 pub(crate) fn workspace_for_ssh_project(
547 &self,
548 ssh_project: &SerializedSshProject,
549 ) -> Option<SerializedWorkspace> {
550 let (workspace_id, window_bounds, display, centered_layout, docks, window_id): (
551 WorkspaceId,
552 Option<SerializedWindowBounds>,
553 Option<Uuid>,
554 Option<bool>,
555 DockStructure,
556 Option<u64>,
557 ) = self
558 .select_row_bound(sql! {
559 SELECT
560 workspace_id,
561 window_state,
562 window_x,
563 window_y,
564 window_width,
565 window_height,
566 display,
567 centered_layout,
568 left_dock_visible,
569 left_dock_active_panel,
570 left_dock_zoom,
571 right_dock_visible,
572 right_dock_active_panel,
573 right_dock_zoom,
574 bottom_dock_visible,
575 bottom_dock_active_panel,
576 bottom_dock_zoom,
577 window_id
578 FROM workspaces
579 WHERE ssh_project_id = ?
580 })
581 .and_then(|mut prepared_statement| (prepared_statement)(ssh_project.id.0))
582 .context("No workspaces found")
583 .warn_on_err()
584 .flatten()?;
585
586 Some(SerializedWorkspace {
587 id: workspace_id,
588 location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()),
589 center_group: self
590 .get_center_pane_group(workspace_id)
591 .context("Getting center group")
592 .log_err()?,
593 window_bounds,
594 centered_layout: centered_layout.unwrap_or(false),
595 display,
596 docks,
597 session_id: None,
598 window_id,
599 })
600 }
601
602 /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
603 /// that used this workspace previously
604 pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
605 self.write(move |conn| {
606 conn.with_savepoint("update_worktrees", || {
607 // Clear out panes and pane_groups
608 conn.exec_bound(sql!(
609 DELETE FROM pane_groups WHERE workspace_id = ?1;
610 DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
611 .context("Clearing old panes")?;
612
613 match workspace.location {
614 SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => {
615 conn.exec_bound(sql!(
616 DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ?
617 ))?((&local_paths, workspace.id))
618 .context("clearing out old locations")?;
619
620 // Upsert
621 let query = sql!(
622 INSERT INTO workspaces(
623 workspace_id,
624 local_paths,
625 local_paths_order,
626 left_dock_visible,
627 left_dock_active_panel,
628 left_dock_zoom,
629 right_dock_visible,
630 right_dock_active_panel,
631 right_dock_zoom,
632 bottom_dock_visible,
633 bottom_dock_active_panel,
634 bottom_dock_zoom,
635 session_id,
636 window_id,
637 timestamp
638 )
639 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, CURRENT_TIMESTAMP)
640 ON CONFLICT DO
641 UPDATE SET
642 local_paths = ?2,
643 local_paths_order = ?3,
644 left_dock_visible = ?4,
645 left_dock_active_panel = ?5,
646 left_dock_zoom = ?6,
647 right_dock_visible = ?7,
648 right_dock_active_panel = ?8,
649 right_dock_zoom = ?9,
650 bottom_dock_visible = ?10,
651 bottom_dock_active_panel = ?11,
652 bottom_dock_zoom = ?12,
653 session_id = ?13,
654 window_id = ?14,
655 timestamp = CURRENT_TIMESTAMP
656 );
657 let mut prepared_query = conn.exec_bound(query)?;
658 let args = (workspace.id, &local_paths, &local_paths_order, workspace.docks, workspace.session_id, workspace.window_id);
659
660 prepared_query(args).context("Updating workspace")?;
661 }
662 SerializedWorkspaceLocation::DevServer(dev_server_project) => {
663 conn.exec_bound(sql!(
664 DELETE FROM workspaces WHERE dev_server_project_id = ? AND workspace_id != ?
665 ))?((dev_server_project.id.0, workspace.id))
666 .context("clearing out old locations")?;
667
668 conn.exec_bound(sql!(
669 INSERT INTO dev_server_projects(
670 id,
671 path,
672 dev_server_name
673 ) VALUES (?1, ?2, ?3)
674 ON CONFLICT DO
675 UPDATE SET
676 path = ?2,
677 dev_server_name = ?3
678 ))?(&dev_server_project)?;
679
680 // Upsert
681 conn.exec_bound(sql!(
682 INSERT INTO workspaces(
683 workspace_id,
684 dev_server_project_id,
685 left_dock_visible,
686 left_dock_active_panel,
687 left_dock_zoom,
688 right_dock_visible,
689 right_dock_active_panel,
690 right_dock_zoom,
691 bottom_dock_visible,
692 bottom_dock_active_panel,
693 bottom_dock_zoom,
694 timestamp
695 )
696 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
697 ON CONFLICT DO
698 UPDATE SET
699 dev_server_project_id = ?2,
700 left_dock_visible = ?3,
701 left_dock_active_panel = ?4,
702 left_dock_zoom = ?5,
703 right_dock_visible = ?6,
704 right_dock_active_panel = ?7,
705 right_dock_zoom = ?8,
706 bottom_dock_visible = ?9,
707 bottom_dock_active_panel = ?10,
708 bottom_dock_zoom = ?11,
709 timestamp = CURRENT_TIMESTAMP
710 ))?((
711 workspace.id,
712 dev_server_project.id.0,
713 workspace.docks,
714 ))
715 .context("Updating workspace")?;
716 },
717 SerializedWorkspaceLocation::Ssh(ssh_project) => {
718 conn.exec_bound(sql!(
719 DELETE FROM workspaces WHERE ssh_project_id = ? AND workspace_id != ?
720 ))?((ssh_project.id.0, workspace.id))
721 .context("clearing out old locations")?;
722
723 // Upsert
724 conn.exec_bound(sql!(
725 INSERT INTO workspaces(
726 workspace_id,
727 ssh_project_id,
728 left_dock_visible,
729 left_dock_active_panel,
730 left_dock_zoom,
731 right_dock_visible,
732 right_dock_active_panel,
733 right_dock_zoom,
734 bottom_dock_visible,
735 bottom_dock_active_panel,
736 bottom_dock_zoom,
737 session_id,
738 window_id,
739 timestamp
740 )
741 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, CURRENT_TIMESTAMP)
742 ON CONFLICT DO
743 UPDATE SET
744 ssh_project_id = ?2,
745 left_dock_visible = ?3,
746 left_dock_active_panel = ?4,
747 left_dock_zoom = ?5,
748 right_dock_visible = ?6,
749 right_dock_active_panel = ?7,
750 right_dock_zoom = ?8,
751 bottom_dock_visible = ?9,
752 bottom_dock_active_panel = ?10,
753 bottom_dock_zoom = ?11,
754 session_id = ?12,
755 window_id = ?13,
756 timestamp = CURRENT_TIMESTAMP
757 ))?((
758 workspace.id,
759 ssh_project.id.0,
760 workspace.docks,
761 workspace.session_id,
762 workspace.window_id
763 ))
764 .context("Updating workspace")?;
765 }
766 }
767
768 // Save center pane group
769 Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
770 .context("save pane group in save workspace")?;
771
772 Ok(())
773 })
774 .log_err();
775 })
776 .await;
777 }
778
779 pub(crate) async fn get_or_create_ssh_project(
780 &self,
781 host: String,
782 port: Option<u16>,
783 paths: Vec<String>,
784 user: Option<String>,
785 ) -> Result<SerializedSshProject> {
786 let paths = serde_json::to_string(&paths)?;
787 if let Some(project) = self
788 .get_ssh_project(host.clone(), port, paths.clone(), user.clone())
789 .await?
790 {
791 Ok(project)
792 } else {
793 self.insert_ssh_project(host, port, paths, user)
794 .await?
795 .ok_or_else(|| anyhow!("failed to insert ssh project"))
796 }
797 }
798
799 query! {
800 async fn get_ssh_project(host: String, port: Option<u16>, paths: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
801 SELECT id, host, port, paths, user
802 FROM ssh_projects
803 WHERE host IS ? AND port IS ? AND paths IS ? AND user IS ?
804 LIMIT 1
805 }
806 }
807
808 query! {
809 async fn insert_ssh_project(host: String, port: Option<u16>, paths: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
810 INSERT INTO ssh_projects(
811 host,
812 port,
813 paths,
814 user
815 ) VALUES (?1, ?2, ?3, ?4)
816 RETURNING id, host, port, paths, user
817 }
818 }
819
820 query! {
821 pub async fn next_id() -> Result<WorkspaceId> {
822 INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
823 }
824 }
825
826 query! {
827 fn recent_workspaces() -> Result<Vec<(WorkspaceId, LocalPaths, LocalPathsOrder, Option<u64>, Option<u64>)>> {
828 SELECT workspace_id, local_paths, local_paths_order, dev_server_project_id, ssh_project_id
829 FROM workspaces
830 WHERE local_paths IS NOT NULL
831 OR dev_server_project_id IS NOT NULL
832 OR ssh_project_id IS NOT NULL
833 ORDER BY timestamp DESC
834 }
835 }
836
837 query! {
838 fn session_workspaces(session_id: String) -> Result<Vec<(LocalPaths, LocalPathsOrder, Option<u64>, Option<u64>)>> {
839 SELECT local_paths, local_paths_order, window_id, ssh_project_id
840 FROM workspaces
841 WHERE session_id = ?1 AND dev_server_project_id IS NULL
842 ORDER BY timestamp DESC
843 }
844 }
845
846 query! {
847 fn dev_server_projects() -> Result<Vec<SerializedDevServerProject>> {
848 SELECT id, path, dev_server_name
849 FROM dev_server_projects
850 }
851 }
852
853 query! {
854 fn ssh_projects() -> Result<Vec<SerializedSshProject>> {
855 SELECT id, host, port, paths, user
856 FROM ssh_projects
857 }
858 }
859
860 query! {
861 fn ssh_project(id: u64) -> Result<SerializedSshProject> {
862 SELECT id, host, port, paths, user
863 FROM ssh_projects
864 WHERE id = ?
865 }
866 }
867
868 pub(crate) fn last_window(
869 &self,
870 ) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
871 let mut prepared_query =
872 self.select::<(Option<Uuid>, Option<SerializedWindowBounds>)>(sql!(
873 SELECT
874 display,
875 window_state, window_x, window_y, window_width, window_height
876 FROM workspaces
877 WHERE local_paths
878 IS NOT NULL
879 ORDER BY timestamp DESC
880 LIMIT 1
881 ))?;
882 let result = prepared_query()?;
883 Ok(result.into_iter().next().unwrap_or((None, None)))
884 }
885
886 query! {
887 pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
888 DELETE FROM workspaces
889 WHERE workspace_id IS ?
890 }
891 }
892
893 pub async fn delete_workspace_by_dev_server_project_id(
894 &self,
895 id: DevServerProjectId,
896 ) -> Result<()> {
897 self.write(move |conn| {
898 conn.exec_bound(sql!(
899 DELETE FROM dev_server_projects WHERE id = ?
900 ))?(id.0)?;
901 conn.exec_bound(sql!(
902 DELETE FROM workspaces
903 WHERE dev_server_project_id IS ?
904 ))?(id.0)
905 })
906 .await
907 }
908
909 // Returns the recent locations which are still valid on disk and deletes ones which no longer
910 // exist.
911 pub async fn recent_workspaces_on_disk(
912 &self,
913 ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation)>> {
914 let mut result = Vec::new();
915 let mut delete_tasks = Vec::new();
916 let dev_server_projects = self.dev_server_projects()?;
917 let ssh_projects = self.ssh_projects()?;
918
919 for (id, location, order, dev_server_project_id, ssh_project_id) in
920 self.recent_workspaces()?
921 {
922 if let Some(dev_server_project_id) = dev_server_project_id.map(DevServerProjectId) {
923 if let Some(dev_server_project) = dev_server_projects
924 .iter()
925 .find(|rp| rp.id == dev_server_project_id)
926 {
927 result.push((id, dev_server_project.clone().into()));
928 } else {
929 delete_tasks.push(self.delete_workspace_by_id(id));
930 }
931 continue;
932 }
933
934 if let Some(ssh_project_id) = ssh_project_id.map(SshProjectId) {
935 if let Some(ssh_project) = ssh_projects.iter().find(|rp| rp.id == ssh_project_id) {
936 result.push((id, SerializedWorkspaceLocation::Ssh(ssh_project.clone())));
937 } else {
938 delete_tasks.push(self.delete_workspace_by_id(id));
939 }
940 continue;
941 }
942
943 if location.paths().iter().all(|path| path.exists())
944 && location.paths().iter().any(|path| path.is_dir())
945 {
946 result.push((id, SerializedWorkspaceLocation::Local(location, order)));
947 } else {
948 delete_tasks.push(self.delete_workspace_by_id(id));
949 }
950 }
951
952 futures::future::join_all(delete_tasks).await;
953 Ok(result)
954 }
955
956 pub async fn last_workspace(&self) -> Result<Option<SerializedWorkspaceLocation>> {
957 Ok(self
958 .recent_workspaces_on_disk()
959 .await?
960 .into_iter()
961 .next()
962 .map(|(_, location)| location))
963 }
964
965 // Returns the locations of the workspaces that were still opened when the last
966 // session was closed (i.e. when Zed was quit).
967 // If `last_session_window_order` is provided, the returned locations are ordered
968 // according to that.
969 pub fn last_session_workspace_locations(
970 &self,
971 last_session_id: &str,
972 last_session_window_stack: Option<Vec<WindowId>>,
973 ) -> Result<Vec<SerializedWorkspaceLocation>> {
974 let mut workspaces = Vec::new();
975
976 for (location, order, window_id, ssh_project_id) in
977 self.session_workspaces(last_session_id.to_owned())?
978 {
979 if let Some(ssh_project_id) = ssh_project_id {
980 let location = SerializedWorkspaceLocation::Ssh(self.ssh_project(ssh_project_id)?);
981 workspaces.push((location, window_id.map(WindowId::from)));
982 } else if location.paths().iter().all(|path| path.exists())
983 && location.paths().iter().any(|path| path.is_dir())
984 {
985 let location = SerializedWorkspaceLocation::Local(location, order);
986 workspaces.push((location, window_id.map(WindowId::from)));
987 }
988 }
989
990 if let Some(stack) = last_session_window_stack {
991 workspaces.sort_by_key(|(_, window_id)| {
992 window_id
993 .and_then(|id| stack.iter().position(|&order_id| order_id == id))
994 .unwrap_or(usize::MAX)
995 });
996 }
997
998 Ok(workspaces
999 .into_iter()
1000 .map(|(paths, _)| paths)
1001 .collect::<Vec<_>>())
1002 }
1003
1004 fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1005 Ok(self
1006 .get_pane_group(workspace_id, None)?
1007 .into_iter()
1008 .next()
1009 .unwrap_or_else(|| {
1010 SerializedPaneGroup::Pane(SerializedPane {
1011 active: true,
1012 children: vec![],
1013 pinned_count: 0,
1014 })
1015 }))
1016 }
1017
1018 fn get_pane_group(
1019 &self,
1020 workspace_id: WorkspaceId,
1021 group_id: Option<GroupId>,
1022 ) -> Result<Vec<SerializedPaneGroup>> {
1023 type GroupKey = (Option<GroupId>, WorkspaceId);
1024 type GroupOrPane = (
1025 Option<GroupId>,
1026 Option<SerializedAxis>,
1027 Option<PaneId>,
1028 Option<bool>,
1029 Option<usize>,
1030 Option<String>,
1031 );
1032 self.select_bound::<GroupKey, GroupOrPane>(sql!(
1033 SELECT group_id, axis, pane_id, active, pinned_count, flexes
1034 FROM (SELECT
1035 group_id,
1036 axis,
1037 NULL as pane_id,
1038 NULL as active,
1039 NULL as pinned_count,
1040 position,
1041 parent_group_id,
1042 workspace_id,
1043 flexes
1044 FROM pane_groups
1045 UNION
1046 SELECT
1047 NULL,
1048 NULL,
1049 center_panes.pane_id,
1050 panes.active as active,
1051 pinned_count,
1052 position,
1053 parent_group_id,
1054 panes.workspace_id as workspace_id,
1055 NULL
1056 FROM center_panes
1057 JOIN panes ON center_panes.pane_id = panes.pane_id)
1058 WHERE parent_group_id IS ? AND workspace_id = ?
1059 ORDER BY position
1060 ))?((group_id, workspace_id))?
1061 .into_iter()
1062 .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1063 let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1064 if let Some((group_id, axis)) = group_id.zip(axis) {
1065 let flexes = flexes
1066 .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1067 .transpose()?;
1068
1069 Ok(SerializedPaneGroup::Group {
1070 axis,
1071 children: self.get_pane_group(workspace_id, Some(group_id))?,
1072 flexes,
1073 })
1074 } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
1075 Ok(SerializedPaneGroup::Pane(SerializedPane::new(
1076 self.get_items(pane_id)?,
1077 active,
1078 pinned_count,
1079 )))
1080 } else {
1081 bail!("Pane Group Child was neither a pane group or a pane");
1082 }
1083 })
1084 // Filter out panes and pane groups which don't have any children or items
1085 .filter(|pane_group| match pane_group {
1086 Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
1087 Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
1088 _ => true,
1089 })
1090 .collect::<Result<_>>()
1091 }
1092
1093 fn save_pane_group(
1094 conn: &Connection,
1095 workspace_id: WorkspaceId,
1096 pane_group: &SerializedPaneGroup,
1097 parent: Option<(GroupId, usize)>,
1098 ) -> Result<()> {
1099 match pane_group {
1100 SerializedPaneGroup::Group {
1101 axis,
1102 children,
1103 flexes,
1104 } => {
1105 let (parent_id, position) = parent.unzip();
1106
1107 let flex_string = flexes
1108 .as_ref()
1109 .map(|flexes| serde_json::json!(flexes).to_string());
1110
1111 let group_id = conn.select_row_bound::<_, i64>(sql!(
1112 INSERT INTO pane_groups(
1113 workspace_id,
1114 parent_group_id,
1115 position,
1116 axis,
1117 flexes
1118 )
1119 VALUES (?, ?, ?, ?, ?)
1120 RETURNING group_id
1121 ))?((
1122 workspace_id,
1123 parent_id,
1124 position,
1125 *axis,
1126 flex_string,
1127 ))?
1128 .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
1129
1130 for (position, group) in children.iter().enumerate() {
1131 Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
1132 }
1133
1134 Ok(())
1135 }
1136 SerializedPaneGroup::Pane(pane) => {
1137 Self::save_pane(conn, workspace_id, pane, parent)?;
1138 Ok(())
1139 }
1140 }
1141 }
1142
1143 fn save_pane(
1144 conn: &Connection,
1145 workspace_id: WorkspaceId,
1146 pane: &SerializedPane,
1147 parent: Option<(GroupId, usize)>,
1148 ) -> Result<PaneId> {
1149 let pane_id = conn.select_row_bound::<_, i64>(sql!(
1150 INSERT INTO panes(workspace_id, active, pinned_count)
1151 VALUES (?, ?, ?)
1152 RETURNING pane_id
1153 ))?((workspace_id, pane.active, pane.pinned_count))?
1154 .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
1155
1156 let (parent_id, order) = parent.unzip();
1157 conn.exec_bound(sql!(
1158 INSERT INTO center_panes(pane_id, parent_group_id, position)
1159 VALUES (?, ?, ?)
1160 ))?((pane_id, parent_id, order))?;
1161
1162 Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
1163
1164 Ok(pane_id)
1165 }
1166
1167 fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
1168 self.select_bound(sql!(
1169 SELECT kind, item_id, active, preview FROM items
1170 WHERE pane_id = ?
1171 ORDER BY position
1172 ))?(pane_id)
1173 }
1174
1175 fn save_items(
1176 conn: &Connection,
1177 workspace_id: WorkspaceId,
1178 pane_id: PaneId,
1179 items: &[SerializedItem],
1180 ) -> Result<()> {
1181 let mut insert = conn.exec_bound(sql!(
1182 INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
1183 )).context("Preparing insertion")?;
1184 for (position, item) in items.iter().enumerate() {
1185 insert((workspace_id, pane_id, position, item))?;
1186 }
1187
1188 Ok(())
1189 }
1190
1191 query! {
1192 pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
1193 UPDATE workspaces
1194 SET timestamp = CURRENT_TIMESTAMP
1195 WHERE workspace_id = ?
1196 }
1197 }
1198
1199 query! {
1200 pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
1201 UPDATE workspaces
1202 SET window_state = ?2,
1203 window_x = ?3,
1204 window_y = ?4,
1205 window_width = ?5,
1206 window_height = ?6,
1207 display = ?7
1208 WHERE workspace_id = ?1
1209 }
1210 }
1211
1212 query! {
1213 pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
1214 UPDATE workspaces
1215 SET centered_layout = ?2
1216 WHERE workspace_id = ?1
1217 }
1218 }
1219}
1220
1221#[cfg(test)]
1222mod tests {
1223 use super::*;
1224 use crate::persistence::model::SerializedWorkspace;
1225 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
1226 use db::open_test_db;
1227 use gpui::{self};
1228
1229 #[gpui::test]
1230 async fn test_next_id_stability() {
1231 env_logger::try_init().ok();
1232
1233 let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
1234
1235 db.write(|conn| {
1236 conn.migrate(
1237 "test_table",
1238 &[sql!(
1239 CREATE TABLE test_table(
1240 text TEXT,
1241 workspace_id INTEGER,
1242 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1243 ON DELETE CASCADE
1244 ) STRICT;
1245 )],
1246 )
1247 .unwrap();
1248 })
1249 .await;
1250
1251 let id = db.next_id().await.unwrap();
1252 // Assert the empty row got inserted
1253 assert_eq!(
1254 Some(id),
1255 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
1256 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
1257 ))
1258 .unwrap()(id)
1259 .unwrap()
1260 );
1261
1262 db.write(move |conn| {
1263 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1264 .unwrap()(("test-text-1", id))
1265 .unwrap()
1266 })
1267 .await;
1268
1269 let test_text_1 = db
1270 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1271 .unwrap()(1)
1272 .unwrap()
1273 .unwrap();
1274 assert_eq!(test_text_1, "test-text-1");
1275 }
1276
1277 #[gpui::test]
1278 async fn test_workspace_id_stability() {
1279 env_logger::try_init().ok();
1280
1281 let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
1282
1283 db.write(|conn| {
1284 conn.migrate(
1285 "test_table",
1286 &[sql!(
1287 CREATE TABLE test_table(
1288 text TEXT,
1289 workspace_id INTEGER,
1290 FOREIGN KEY(workspace_id)
1291 REFERENCES workspaces(workspace_id)
1292 ON DELETE CASCADE
1293 ) STRICT;)],
1294 )
1295 })
1296 .await
1297 .unwrap();
1298
1299 let mut workspace_1 = SerializedWorkspace {
1300 id: WorkspaceId(1),
1301 location: SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]),
1302 center_group: Default::default(),
1303 window_bounds: Default::default(),
1304 display: Default::default(),
1305 docks: Default::default(),
1306 centered_layout: false,
1307 session_id: None,
1308 window_id: None,
1309 };
1310
1311 let workspace_2 = SerializedWorkspace {
1312 id: WorkspaceId(2),
1313 location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1314 center_group: Default::default(),
1315 window_bounds: Default::default(),
1316 display: Default::default(),
1317 docks: Default::default(),
1318 centered_layout: false,
1319 session_id: None,
1320 window_id: None,
1321 };
1322
1323 db.save_workspace(workspace_1.clone()).await;
1324
1325 db.write(|conn| {
1326 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1327 .unwrap()(("test-text-1", 1))
1328 .unwrap();
1329 })
1330 .await;
1331
1332 db.save_workspace(workspace_2.clone()).await;
1333
1334 db.write(|conn| {
1335 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1336 .unwrap()(("test-text-2", 2))
1337 .unwrap();
1338 })
1339 .await;
1340
1341 workspace_1.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp3"]);
1342 db.save_workspace(workspace_1.clone()).await;
1343 db.save_workspace(workspace_1).await;
1344 db.save_workspace(workspace_2).await;
1345
1346 let test_text_2 = db
1347 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1348 .unwrap()(2)
1349 .unwrap()
1350 .unwrap();
1351 assert_eq!(test_text_2, "test-text-2");
1352
1353 let test_text_1 = db
1354 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1355 .unwrap()(1)
1356 .unwrap()
1357 .unwrap();
1358 assert_eq!(test_text_1, "test-text-1");
1359 }
1360
1361 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
1362 SerializedPaneGroup::Group {
1363 axis: SerializedAxis(axis),
1364 flexes: None,
1365 children,
1366 }
1367 }
1368
1369 #[gpui::test]
1370 async fn test_full_workspace_serialization() {
1371 env_logger::try_init().ok();
1372
1373 let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
1374
1375 // -----------------
1376 // | 1,2 | 5,6 |
1377 // | - - - | |
1378 // | 3,4 | |
1379 // -----------------
1380 let center_group = group(
1381 Axis::Horizontal,
1382 vec![
1383 group(
1384 Axis::Vertical,
1385 vec![
1386 SerializedPaneGroup::Pane(SerializedPane::new(
1387 vec![
1388 SerializedItem::new("Terminal", 5, false, false),
1389 SerializedItem::new("Terminal", 6, true, false),
1390 ],
1391 false,
1392 0,
1393 )),
1394 SerializedPaneGroup::Pane(SerializedPane::new(
1395 vec![
1396 SerializedItem::new("Terminal", 7, true, false),
1397 SerializedItem::new("Terminal", 8, false, false),
1398 ],
1399 false,
1400 0,
1401 )),
1402 ],
1403 ),
1404 SerializedPaneGroup::Pane(SerializedPane::new(
1405 vec![
1406 SerializedItem::new("Terminal", 9, false, false),
1407 SerializedItem::new("Terminal", 10, true, false),
1408 ],
1409 false,
1410 0,
1411 )),
1412 ],
1413 );
1414
1415 let workspace = SerializedWorkspace {
1416 id: WorkspaceId(5),
1417 location: SerializedWorkspaceLocation::Local(
1418 LocalPaths::new(["/tmp", "/tmp2"]),
1419 LocalPathsOrder::new([1, 0]),
1420 ),
1421 center_group,
1422 window_bounds: Default::default(),
1423 display: Default::default(),
1424 docks: Default::default(),
1425 centered_layout: false,
1426 session_id: None,
1427 window_id: Some(999),
1428 };
1429
1430 db.save_workspace(workspace.clone()).await;
1431
1432 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
1433 assert_eq!(workspace, round_trip_workspace.unwrap());
1434
1435 // Test guaranteed duplicate IDs
1436 db.save_workspace(workspace.clone()).await;
1437 db.save_workspace(workspace.clone()).await;
1438
1439 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
1440 assert_eq!(workspace, round_trip_workspace.unwrap());
1441 }
1442
1443 #[gpui::test]
1444 async fn test_workspace_assignment() {
1445 env_logger::try_init().ok();
1446
1447 let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
1448
1449 let workspace_1 = SerializedWorkspace {
1450 id: WorkspaceId(1),
1451 location: SerializedWorkspaceLocation::Local(
1452 LocalPaths::new(["/tmp", "/tmp2"]),
1453 LocalPathsOrder::new([0, 1]),
1454 ),
1455 center_group: Default::default(),
1456 window_bounds: Default::default(),
1457 display: Default::default(),
1458 docks: Default::default(),
1459 centered_layout: false,
1460 session_id: None,
1461 window_id: Some(1),
1462 };
1463
1464 let mut workspace_2 = SerializedWorkspace {
1465 id: WorkspaceId(2),
1466 location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1467 center_group: Default::default(),
1468 window_bounds: Default::default(),
1469 display: Default::default(),
1470 docks: Default::default(),
1471 centered_layout: false,
1472 session_id: None,
1473 window_id: Some(2),
1474 };
1475
1476 db.save_workspace(workspace_1.clone()).await;
1477 db.save_workspace(workspace_2.clone()).await;
1478
1479 // Test that paths are treated as a set
1480 assert_eq!(
1481 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1482 workspace_1
1483 );
1484 assert_eq!(
1485 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
1486 workspace_1
1487 );
1488
1489 // Make sure that other keys work
1490 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
1491 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
1492
1493 // Test 'mutate' case of updating a pre-existing id
1494 workspace_2.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]);
1495
1496 db.save_workspace(workspace_2.clone()).await;
1497 assert_eq!(
1498 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1499 workspace_2
1500 );
1501
1502 // Test other mechanism for mutating
1503 let mut workspace_3 = SerializedWorkspace {
1504 id: WorkspaceId(3),
1505 location: SerializedWorkspaceLocation::Local(
1506 LocalPaths::new(["/tmp", "/tmp2"]),
1507 LocalPathsOrder::new([1, 0]),
1508 ),
1509 center_group: Default::default(),
1510 window_bounds: Default::default(),
1511 display: Default::default(),
1512 docks: Default::default(),
1513 centered_layout: false,
1514 session_id: None,
1515 window_id: Some(3),
1516 };
1517
1518 db.save_workspace(workspace_3.clone()).await;
1519 assert_eq!(
1520 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1521 workspace_3
1522 );
1523
1524 // Make sure that updating paths differently also works
1525 workspace_3.location =
1526 SerializedWorkspaceLocation::from_local_paths(["/tmp3", "/tmp4", "/tmp2"]);
1527 db.save_workspace(workspace_3.clone()).await;
1528 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
1529 assert_eq!(
1530 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
1531 .unwrap(),
1532 workspace_3
1533 );
1534 }
1535
1536 #[gpui::test]
1537 async fn test_session_workspaces() {
1538 env_logger::try_init().ok();
1539
1540 let db = WorkspaceDb(open_test_db("test_serializing_workspaces_session_id").await);
1541
1542 let workspace_1 = SerializedWorkspace {
1543 id: WorkspaceId(1),
1544 location: SerializedWorkspaceLocation::from_local_paths(["/tmp1"]),
1545 center_group: Default::default(),
1546 window_bounds: Default::default(),
1547 display: Default::default(),
1548 docks: Default::default(),
1549 centered_layout: false,
1550 session_id: Some("session-id-1".to_owned()),
1551 window_id: Some(10),
1552 };
1553
1554 let workspace_2 = SerializedWorkspace {
1555 id: WorkspaceId(2),
1556 location: SerializedWorkspaceLocation::from_local_paths(["/tmp2"]),
1557 center_group: Default::default(),
1558 window_bounds: Default::default(),
1559 display: Default::default(),
1560 docks: Default::default(),
1561 centered_layout: false,
1562 session_id: Some("session-id-1".to_owned()),
1563 window_id: Some(20),
1564 };
1565
1566 let workspace_3 = SerializedWorkspace {
1567 id: WorkspaceId(3),
1568 location: SerializedWorkspaceLocation::from_local_paths(["/tmp3"]),
1569 center_group: Default::default(),
1570 window_bounds: Default::default(),
1571 display: Default::default(),
1572 docks: Default::default(),
1573 centered_layout: false,
1574 session_id: Some("session-id-2".to_owned()),
1575 window_id: Some(30),
1576 };
1577
1578 let workspace_4 = SerializedWorkspace {
1579 id: WorkspaceId(4),
1580 location: SerializedWorkspaceLocation::from_local_paths(["/tmp4"]),
1581 center_group: Default::default(),
1582 window_bounds: Default::default(),
1583 display: Default::default(),
1584 docks: Default::default(),
1585 centered_layout: false,
1586 session_id: None,
1587 window_id: None,
1588 };
1589
1590 let ssh_project = db
1591 .get_or_create_ssh_project("my-host".to_string(), Some(1234), vec![], None)
1592 .await
1593 .unwrap();
1594
1595 let workspace_5 = SerializedWorkspace {
1596 id: WorkspaceId(5),
1597 location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()),
1598 center_group: Default::default(),
1599 window_bounds: Default::default(),
1600 display: Default::default(),
1601 docks: Default::default(),
1602 centered_layout: false,
1603 session_id: Some("session-id-2".to_owned()),
1604 window_id: Some(50),
1605 };
1606
1607 let workspace_6 = SerializedWorkspace {
1608 id: WorkspaceId(6),
1609 location: SerializedWorkspaceLocation::Local(
1610 LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
1611 LocalPathsOrder::new([2, 1, 0]),
1612 ),
1613 center_group: Default::default(),
1614 window_bounds: Default::default(),
1615 display: Default::default(),
1616 docks: Default::default(),
1617 centered_layout: false,
1618 session_id: Some("session-id-3".to_owned()),
1619 window_id: Some(60),
1620 };
1621
1622 db.save_workspace(workspace_1.clone()).await;
1623 db.save_workspace(workspace_2.clone()).await;
1624 db.save_workspace(workspace_3.clone()).await;
1625 db.save_workspace(workspace_4.clone()).await;
1626 db.save_workspace(workspace_5.clone()).await;
1627 db.save_workspace(workspace_6.clone()).await;
1628
1629 let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
1630 assert_eq!(locations.len(), 2);
1631 assert_eq!(locations[0].0, LocalPaths::new(["/tmp1"]));
1632 assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
1633 assert_eq!(locations[0].2, Some(10));
1634 assert_eq!(locations[1].0, LocalPaths::new(["/tmp2"]));
1635 assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
1636 assert_eq!(locations[1].2, Some(20));
1637
1638 let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
1639 assert_eq!(locations.len(), 2);
1640 assert_eq!(locations[0].0, LocalPaths::new(["/tmp3"]));
1641 assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
1642 assert_eq!(locations[0].2, Some(30));
1643 let empty_paths: Vec<&str> = Vec::new();
1644 assert_eq!(locations[1].0, LocalPaths::new(empty_paths.iter()));
1645 assert_eq!(locations[1].1, LocalPathsOrder::new([]));
1646 assert_eq!(locations[1].2, Some(50));
1647 assert_eq!(locations[1].3, Some(ssh_project.id.0));
1648
1649 let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
1650 assert_eq!(locations.len(), 1);
1651 assert_eq!(
1652 locations[0].0,
1653 LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
1654 );
1655 assert_eq!(locations[0].1, LocalPathsOrder::new([2, 1, 0]));
1656 assert_eq!(locations[0].2, Some(60));
1657 }
1658
1659 fn default_workspace<P: AsRef<Path>>(
1660 workspace_id: &[P],
1661 center_group: &SerializedPaneGroup,
1662 ) -> SerializedWorkspace {
1663 SerializedWorkspace {
1664 id: WorkspaceId(4),
1665 location: SerializedWorkspaceLocation::from_local_paths(workspace_id),
1666 center_group: center_group.clone(),
1667 window_bounds: Default::default(),
1668 display: Default::default(),
1669 docks: Default::default(),
1670 centered_layout: false,
1671 session_id: None,
1672 window_id: None,
1673 }
1674 }
1675
1676 #[gpui::test]
1677 async fn test_last_session_workspace_locations() {
1678 let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
1679 let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
1680 let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
1681 let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
1682
1683 let db =
1684 WorkspaceDb(open_test_db("test_serializing_workspaces_last_session_workspaces").await);
1685
1686 let workspaces = [
1687 (1, vec![dir1.path()], vec![0], 9),
1688 (2, vec![dir2.path()], vec![0], 5),
1689 (3, vec![dir3.path()], vec![0], 8),
1690 (4, vec![dir4.path()], vec![0], 2),
1691 (
1692 5,
1693 vec![dir1.path(), dir2.path(), dir3.path()],
1694 vec![0, 1, 2],
1695 3,
1696 ),
1697 (
1698 6,
1699 vec![dir2.path(), dir3.path(), dir4.path()],
1700 vec![2, 1, 0],
1701 4,
1702 ),
1703 ]
1704 .into_iter()
1705 .map(|(id, locations, order, window_id)| SerializedWorkspace {
1706 id: WorkspaceId(id),
1707 location: SerializedWorkspaceLocation::Local(
1708 LocalPaths::new(locations),
1709 LocalPathsOrder::new(order),
1710 ),
1711 center_group: Default::default(),
1712 window_bounds: Default::default(),
1713 display: Default::default(),
1714 docks: Default::default(),
1715 centered_layout: false,
1716 session_id: Some("one-session".to_owned()),
1717 window_id: Some(window_id),
1718 })
1719 .collect::<Vec<_>>();
1720
1721 for workspace in workspaces.iter() {
1722 db.save_workspace(workspace.clone()).await;
1723 }
1724
1725 let stack = Some(Vec::from([
1726 WindowId::from(2), // Top
1727 WindowId::from(8),
1728 WindowId::from(5),
1729 WindowId::from(9),
1730 WindowId::from(3),
1731 WindowId::from(4), // Bottom
1732 ]));
1733
1734 let have = db
1735 .last_session_workspace_locations("one-session", stack)
1736 .unwrap();
1737 assert_eq!(have.len(), 6);
1738 assert_eq!(
1739 have[0],
1740 SerializedWorkspaceLocation::from_local_paths(&[dir4.path()])
1741 );
1742 assert_eq!(
1743 have[1],
1744 SerializedWorkspaceLocation::from_local_paths([dir3.path()])
1745 );
1746 assert_eq!(
1747 have[2],
1748 SerializedWorkspaceLocation::from_local_paths([dir2.path()])
1749 );
1750 assert_eq!(
1751 have[3],
1752 SerializedWorkspaceLocation::from_local_paths([dir1.path()])
1753 );
1754 assert_eq!(
1755 have[4],
1756 SerializedWorkspaceLocation::Local(
1757 LocalPaths::new([dir1.path(), dir2.path(), dir3.path()]),
1758 LocalPathsOrder::new([0, 1, 2]),
1759 ),
1760 );
1761 assert_eq!(
1762 have[5],
1763 SerializedWorkspaceLocation::Local(
1764 LocalPaths::new([dir2.path(), dir3.path(), dir4.path()]),
1765 LocalPathsOrder::new([2, 1, 0]),
1766 ),
1767 );
1768 }
1769
1770 #[gpui::test]
1771 async fn test_last_session_workspace_locations_ssh_projects() {
1772 let db = WorkspaceDb(
1773 open_test_db("test_serializing_workspaces_last_session_workspaces_ssh_projects").await,
1774 );
1775
1776 let ssh_projects = [
1777 ("host-1", "my-user-1"),
1778 ("host-2", "my-user-2"),
1779 ("host-3", "my-user-3"),
1780 ("host-4", "my-user-4"),
1781 ]
1782 .into_iter()
1783 .map(|(host, user)| async {
1784 db.get_or_create_ssh_project(host.to_string(), None, vec![], Some(user.to_string()))
1785 .await
1786 .unwrap()
1787 })
1788 .collect::<Vec<_>>();
1789
1790 let ssh_projects = futures::future::join_all(ssh_projects).await;
1791
1792 let workspaces = [
1793 (1, ssh_projects[0].clone(), 9),
1794 (2, ssh_projects[1].clone(), 5),
1795 (3, ssh_projects[2].clone(), 8),
1796 (4, ssh_projects[3].clone(), 2),
1797 ]
1798 .into_iter()
1799 .map(|(id, ssh_project, window_id)| SerializedWorkspace {
1800 id: WorkspaceId(id),
1801 location: SerializedWorkspaceLocation::Ssh(ssh_project),
1802 center_group: Default::default(),
1803 window_bounds: Default::default(),
1804 display: Default::default(),
1805 docks: Default::default(),
1806 centered_layout: false,
1807 session_id: Some("one-session".to_owned()),
1808 window_id: Some(window_id),
1809 })
1810 .collect::<Vec<_>>();
1811
1812 for workspace in workspaces.iter() {
1813 db.save_workspace(workspace.clone()).await;
1814 }
1815
1816 let stack = Some(Vec::from([
1817 WindowId::from(2), // Top
1818 WindowId::from(8),
1819 WindowId::from(5),
1820 WindowId::from(9), // Bottom
1821 ]));
1822
1823 let have = db
1824 .last_session_workspace_locations("one-session", stack)
1825 .unwrap();
1826 assert_eq!(have.len(), 4);
1827 assert_eq!(
1828 have[0],
1829 SerializedWorkspaceLocation::Ssh(ssh_projects[3].clone())
1830 );
1831 assert_eq!(
1832 have[1],
1833 SerializedWorkspaceLocation::Ssh(ssh_projects[2].clone())
1834 );
1835 assert_eq!(
1836 have[2],
1837 SerializedWorkspaceLocation::Ssh(ssh_projects[1].clone())
1838 );
1839 assert_eq!(
1840 have[3],
1841 SerializedWorkspaceLocation::Ssh(ssh_projects[0].clone())
1842 );
1843 }
1844
1845 #[gpui::test]
1846 async fn test_get_or_create_ssh_project() {
1847 let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project").await);
1848
1849 let (host, port, paths, user) = (
1850 "example.com".to_string(),
1851 Some(22_u16),
1852 vec!["/home/user".to_string(), "/etc/nginx".to_string()],
1853 Some("user".to_string()),
1854 );
1855
1856 let project = db
1857 .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
1858 .await
1859 .unwrap();
1860
1861 assert_eq!(project.host, host);
1862 assert_eq!(project.paths, paths);
1863 assert_eq!(project.user, user);
1864
1865 // Test that calling the function again with the same parameters returns the same project
1866 let same_project = db
1867 .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
1868 .await
1869 .unwrap();
1870
1871 assert_eq!(project.id, same_project.id);
1872
1873 // Test with different parameters
1874 let (host2, paths2, user2) = (
1875 "otherexample.com".to_string(),
1876 vec!["/home/otheruser".to_string()],
1877 Some("otheruser".to_string()),
1878 );
1879
1880 let different_project = db
1881 .get_or_create_ssh_project(host2.clone(), None, paths2.clone(), user2.clone())
1882 .await
1883 .unwrap();
1884
1885 assert_ne!(project.id, different_project.id);
1886 assert_eq!(different_project.host, host2);
1887 assert_eq!(different_project.paths, paths2);
1888 assert_eq!(different_project.user, user2);
1889 }
1890
1891 #[gpui::test]
1892 async fn test_get_or_create_ssh_project_with_null_user() {
1893 let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project_with_null_user").await);
1894
1895 let (host, port, paths, user) = (
1896 "example.com".to_string(),
1897 None,
1898 vec!["/home/user".to_string()],
1899 None,
1900 );
1901
1902 let project = db
1903 .get_or_create_ssh_project(host.clone(), port, paths.clone(), None)
1904 .await
1905 .unwrap();
1906
1907 assert_eq!(project.host, host);
1908 assert_eq!(project.paths, paths);
1909 assert_eq!(project.user, None);
1910
1911 // Test that calling the function again with the same parameters returns the same project
1912 let same_project = db
1913 .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
1914 .await
1915 .unwrap();
1916
1917 assert_eq!(project.id, same_project.id);
1918 }
1919
1920 #[gpui::test]
1921 async fn test_get_ssh_projects() {
1922 let db = WorkspaceDb(open_test_db("test_get_ssh_projects").await);
1923
1924 let projects = vec![
1925 (
1926 "example.com".to_string(),
1927 None,
1928 vec!["/home/user".to_string()],
1929 None,
1930 ),
1931 (
1932 "anotherexample.com".to_string(),
1933 Some(123_u16),
1934 vec!["/home/user2".to_string()],
1935 Some("user2".to_string()),
1936 ),
1937 (
1938 "yetanother.com".to_string(),
1939 Some(345_u16),
1940 vec!["/home/user3".to_string(), "/proc/1234/exe".to_string()],
1941 None,
1942 ),
1943 ];
1944
1945 for (host, port, paths, user) in projects.iter() {
1946 let project = db
1947 .get_or_create_ssh_project(host.clone(), *port, paths.clone(), user.clone())
1948 .await
1949 .unwrap();
1950
1951 assert_eq!(&project.host, host);
1952 assert_eq!(&project.port, port);
1953 assert_eq!(&project.paths, paths);
1954 assert_eq!(&project.user, user);
1955 }
1956
1957 let stored_projects = db.ssh_projects().unwrap();
1958 assert_eq!(stored_projects.len(), projects.len());
1959 }
1960
1961 #[gpui::test]
1962 async fn test_simple_split() {
1963 env_logger::try_init().ok();
1964
1965 let db = WorkspaceDb(open_test_db("simple_split").await);
1966
1967 // -----------------
1968 // | 1,2 | 5,6 |
1969 // | - - - | |
1970 // | 3,4 | |
1971 // -----------------
1972 let center_pane = group(
1973 Axis::Horizontal,
1974 vec![
1975 group(
1976 Axis::Vertical,
1977 vec![
1978 SerializedPaneGroup::Pane(SerializedPane::new(
1979 vec![
1980 SerializedItem::new("Terminal", 1, false, false),
1981 SerializedItem::new("Terminal", 2, true, false),
1982 ],
1983 false,
1984 0,
1985 )),
1986 SerializedPaneGroup::Pane(SerializedPane::new(
1987 vec![
1988 SerializedItem::new("Terminal", 4, false, false),
1989 SerializedItem::new("Terminal", 3, true, false),
1990 ],
1991 true,
1992 0,
1993 )),
1994 ],
1995 ),
1996 SerializedPaneGroup::Pane(SerializedPane::new(
1997 vec![
1998 SerializedItem::new("Terminal", 5, true, false),
1999 SerializedItem::new("Terminal", 6, false, false),
2000 ],
2001 false,
2002 0,
2003 )),
2004 ],
2005 );
2006
2007 let workspace = default_workspace(&["/tmp"], ¢er_pane);
2008
2009 db.save_workspace(workspace.clone()).await;
2010
2011 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
2012
2013 assert_eq!(workspace.center_group, new_workspace.center_group);
2014 }
2015
2016 #[gpui::test]
2017 async fn test_cleanup_panes() {
2018 env_logger::try_init().ok();
2019
2020 let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
2021
2022 let center_pane = group(
2023 Axis::Horizontal,
2024 vec![
2025 group(
2026 Axis::Vertical,
2027 vec![
2028 SerializedPaneGroup::Pane(SerializedPane::new(
2029 vec![
2030 SerializedItem::new("Terminal", 1, false, false),
2031 SerializedItem::new("Terminal", 2, true, false),
2032 ],
2033 false,
2034 0,
2035 )),
2036 SerializedPaneGroup::Pane(SerializedPane::new(
2037 vec![
2038 SerializedItem::new("Terminal", 4, false, false),
2039 SerializedItem::new("Terminal", 3, true, false),
2040 ],
2041 true,
2042 0,
2043 )),
2044 ],
2045 ),
2046 SerializedPaneGroup::Pane(SerializedPane::new(
2047 vec![
2048 SerializedItem::new("Terminal", 5, false, false),
2049 SerializedItem::new("Terminal", 6, true, false),
2050 ],
2051 false,
2052 0,
2053 )),
2054 ],
2055 );
2056
2057 let id = &["/tmp"];
2058
2059 let mut workspace = default_workspace(id, ¢er_pane);
2060
2061 db.save_workspace(workspace.clone()).await;
2062
2063 workspace.center_group = group(
2064 Axis::Vertical,
2065 vec![
2066 SerializedPaneGroup::Pane(SerializedPane::new(
2067 vec![
2068 SerializedItem::new("Terminal", 1, false, false),
2069 SerializedItem::new("Terminal", 2, true, false),
2070 ],
2071 false,
2072 0,
2073 )),
2074 SerializedPaneGroup::Pane(SerializedPane::new(
2075 vec![
2076 SerializedItem::new("Terminal", 4, true, false),
2077 SerializedItem::new("Terminal", 3, false, false),
2078 ],
2079 true,
2080 0,
2081 )),
2082 ],
2083 );
2084
2085 db.save_workspace(workspace.clone()).await;
2086
2087 let new_workspace = db.workspace_for_roots(id).unwrap();
2088
2089 assert_eq!(workspace.center_group, new_workspace.center_group);
2090 }
2091}