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