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