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