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