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