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};
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, 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(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 sql!(
303 DROP TABLE remote_projects;
304 CREATE TABLE dev_server_projects (
305 id INTEGER NOT NULL UNIQUE,
306 path TEXT,
307 dev_server_name TEXT
308 );
309 ALTER TABLE workspaces DROP COLUMN remote_project_id;
310 ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
311 ),
312 ];
313}
314
315impl WorkspaceDb {
316 /// Returns a serialized workspace for the given worktree_roots. If the passed array
317 /// is empty, the most recent workspace is returned instead. If no workspace for the
318 /// passed roots is stored, returns none.
319 pub(crate) fn workspace_for_roots<P: AsRef<Path>>(
320 &self,
321 worktree_roots: &[P],
322 ) -> Option<SerializedWorkspace> {
323 let local_paths = LocalPaths::new(worktree_roots);
324
325 // Note that we re-assign the workspace_id here in case it's empty
326 // and we've grabbed the most recent workspace
327 let (
328 workspace_id,
329 local_paths,
330 dev_server_project_id,
331 bounds,
332 display,
333 fullscreen,
334 centered_layout,
335 docks,
336 ): (
337 WorkspaceId,
338 Option<LocalPaths>,
339 Option<u64>,
340 Option<SerializedWindowsBounds>,
341 Option<Uuid>,
342 Option<bool>,
343 Option<bool>,
344 DockStructure,
345 ) = self
346 .select_row_bound(sql! {
347 SELECT
348 workspace_id,
349 local_paths,
350 dev_server_project_id,
351 window_state,
352 window_x,
353 window_y,
354 window_width,
355 window_height,
356 display,
357 fullscreen,
358 centered_layout,
359 left_dock_visible,
360 left_dock_active_panel,
361 left_dock_zoom,
362 right_dock_visible,
363 right_dock_active_panel,
364 right_dock_zoom,
365 bottom_dock_visible,
366 bottom_dock_active_panel,
367 bottom_dock_zoom
368 FROM workspaces
369 WHERE local_paths = ?
370 })
371 .and_then(|mut prepared_statement| (prepared_statement)(&local_paths))
372 .context("No workspaces found")
373 .warn_on_err()
374 .flatten()?;
375
376 let location = if let Some(dev_server_project_id) = dev_server_project_id {
377 let dev_server_project: SerializedDevServerProject = self
378 .select_row_bound(sql! {
379 SELECT id, path, dev_server_name
380 FROM dev_server_projects
381 WHERE id = ?
382 })
383 .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id))
384 .context("No remote project found")
385 .warn_on_err()
386 .flatten()?;
387 SerializedWorkspaceLocation::DevServer(dev_server_project)
388 } else if let Some(local_paths) = local_paths {
389 SerializedWorkspaceLocation::Local(local_paths)
390 } else {
391 return None;
392 };
393
394 Some(SerializedWorkspace {
395 id: workspace_id,
396 location,
397 center_group: self
398 .get_center_pane_group(workspace_id)
399 .context("Getting center group")
400 .log_err()?,
401 bounds: bounds.map(|bounds| bounds.0),
402 fullscreen: fullscreen.unwrap_or(false),
403 centered_layout: centered_layout.unwrap_or(false),
404 display,
405 docks,
406 })
407 }
408
409 /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
410 /// that used this workspace previously
411 pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
412 self.write(move |conn| {
413 conn.with_savepoint("update_worktrees", || {
414 // Clear out panes and pane_groups
415 conn.exec_bound(sql!(
416 DELETE FROM pane_groups WHERE workspace_id = ?1;
417 DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
418 .context("Clearing old panes")?;
419
420 match workspace.location {
421 SerializedWorkspaceLocation::Local(local_paths) => {
422 conn.exec_bound(sql!(
423 DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ?
424 ))?((&local_paths, workspace.id))
425 .context("clearing out old locations")?;
426
427 // Upsert
428 conn.exec_bound(sql!(
429 INSERT INTO workspaces(
430 workspace_id,
431 local_paths,
432 left_dock_visible,
433 left_dock_active_panel,
434 left_dock_zoom,
435 right_dock_visible,
436 right_dock_active_panel,
437 right_dock_zoom,
438 bottom_dock_visible,
439 bottom_dock_active_panel,
440 bottom_dock_zoom,
441 timestamp
442 )
443 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
444 ON CONFLICT DO
445 UPDATE SET
446 local_paths = ?2,
447 left_dock_visible = ?3,
448 left_dock_active_panel = ?4,
449 left_dock_zoom = ?5,
450 right_dock_visible = ?6,
451 right_dock_active_panel = ?7,
452 right_dock_zoom = ?8,
453 bottom_dock_visible = ?9,
454 bottom_dock_active_panel = ?10,
455 bottom_dock_zoom = ?11,
456 timestamp = CURRENT_TIMESTAMP
457 ))?((workspace.id, &local_paths, workspace.docks))
458 .context("Updating workspace")?;
459 }
460 SerializedWorkspaceLocation::DevServer(dev_server_project) => {
461 conn.exec_bound(sql!(
462 DELETE FROM workspaces WHERE dev_server_project_id = ? AND workspace_id != ?
463 ))?((dev_server_project.id.0, workspace.id))
464 .context("clearing out old locations")?;
465
466 conn.exec_bound(sql!(
467 INSERT INTO dev_server_projects(
468 id,
469 path,
470 dev_server_name
471 ) VALUES (?1, ?2, ?3)
472 ON CONFLICT DO
473 UPDATE SET
474 path = ?2,
475 dev_server_name = ?3
476 ))?(&dev_server_project)?;
477
478 // Upsert
479 conn.exec_bound(sql!(
480 INSERT INTO workspaces(
481 workspace_id,
482 dev_server_project_id,
483 left_dock_visible,
484 left_dock_active_panel,
485 left_dock_zoom,
486 right_dock_visible,
487 right_dock_active_panel,
488 right_dock_zoom,
489 bottom_dock_visible,
490 bottom_dock_active_panel,
491 bottom_dock_zoom,
492 timestamp
493 )
494 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
495 ON CONFLICT DO
496 UPDATE SET
497 dev_server_project_id = ?2,
498 left_dock_visible = ?3,
499 left_dock_active_panel = ?4,
500 left_dock_zoom = ?5,
501 right_dock_visible = ?6,
502 right_dock_active_panel = ?7,
503 right_dock_zoom = ?8,
504 bottom_dock_visible = ?9,
505 bottom_dock_active_panel = ?10,
506 bottom_dock_zoom = ?11,
507 timestamp = CURRENT_TIMESTAMP
508 ))?((
509 workspace.id,
510 dev_server_project.id.0,
511 workspace.docks,
512 ))
513 .context("Updating workspace")?;
514 }
515 }
516
517 // Save center pane group
518 Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
519 .context("save pane group in save workspace")?;
520
521 Ok(())
522 })
523 .log_err();
524 })
525 .await;
526 }
527
528 query! {
529 pub async fn next_id() -> Result<WorkspaceId> {
530 INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
531 }
532 }
533
534 query! {
535 fn recent_workspaces() -> Result<Vec<(WorkspaceId, LocalPaths, Option<u64>)>> {
536 SELECT workspace_id, local_paths, dev_server_project_id
537 FROM workspaces
538 WHERE local_paths IS NOT NULL OR dev_server_project_id IS NOT NULL
539 ORDER BY timestamp DESC
540 }
541 }
542
543 query! {
544 fn dev_server_projects() -> Result<Vec<SerializedDevServerProject>> {
545 SELECT id, path, dev_server_name
546 FROM dev_server_projects
547 }
548 }
549
550 pub(crate) fn last_window(
551 &self,
552 ) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowsBounds>, Option<bool>)> {
553 let mut prepared_query =
554 self.select::<(Option<Uuid>, Option<SerializedWindowsBounds>, Option<bool>)>(sql!(
555 SELECT
556 display,
557 window_state, window_x, window_y, window_width, window_height,
558 fullscreen
559 FROM workspaces
560 WHERE local_paths
561 IS NOT NULL
562 ORDER BY timestamp DESC
563 LIMIT 1
564 ))?;
565 let result = prepared_query()?;
566 Ok(result
567 .into_iter()
568 .next()
569 .unwrap_or_else(|| (None, None, None)))
570 }
571
572 query! {
573 pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
574 DELETE FROM workspaces
575 WHERE workspace_id IS ?
576 }
577 }
578
579 pub async fn delete_workspace_by_dev_server_project_id(
580 &self,
581 id: DevServerProjectId,
582 ) -> Result<()> {
583 self.write(move |conn| {
584 conn.exec_bound(sql!(
585 DELETE FROM dev_server_projects WHERE id = ?
586 ))?(id.0)?;
587 conn.exec_bound(sql!(
588 DELETE FROM workspaces
589 WHERE dev_server_project_id IS ?
590 ))?(id.0)
591 })
592 .await
593 }
594
595 // Returns the recent locations which are still valid on disk and deletes ones which no longer
596 // exist.
597 pub async fn recent_workspaces_on_disk(
598 &self,
599 ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation)>> {
600 let mut result = Vec::new();
601 let mut delete_tasks = Vec::new();
602 let dev_server_projects = self.dev_server_projects()?;
603
604 for (id, location, dev_server_project_id) in self.recent_workspaces()? {
605 if let Some(dev_server_project_id) = dev_server_project_id.map(DevServerProjectId) {
606 if let Some(dev_server_project) = dev_server_projects
607 .iter()
608 .find(|rp| rp.id == dev_server_project_id)
609 {
610 result.push((id, dev_server_project.clone().into()));
611 } else {
612 delete_tasks.push(self.delete_workspace_by_id(id));
613 }
614 continue;
615 }
616
617 if location.paths().iter().all(|path| path.exists())
618 && location.paths().iter().any(|path| path.is_dir())
619 {
620 result.push((id, location.into()));
621 } else {
622 delete_tasks.push(self.delete_workspace_by_id(id));
623 }
624 }
625
626 futures::future::join_all(delete_tasks).await;
627 Ok(result)
628 }
629
630 pub async fn last_workspace(&self) -> Result<Option<LocalPaths>> {
631 Ok(self
632 .recent_workspaces_on_disk()
633 .await?
634 .into_iter()
635 .filter_map(|(_, location)| match location {
636 SerializedWorkspaceLocation::Local(local_paths) => Some(local_paths),
637 SerializedWorkspaceLocation::DevServer(_) => None,
638 })
639 .next())
640 }
641
642 fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
643 Ok(self
644 .get_pane_group(workspace_id, None)?
645 .into_iter()
646 .next()
647 .unwrap_or_else(|| {
648 SerializedPaneGroup::Pane(SerializedPane {
649 active: true,
650 children: vec![],
651 })
652 }))
653 }
654
655 fn get_pane_group(
656 &self,
657 workspace_id: WorkspaceId,
658 group_id: Option<GroupId>,
659 ) -> Result<Vec<SerializedPaneGroup>> {
660 type GroupKey = (Option<GroupId>, WorkspaceId);
661 type GroupOrPane = (
662 Option<GroupId>,
663 Option<SerializedAxis>,
664 Option<PaneId>,
665 Option<bool>,
666 Option<String>,
667 );
668 self.select_bound::<GroupKey, GroupOrPane>(sql!(
669 SELECT group_id, axis, pane_id, active, flexes
670 FROM (SELECT
671 group_id,
672 axis,
673 NULL as pane_id,
674 NULL as active,
675 position,
676 parent_group_id,
677 workspace_id,
678 flexes
679 FROM pane_groups
680 UNION
681 SELECT
682 NULL,
683 NULL,
684 center_panes.pane_id,
685 panes.active as active,
686 position,
687 parent_group_id,
688 panes.workspace_id as workspace_id,
689 NULL
690 FROM center_panes
691 JOIN panes ON center_panes.pane_id = panes.pane_id)
692 WHERE parent_group_id IS ? AND workspace_id = ?
693 ORDER BY position
694 ))?((group_id, workspace_id))?
695 .into_iter()
696 .map(|(group_id, axis, pane_id, active, flexes)| {
697 if let Some((group_id, axis)) = group_id.zip(axis) {
698 let flexes = flexes
699 .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
700 .transpose()?;
701
702 Ok(SerializedPaneGroup::Group {
703 axis,
704 children: self.get_pane_group(workspace_id, Some(group_id))?,
705 flexes,
706 })
707 } else if let Some((pane_id, active)) = pane_id.zip(active) {
708 Ok(SerializedPaneGroup::Pane(SerializedPane::new(
709 self.get_items(pane_id)?,
710 active,
711 )))
712 } else {
713 bail!("Pane Group Child was neither a pane group or a pane");
714 }
715 })
716 // Filter out panes and pane groups which don't have any children or items
717 .filter(|pane_group| match pane_group {
718 Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
719 Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
720 _ => true,
721 })
722 .collect::<Result<_>>()
723 }
724
725 fn save_pane_group(
726 conn: &Connection,
727 workspace_id: WorkspaceId,
728 pane_group: &SerializedPaneGroup,
729 parent: Option<(GroupId, usize)>,
730 ) -> Result<()> {
731 match pane_group {
732 SerializedPaneGroup::Group {
733 axis,
734 children,
735 flexes,
736 } => {
737 let (parent_id, position) = unzip_option(parent);
738
739 let flex_string = flexes
740 .as_ref()
741 .map(|flexes| serde_json::json!(flexes).to_string());
742
743 let group_id = conn.select_row_bound::<_, i64>(sql!(
744 INSERT INTO pane_groups(
745 workspace_id,
746 parent_group_id,
747 position,
748 axis,
749 flexes
750 )
751 VALUES (?, ?, ?, ?, ?)
752 RETURNING group_id
753 ))?((
754 workspace_id,
755 parent_id,
756 position,
757 *axis,
758 flex_string,
759 ))?
760 .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
761
762 for (position, group) in children.iter().enumerate() {
763 Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
764 }
765
766 Ok(())
767 }
768 SerializedPaneGroup::Pane(pane) => {
769 Self::save_pane(conn, workspace_id, pane, parent)?;
770 Ok(())
771 }
772 }
773 }
774
775 fn save_pane(
776 conn: &Connection,
777 workspace_id: WorkspaceId,
778 pane: &SerializedPane,
779 parent: Option<(GroupId, usize)>,
780 ) -> Result<PaneId> {
781 let pane_id = conn.select_row_bound::<_, i64>(sql!(
782 INSERT INTO panes(workspace_id, active)
783 VALUES (?, ?)
784 RETURNING pane_id
785 ))?((workspace_id, pane.active))?
786 .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
787
788 let (parent_id, order) = unzip_option(parent);
789 conn.exec_bound(sql!(
790 INSERT INTO center_panes(pane_id, parent_group_id, position)
791 VALUES (?, ?, ?)
792 ))?((pane_id, parent_id, order))?;
793
794 Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
795
796 Ok(pane_id)
797 }
798
799 fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
800 self.select_bound(sql!(
801 SELECT kind, item_id, active, preview FROM items
802 WHERE pane_id = ?
803 ORDER BY position
804 ))?(pane_id)
805 }
806
807 fn save_items(
808 conn: &Connection,
809 workspace_id: WorkspaceId,
810 pane_id: PaneId,
811 items: &[SerializedItem],
812 ) -> Result<()> {
813 let mut insert = conn.exec_bound(sql!(
814 INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
815 )).context("Preparing insertion")?;
816 for (position, item) in items.iter().enumerate() {
817 insert((workspace_id, pane_id, position, item))?;
818 }
819
820 Ok(())
821 }
822
823 query! {
824 pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
825 UPDATE workspaces
826 SET timestamp = CURRENT_TIMESTAMP
827 WHERE workspace_id = ?
828 }
829 }
830
831 query! {
832 pub(crate) async fn set_window_bounds(workspace_id: WorkspaceId, bounds: SerializedWindowsBounds, display: Uuid) -> Result<()> {
833 UPDATE workspaces
834 SET window_state = ?2,
835 window_x = ?3,
836 window_y = ?4,
837 window_width = ?5,
838 window_height = ?6,
839 display = ?7
840 WHERE workspace_id = ?1
841 }
842 }
843
844 query! {
845 pub(crate) async fn set_fullscreen(workspace_id: WorkspaceId, fullscreen: bool) -> Result<()> {
846 UPDATE workspaces
847 SET fullscreen = ?2
848 WHERE workspace_id = ?1
849 }
850 }
851
852 query! {
853 pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
854 UPDATE workspaces
855 SET centered_layout = ?2
856 WHERE workspace_id = ?1
857 }
858 }
859}
860
861#[cfg(test)]
862mod tests {
863 use super::*;
864 use db::open_test_db;
865 use gpui;
866
867 #[gpui::test]
868 async fn test_next_id_stability() {
869 env_logger::try_init().ok();
870
871 let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
872
873 db.write(|conn| {
874 conn.migrate(
875 "test_table",
876 &[sql!(
877 CREATE TABLE test_table(
878 text TEXT,
879 workspace_id INTEGER,
880 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
881 ON DELETE CASCADE
882 ) STRICT;
883 )],
884 )
885 .unwrap();
886 })
887 .await;
888
889 let id = db.next_id().await.unwrap();
890 // Assert the empty row got inserted
891 assert_eq!(
892 Some(id),
893 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
894 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
895 ))
896 .unwrap()(id)
897 .unwrap()
898 );
899
900 db.write(move |conn| {
901 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
902 .unwrap()(("test-text-1", id))
903 .unwrap()
904 })
905 .await;
906
907 let test_text_1 = db
908 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
909 .unwrap()(1)
910 .unwrap()
911 .unwrap();
912 assert_eq!(test_text_1, "test-text-1");
913 }
914
915 #[gpui::test]
916 async fn test_workspace_id_stability() {
917 env_logger::try_init().ok();
918
919 let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
920
921 db.write(|conn| {
922 conn.migrate(
923 "test_table",
924 &[sql!(
925 CREATE TABLE test_table(
926 text TEXT,
927 workspace_id INTEGER,
928 FOREIGN KEY(workspace_id)
929 REFERENCES workspaces(workspace_id)
930 ON DELETE CASCADE
931 ) STRICT;)],
932 )
933 })
934 .await
935 .unwrap();
936
937 let mut workspace_1 = SerializedWorkspace {
938 id: WorkspaceId(1),
939 location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
940 center_group: Default::default(),
941 bounds: Default::default(),
942 display: Default::default(),
943 docks: Default::default(),
944 fullscreen: false,
945 centered_layout: false,
946 };
947
948 let workspace_2 = SerializedWorkspace {
949 id: WorkspaceId(2),
950 location: LocalPaths::new(["/tmp"]).into(),
951 center_group: Default::default(),
952 bounds: Default::default(),
953 display: Default::default(),
954 docks: Default::default(),
955 fullscreen: false,
956 centered_layout: false,
957 };
958
959 db.save_workspace(workspace_1.clone()).await;
960
961 db.write(|conn| {
962 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
963 .unwrap()(("test-text-1", 1))
964 .unwrap();
965 })
966 .await;
967
968 db.save_workspace(workspace_2.clone()).await;
969
970 db.write(|conn| {
971 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
972 .unwrap()(("test-text-2", 2))
973 .unwrap();
974 })
975 .await;
976
977 workspace_1.location = LocalPaths::new(["/tmp", "/tmp3"]).into();
978 db.save_workspace(workspace_1.clone()).await;
979 db.save_workspace(workspace_1).await;
980 db.save_workspace(workspace_2).await;
981
982 let test_text_2 = db
983 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
984 .unwrap()(2)
985 .unwrap()
986 .unwrap();
987 assert_eq!(test_text_2, "test-text-2");
988
989 let test_text_1 = db
990 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
991 .unwrap()(1)
992 .unwrap()
993 .unwrap();
994 assert_eq!(test_text_1, "test-text-1");
995 }
996
997 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
998 SerializedPaneGroup::Group {
999 axis: SerializedAxis(axis),
1000 flexes: None,
1001 children,
1002 }
1003 }
1004
1005 #[gpui::test]
1006 async fn test_full_workspace_serialization() {
1007 env_logger::try_init().ok();
1008
1009 let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
1010
1011 // -----------------
1012 // | 1,2 | 5,6 |
1013 // | - - - | |
1014 // | 3,4 | |
1015 // -----------------
1016 let center_group = group(
1017 Axis::Horizontal,
1018 vec![
1019 group(
1020 Axis::Vertical,
1021 vec![
1022 SerializedPaneGroup::Pane(SerializedPane::new(
1023 vec![
1024 SerializedItem::new("Terminal", 5, false, false),
1025 SerializedItem::new("Terminal", 6, true, false),
1026 ],
1027 false,
1028 )),
1029 SerializedPaneGroup::Pane(SerializedPane::new(
1030 vec![
1031 SerializedItem::new("Terminal", 7, true, false),
1032 SerializedItem::new("Terminal", 8, false, false),
1033 ],
1034 false,
1035 )),
1036 ],
1037 ),
1038 SerializedPaneGroup::Pane(SerializedPane::new(
1039 vec![
1040 SerializedItem::new("Terminal", 9, false, false),
1041 SerializedItem::new("Terminal", 10, true, false),
1042 ],
1043 false,
1044 )),
1045 ],
1046 );
1047
1048 let workspace = SerializedWorkspace {
1049 id: WorkspaceId(5),
1050 location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
1051 center_group,
1052 bounds: Default::default(),
1053 display: Default::default(),
1054 docks: Default::default(),
1055 fullscreen: false,
1056 centered_layout: false,
1057 };
1058
1059 db.save_workspace(workspace.clone()).await;
1060 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
1061
1062 assert_eq!(workspace, round_trip_workspace.unwrap());
1063
1064 // Test guaranteed duplicate IDs
1065 db.save_workspace(workspace.clone()).await;
1066 db.save_workspace(workspace.clone()).await;
1067
1068 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
1069 assert_eq!(workspace, round_trip_workspace.unwrap());
1070 }
1071
1072 #[gpui::test]
1073 async fn test_workspace_assignment() {
1074 env_logger::try_init().ok();
1075
1076 let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
1077
1078 let workspace_1 = SerializedWorkspace {
1079 id: WorkspaceId(1),
1080 location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
1081 center_group: Default::default(),
1082 bounds: Default::default(),
1083 display: Default::default(),
1084 docks: Default::default(),
1085 fullscreen: false,
1086 centered_layout: false,
1087 };
1088
1089 let mut workspace_2 = SerializedWorkspace {
1090 id: WorkspaceId(2),
1091 location: LocalPaths::new(["/tmp"]).into(),
1092 center_group: Default::default(),
1093 bounds: Default::default(),
1094 display: Default::default(),
1095 docks: Default::default(),
1096 fullscreen: false,
1097 centered_layout: false,
1098 };
1099
1100 db.save_workspace(workspace_1.clone()).await;
1101 db.save_workspace(workspace_2.clone()).await;
1102
1103 // Test that paths are treated as a set
1104 assert_eq!(
1105 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1106 workspace_1
1107 );
1108 assert_eq!(
1109 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
1110 workspace_1
1111 );
1112
1113 // Make sure that other keys work
1114 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
1115 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
1116
1117 // Test 'mutate' case of updating a pre-existing id
1118 workspace_2.location = LocalPaths::new(["/tmp", "/tmp2"]).into();
1119
1120 db.save_workspace(workspace_2.clone()).await;
1121 assert_eq!(
1122 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1123 workspace_2
1124 );
1125
1126 // Test other mechanism for mutating
1127 let mut workspace_3 = SerializedWorkspace {
1128 id: WorkspaceId(3),
1129 location: LocalPaths::new(&["/tmp", "/tmp2"]).into(),
1130 center_group: Default::default(),
1131 bounds: Default::default(),
1132 display: Default::default(),
1133 docks: Default::default(),
1134 fullscreen: false,
1135 centered_layout: false,
1136 };
1137
1138 db.save_workspace(workspace_3.clone()).await;
1139 assert_eq!(
1140 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1141 workspace_3
1142 );
1143
1144 // Make sure that updating paths differently also works
1145 workspace_3.location = LocalPaths::new(["/tmp3", "/tmp4", "/tmp2"]).into();
1146 db.save_workspace(workspace_3.clone()).await;
1147 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
1148 assert_eq!(
1149 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
1150 .unwrap(),
1151 workspace_3
1152 );
1153 }
1154
1155 use crate::persistence::model::SerializedWorkspace;
1156 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
1157
1158 fn default_workspace<P: AsRef<Path>>(
1159 workspace_id: &[P],
1160 center_group: &SerializedPaneGroup,
1161 ) -> SerializedWorkspace {
1162 SerializedWorkspace {
1163 id: WorkspaceId(4),
1164 location: LocalPaths::new(workspace_id).into(),
1165 center_group: center_group.clone(),
1166 bounds: Default::default(),
1167 display: Default::default(),
1168 docks: Default::default(),
1169 fullscreen: false,
1170 centered_layout: false,
1171 }
1172 }
1173
1174 #[gpui::test]
1175 async fn test_simple_split() {
1176 env_logger::try_init().ok();
1177
1178 let db = WorkspaceDb(open_test_db("simple_split").await);
1179
1180 // -----------------
1181 // | 1,2 | 5,6 |
1182 // | - - - | |
1183 // | 3,4 | |
1184 // -----------------
1185 let center_pane = group(
1186 Axis::Horizontal,
1187 vec![
1188 group(
1189 Axis::Vertical,
1190 vec![
1191 SerializedPaneGroup::Pane(SerializedPane::new(
1192 vec![
1193 SerializedItem::new("Terminal", 1, false, false),
1194 SerializedItem::new("Terminal", 2, true, false),
1195 ],
1196 false,
1197 )),
1198 SerializedPaneGroup::Pane(SerializedPane::new(
1199 vec![
1200 SerializedItem::new("Terminal", 4, false, false),
1201 SerializedItem::new("Terminal", 3, true, false),
1202 ],
1203 true,
1204 )),
1205 ],
1206 ),
1207 SerializedPaneGroup::Pane(SerializedPane::new(
1208 vec![
1209 SerializedItem::new("Terminal", 5, true, false),
1210 SerializedItem::new("Terminal", 6, false, false),
1211 ],
1212 false,
1213 )),
1214 ],
1215 );
1216
1217 let workspace = default_workspace(&["/tmp"], ¢er_pane);
1218
1219 db.save_workspace(workspace.clone()).await;
1220
1221 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
1222
1223 assert_eq!(workspace.center_group, new_workspace.center_group);
1224 }
1225
1226 #[gpui::test]
1227 async fn test_cleanup_panes() {
1228 env_logger::try_init().ok();
1229
1230 let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
1231
1232 let center_pane = group(
1233 Axis::Horizontal,
1234 vec![
1235 group(
1236 Axis::Vertical,
1237 vec![
1238 SerializedPaneGroup::Pane(SerializedPane::new(
1239 vec![
1240 SerializedItem::new("Terminal", 1, false, false),
1241 SerializedItem::new("Terminal", 2, true, false),
1242 ],
1243 false,
1244 )),
1245 SerializedPaneGroup::Pane(SerializedPane::new(
1246 vec![
1247 SerializedItem::new("Terminal", 4, false, false),
1248 SerializedItem::new("Terminal", 3, true, false),
1249 ],
1250 true,
1251 )),
1252 ],
1253 ),
1254 SerializedPaneGroup::Pane(SerializedPane::new(
1255 vec![
1256 SerializedItem::new("Terminal", 5, false, false),
1257 SerializedItem::new("Terminal", 6, true, false),
1258 ],
1259 false,
1260 )),
1261 ],
1262 );
1263
1264 let id = &["/tmp"];
1265
1266 let mut workspace = default_workspace(id, ¢er_pane);
1267
1268 db.save_workspace(workspace.clone()).await;
1269
1270 workspace.center_group = group(
1271 Axis::Vertical,
1272 vec![
1273 SerializedPaneGroup::Pane(SerializedPane::new(
1274 vec![
1275 SerializedItem::new("Terminal", 1, false, false),
1276 SerializedItem::new("Terminal", 2, true, false),
1277 ],
1278 false,
1279 )),
1280 SerializedPaneGroup::Pane(SerializedPane::new(
1281 vec![
1282 SerializedItem::new("Terminal", 4, true, false),
1283 SerializedItem::new("Terminal", 3, false, false),
1284 ],
1285 true,
1286 )),
1287 ],
1288 );
1289
1290 db.save_workspace(workspace.clone()).await;
1291
1292 let new_workspace = db.workspace_for_roots(id).unwrap();
1293
1294 assert_eq!(workspace.center_group, new_workspace.center_group);
1295 }
1296}