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