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 // Returns the recent locations which are still valid on disk and deletes ones which no longer
580 // exist.
581 pub async fn recent_workspaces_on_disk(
582 &self,
583 ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation)>> {
584 let mut result = Vec::new();
585 let mut delete_tasks = Vec::new();
586 let dev_server_projects = self.dev_server_projects()?;
587
588 for (id, location, dev_server_project_id) in self.recent_workspaces()? {
589 if let Some(dev_server_project_id) = dev_server_project_id.map(DevServerProjectId) {
590 if let Some(dev_server_project) = dev_server_projects
591 .iter()
592 .find(|rp| rp.id == dev_server_project_id)
593 {
594 result.push((id, dev_server_project.clone().into()));
595 } else {
596 delete_tasks.push(self.delete_workspace_by_id(id));
597 }
598 continue;
599 }
600
601 if location.paths().iter().all(|path| path.exists())
602 && location.paths().iter().any(|path| path.is_dir())
603 {
604 result.push((id, location.into()));
605 } else {
606 delete_tasks.push(self.delete_workspace_by_id(id));
607 }
608 }
609
610 futures::future::join_all(delete_tasks).await;
611 Ok(result)
612 }
613
614 pub async fn last_workspace(&self) -> Result<Option<LocalPaths>> {
615 Ok(self
616 .recent_workspaces_on_disk()
617 .await?
618 .into_iter()
619 .filter_map(|(_, location)| match location {
620 SerializedWorkspaceLocation::Local(local_paths) => Some(local_paths),
621 SerializedWorkspaceLocation::DevServer(_) => None,
622 })
623 .next())
624 }
625
626 fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
627 Ok(self
628 .get_pane_group(workspace_id, None)?
629 .into_iter()
630 .next()
631 .unwrap_or_else(|| {
632 SerializedPaneGroup::Pane(SerializedPane {
633 active: true,
634 children: vec![],
635 })
636 }))
637 }
638
639 fn get_pane_group(
640 &self,
641 workspace_id: WorkspaceId,
642 group_id: Option<GroupId>,
643 ) -> Result<Vec<SerializedPaneGroup>> {
644 type GroupKey = (Option<GroupId>, WorkspaceId);
645 type GroupOrPane = (
646 Option<GroupId>,
647 Option<SerializedAxis>,
648 Option<PaneId>,
649 Option<bool>,
650 Option<String>,
651 );
652 self.select_bound::<GroupKey, GroupOrPane>(sql!(
653 SELECT group_id, axis, pane_id, active, flexes
654 FROM (SELECT
655 group_id,
656 axis,
657 NULL as pane_id,
658 NULL as active,
659 position,
660 parent_group_id,
661 workspace_id,
662 flexes
663 FROM pane_groups
664 UNION
665 SELECT
666 NULL,
667 NULL,
668 center_panes.pane_id,
669 panes.active as active,
670 position,
671 parent_group_id,
672 panes.workspace_id as workspace_id,
673 NULL
674 FROM center_panes
675 JOIN panes ON center_panes.pane_id = panes.pane_id)
676 WHERE parent_group_id IS ? AND workspace_id = ?
677 ORDER BY position
678 ))?((group_id, workspace_id))?
679 .into_iter()
680 .map(|(group_id, axis, pane_id, active, flexes)| {
681 if let Some((group_id, axis)) = group_id.zip(axis) {
682 let flexes = flexes
683 .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
684 .transpose()?;
685
686 Ok(SerializedPaneGroup::Group {
687 axis,
688 children: self.get_pane_group(workspace_id, Some(group_id))?,
689 flexes,
690 })
691 } else if let Some((pane_id, active)) = pane_id.zip(active) {
692 Ok(SerializedPaneGroup::Pane(SerializedPane::new(
693 self.get_items(pane_id)?,
694 active,
695 )))
696 } else {
697 bail!("Pane Group Child was neither a pane group or a pane");
698 }
699 })
700 // Filter out panes and pane groups which don't have any children or items
701 .filter(|pane_group| match pane_group {
702 Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
703 Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
704 _ => true,
705 })
706 .collect::<Result<_>>()
707 }
708
709 fn save_pane_group(
710 conn: &Connection,
711 workspace_id: WorkspaceId,
712 pane_group: &SerializedPaneGroup,
713 parent: Option<(GroupId, usize)>,
714 ) -> Result<()> {
715 match pane_group {
716 SerializedPaneGroup::Group {
717 axis,
718 children,
719 flexes,
720 } => {
721 let (parent_id, position) = unzip_option(parent);
722
723 let flex_string = flexes
724 .as_ref()
725 .map(|flexes| serde_json::json!(flexes).to_string());
726
727 let group_id = conn.select_row_bound::<_, i64>(sql!(
728 INSERT INTO pane_groups(
729 workspace_id,
730 parent_group_id,
731 position,
732 axis,
733 flexes
734 )
735 VALUES (?, ?, ?, ?, ?)
736 RETURNING group_id
737 ))?((
738 workspace_id,
739 parent_id,
740 position,
741 *axis,
742 flex_string,
743 ))?
744 .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
745
746 for (position, group) in children.iter().enumerate() {
747 Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
748 }
749
750 Ok(())
751 }
752 SerializedPaneGroup::Pane(pane) => {
753 Self::save_pane(conn, workspace_id, pane, parent)?;
754 Ok(())
755 }
756 }
757 }
758
759 fn save_pane(
760 conn: &Connection,
761 workspace_id: WorkspaceId,
762 pane: &SerializedPane,
763 parent: Option<(GroupId, usize)>,
764 ) -> Result<PaneId> {
765 let pane_id = conn.select_row_bound::<_, i64>(sql!(
766 INSERT INTO panes(workspace_id, active)
767 VALUES (?, ?)
768 RETURNING pane_id
769 ))?((workspace_id, pane.active))?
770 .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
771
772 let (parent_id, order) = unzip_option(parent);
773 conn.exec_bound(sql!(
774 INSERT INTO center_panes(pane_id, parent_group_id, position)
775 VALUES (?, ?, ?)
776 ))?((pane_id, parent_id, order))?;
777
778 Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
779
780 Ok(pane_id)
781 }
782
783 fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
784 self.select_bound(sql!(
785 SELECT kind, item_id, active, preview FROM items
786 WHERE pane_id = ?
787 ORDER BY position
788 ))?(pane_id)
789 }
790
791 fn save_items(
792 conn: &Connection,
793 workspace_id: WorkspaceId,
794 pane_id: PaneId,
795 items: &[SerializedItem],
796 ) -> Result<()> {
797 let mut insert = conn.exec_bound(sql!(
798 INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
799 )).context("Preparing insertion")?;
800 for (position, item) in items.iter().enumerate() {
801 insert((workspace_id, pane_id, position, item))?;
802 }
803
804 Ok(())
805 }
806
807 query! {
808 pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
809 UPDATE workspaces
810 SET timestamp = CURRENT_TIMESTAMP
811 WHERE workspace_id = ?
812 }
813 }
814
815 query! {
816 pub(crate) async fn set_window_bounds(workspace_id: WorkspaceId, bounds: SerializedWindowsBounds, display: Uuid) -> Result<()> {
817 UPDATE workspaces
818 SET window_state = ?2,
819 window_x = ?3,
820 window_y = ?4,
821 window_width = ?5,
822 window_height = ?6,
823 display = ?7
824 WHERE workspace_id = ?1
825 }
826 }
827
828 query! {
829 pub(crate) async fn set_fullscreen(workspace_id: WorkspaceId, fullscreen: bool) -> Result<()> {
830 UPDATE workspaces
831 SET fullscreen = ?2
832 WHERE workspace_id = ?1
833 }
834 }
835
836 query! {
837 pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
838 UPDATE workspaces
839 SET centered_layout = ?2
840 WHERE workspace_id = ?1
841 }
842 }
843}
844
845#[cfg(test)]
846mod tests {
847 use super::*;
848 use db::open_test_db;
849 use gpui;
850
851 #[gpui::test]
852 async fn test_next_id_stability() {
853 env_logger::try_init().ok();
854
855 let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
856
857 db.write(|conn| {
858 conn.migrate(
859 "test_table",
860 &[sql!(
861 CREATE TABLE test_table(
862 text TEXT,
863 workspace_id INTEGER,
864 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
865 ON DELETE CASCADE
866 ) STRICT;
867 )],
868 )
869 .unwrap();
870 })
871 .await;
872
873 let id = db.next_id().await.unwrap();
874 // Assert the empty row got inserted
875 assert_eq!(
876 Some(id),
877 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
878 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
879 ))
880 .unwrap()(id)
881 .unwrap()
882 );
883
884 db.write(move |conn| {
885 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
886 .unwrap()(("test-text-1", id))
887 .unwrap()
888 })
889 .await;
890
891 let test_text_1 = db
892 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
893 .unwrap()(1)
894 .unwrap()
895 .unwrap();
896 assert_eq!(test_text_1, "test-text-1");
897 }
898
899 #[gpui::test]
900 async fn test_workspace_id_stability() {
901 env_logger::try_init().ok();
902
903 let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
904
905 db.write(|conn| {
906 conn.migrate(
907 "test_table",
908 &[sql!(
909 CREATE TABLE test_table(
910 text TEXT,
911 workspace_id INTEGER,
912 FOREIGN KEY(workspace_id)
913 REFERENCES workspaces(workspace_id)
914 ON DELETE CASCADE
915 ) STRICT;)],
916 )
917 })
918 .await
919 .unwrap();
920
921 let mut workspace_1 = SerializedWorkspace {
922 id: WorkspaceId(1),
923 location: LocalPaths::new(["/tmp", "/tmp2"]).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 let workspace_2 = SerializedWorkspace {
933 id: WorkspaceId(2),
934 location: LocalPaths::new(["/tmp"]).into(),
935 center_group: Default::default(),
936 bounds: Default::default(),
937 display: Default::default(),
938 docks: Default::default(),
939 fullscreen: false,
940 centered_layout: false,
941 };
942
943 db.save_workspace(workspace_1.clone()).await;
944
945 db.write(|conn| {
946 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
947 .unwrap()(("test-text-1", 1))
948 .unwrap();
949 })
950 .await;
951
952 db.save_workspace(workspace_2.clone()).await;
953
954 db.write(|conn| {
955 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
956 .unwrap()(("test-text-2", 2))
957 .unwrap();
958 })
959 .await;
960
961 workspace_1.location = LocalPaths::new(["/tmp", "/tmp3"]).into();
962 db.save_workspace(workspace_1.clone()).await;
963 db.save_workspace(workspace_1).await;
964 db.save_workspace(workspace_2).await;
965
966 let test_text_2 = db
967 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
968 .unwrap()(2)
969 .unwrap()
970 .unwrap();
971 assert_eq!(test_text_2, "test-text-2");
972
973 let test_text_1 = db
974 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
975 .unwrap()(1)
976 .unwrap()
977 .unwrap();
978 assert_eq!(test_text_1, "test-text-1");
979 }
980
981 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
982 SerializedPaneGroup::Group {
983 axis: SerializedAxis(axis),
984 flexes: None,
985 children,
986 }
987 }
988
989 #[gpui::test]
990 async fn test_full_workspace_serialization() {
991 env_logger::try_init().ok();
992
993 let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
994
995 // -----------------
996 // | 1,2 | 5,6 |
997 // | - - - | |
998 // | 3,4 | |
999 // -----------------
1000 let center_group = group(
1001 Axis::Horizontal,
1002 vec![
1003 group(
1004 Axis::Vertical,
1005 vec![
1006 SerializedPaneGroup::Pane(SerializedPane::new(
1007 vec![
1008 SerializedItem::new("Terminal", 5, false, false),
1009 SerializedItem::new("Terminal", 6, true, false),
1010 ],
1011 false,
1012 )),
1013 SerializedPaneGroup::Pane(SerializedPane::new(
1014 vec![
1015 SerializedItem::new("Terminal", 7, true, false),
1016 SerializedItem::new("Terminal", 8, false, false),
1017 ],
1018 false,
1019 )),
1020 ],
1021 ),
1022 SerializedPaneGroup::Pane(SerializedPane::new(
1023 vec![
1024 SerializedItem::new("Terminal", 9, false, false),
1025 SerializedItem::new("Terminal", 10, true, false),
1026 ],
1027 false,
1028 )),
1029 ],
1030 );
1031
1032 let workspace = SerializedWorkspace {
1033 id: WorkspaceId(5),
1034 location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
1035 center_group,
1036 bounds: Default::default(),
1037 display: Default::default(),
1038 docks: Default::default(),
1039 fullscreen: false,
1040 centered_layout: false,
1041 };
1042
1043 db.save_workspace(workspace.clone()).await;
1044 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
1045
1046 assert_eq!(workspace, round_trip_workspace.unwrap());
1047
1048 // Test guaranteed duplicate IDs
1049 db.save_workspace(workspace.clone()).await;
1050 db.save_workspace(workspace.clone()).await;
1051
1052 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
1053 assert_eq!(workspace, round_trip_workspace.unwrap());
1054 }
1055
1056 #[gpui::test]
1057 async fn test_workspace_assignment() {
1058 env_logger::try_init().ok();
1059
1060 let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
1061
1062 let workspace_1 = SerializedWorkspace {
1063 id: WorkspaceId(1),
1064 location: LocalPaths::new(["/tmp", "/tmp2"]).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 let mut workspace_2 = SerializedWorkspace {
1074 id: WorkspaceId(2),
1075 location: LocalPaths::new(["/tmp"]).into(),
1076 center_group: Default::default(),
1077 bounds: Default::default(),
1078 display: Default::default(),
1079 docks: Default::default(),
1080 fullscreen: false,
1081 centered_layout: false,
1082 };
1083
1084 db.save_workspace(workspace_1.clone()).await;
1085 db.save_workspace(workspace_2.clone()).await;
1086
1087 // Test that paths are treated as a set
1088 assert_eq!(
1089 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1090 workspace_1
1091 );
1092 assert_eq!(
1093 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
1094 workspace_1
1095 );
1096
1097 // Make sure that other keys work
1098 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
1099 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
1100
1101 // Test 'mutate' case of updating a pre-existing id
1102 workspace_2.location = LocalPaths::new(["/tmp", "/tmp2"]).into();
1103
1104 db.save_workspace(workspace_2.clone()).await;
1105 assert_eq!(
1106 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1107 workspace_2
1108 );
1109
1110 // Test other mechanism for mutating
1111 let mut workspace_3 = SerializedWorkspace {
1112 id: WorkspaceId(3),
1113 location: LocalPaths::new(&["/tmp", "/tmp2"]).into(),
1114 center_group: Default::default(),
1115 bounds: Default::default(),
1116 display: Default::default(),
1117 docks: Default::default(),
1118 fullscreen: false,
1119 centered_layout: false,
1120 };
1121
1122 db.save_workspace(workspace_3.clone()).await;
1123 assert_eq!(
1124 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1125 workspace_3
1126 );
1127
1128 // Make sure that updating paths differently also works
1129 workspace_3.location = LocalPaths::new(["/tmp3", "/tmp4", "/tmp2"]).into();
1130 db.save_workspace(workspace_3.clone()).await;
1131 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
1132 assert_eq!(
1133 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
1134 .unwrap(),
1135 workspace_3
1136 );
1137 }
1138
1139 use crate::persistence::model::SerializedWorkspace;
1140 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
1141
1142 fn default_workspace<P: AsRef<Path>>(
1143 workspace_id: &[P],
1144 center_group: &SerializedPaneGroup,
1145 ) -> SerializedWorkspace {
1146 SerializedWorkspace {
1147 id: WorkspaceId(4),
1148 location: LocalPaths::new(workspace_id).into(),
1149 center_group: center_group.clone(),
1150 bounds: Default::default(),
1151 display: Default::default(),
1152 docks: Default::default(),
1153 fullscreen: false,
1154 centered_layout: false,
1155 }
1156 }
1157
1158 #[gpui::test]
1159 async fn test_simple_split() {
1160 env_logger::try_init().ok();
1161
1162 let db = WorkspaceDb(open_test_db("simple_split").await);
1163
1164 // -----------------
1165 // | 1,2 | 5,6 |
1166 // | - - - | |
1167 // | 3,4 | |
1168 // -----------------
1169 let center_pane = group(
1170 Axis::Horizontal,
1171 vec![
1172 group(
1173 Axis::Vertical,
1174 vec![
1175 SerializedPaneGroup::Pane(SerializedPane::new(
1176 vec![
1177 SerializedItem::new("Terminal", 1, false, false),
1178 SerializedItem::new("Terminal", 2, true, false),
1179 ],
1180 false,
1181 )),
1182 SerializedPaneGroup::Pane(SerializedPane::new(
1183 vec![
1184 SerializedItem::new("Terminal", 4, false, false),
1185 SerializedItem::new("Terminal", 3, true, false),
1186 ],
1187 true,
1188 )),
1189 ],
1190 ),
1191 SerializedPaneGroup::Pane(SerializedPane::new(
1192 vec![
1193 SerializedItem::new("Terminal", 5, true, false),
1194 SerializedItem::new("Terminal", 6, false, false),
1195 ],
1196 false,
1197 )),
1198 ],
1199 );
1200
1201 let workspace = default_workspace(&["/tmp"], ¢er_pane);
1202
1203 db.save_workspace(workspace.clone()).await;
1204
1205 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
1206
1207 assert_eq!(workspace.center_group, new_workspace.center_group);
1208 }
1209
1210 #[gpui::test]
1211 async fn test_cleanup_panes() {
1212 env_logger::try_init().ok();
1213
1214 let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
1215
1216 let center_pane = group(
1217 Axis::Horizontal,
1218 vec![
1219 group(
1220 Axis::Vertical,
1221 vec![
1222 SerializedPaneGroup::Pane(SerializedPane::new(
1223 vec![
1224 SerializedItem::new("Terminal", 1, false, false),
1225 SerializedItem::new("Terminal", 2, true, false),
1226 ],
1227 false,
1228 )),
1229 SerializedPaneGroup::Pane(SerializedPane::new(
1230 vec![
1231 SerializedItem::new("Terminal", 4, false, false),
1232 SerializedItem::new("Terminal", 3, true, false),
1233 ],
1234 true,
1235 )),
1236 ],
1237 ),
1238 SerializedPaneGroup::Pane(SerializedPane::new(
1239 vec![
1240 SerializedItem::new("Terminal", 5, false, false),
1241 SerializedItem::new("Terminal", 6, true, false),
1242 ],
1243 false,
1244 )),
1245 ],
1246 );
1247
1248 let id = &["/tmp"];
1249
1250 let mut workspace = default_workspace(id, ¢er_pane);
1251
1252 db.save_workspace(workspace.clone()).await;
1253
1254 workspace.center_group = group(
1255 Axis::Vertical,
1256 vec![
1257 SerializedPaneGroup::Pane(SerializedPane::new(
1258 vec![
1259 SerializedItem::new("Terminal", 1, false, false),
1260 SerializedItem::new("Terminal", 2, true, false),
1261 ],
1262 false,
1263 )),
1264 SerializedPaneGroup::Pane(SerializedPane::new(
1265 vec![
1266 SerializedItem::new("Terminal", 4, true, false),
1267 SerializedItem::new("Terminal", 3, false, false),
1268 ],
1269 true,
1270 )),
1271 ],
1272 );
1273
1274 db.save_workspace(workspace.clone()).await;
1275
1276 let new_workspace = db.workspace_for_roots(id).unwrap();
1277
1278 assert_eq!(workspace.center_group, new_workspace.center_group);
1279 }
1280}