1#![allow(dead_code)]
2
3pub mod model;
4
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use anyhow::{anyhow, bail, Result, Context};
9use db::connection;
10use gpui::Axis;
11use indoc::indoc;
12use lazy_static::lazy_static;
13
14
15use sqlez::domain::Domain;
16use util::{iife, unzip_option, ResultExt};
17
18use crate::dock::DockPosition;
19
20use super::Workspace;
21
22use model::{
23 GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
24 SerializedWorkspace, WorkspaceId,
25};
26
27connection!(DB: WorkspaceDb<Workspace>);
28
29impl Domain for Workspace {
30 fn name() -> &'static str {
31 "workspace"
32 }
33
34 fn migrations() -> &'static [&'static str] {
35 &[indoc! {"
36 CREATE TABLE workspaces(
37 workspace_id BLOB PRIMARY KEY,
38 dock_visible INTEGER, -- Boolean
39 dock_anchor TEXT, -- Enum: 'Bottom' / 'Right' / 'Expanded'
40 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL
41 ) STRICT;
42
43 CREATE TABLE pane_groups(
44 group_id INTEGER PRIMARY KEY,
45 workspace_id BLOB NOT NULL,
46 parent_group_id INTEGER, -- NULL indicates that this is a root node
47 position INTEGER, -- NULL indicates that this is a root node
48 axis TEXT NOT NULL, -- Enum: 'Vertical' / 'Horizontal'
49 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
50 ON DELETE CASCADE
51 ON UPDATE CASCADE,
52 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
53 ) STRICT;
54
55 CREATE TABLE panes(
56 pane_id INTEGER PRIMARY KEY,
57 workspace_id BLOB NOT NULL,
58 parent_group_id INTEGER, -- NULL means that this is a dock pane
59 position INTEGER, -- NULL means that this is a dock pane
60 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
61 ON DELETE CASCADE
62 ON UPDATE CASCADE,
63 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
64 ) STRICT;
65
66 CREATE TABLE items(
67 item_id INTEGER NOT NULL, -- This is the item's view id, so this is not unique
68 workspace_id BLOB NOT NULL,
69 pane_id INTEGER NOT NULL,
70 kind TEXT NOT NULL,
71 position INTEGER NOT NULL,
72 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
73 ON DELETE CASCADE
74 ON UPDATE CASCADE,
75 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
76 ON DELETE CASCADE,
77 PRIMARY KEY(item_id, workspace_id)
78 ) STRICT;
79 "}]
80 }
81}
82
83impl WorkspaceDb {
84 /// Returns a serialized workspace for the given worktree_roots. If the passed array
85 /// is empty, the most recent workspace is returned instead. If no workspace for the
86 /// passed roots is stored, returns none.
87 pub fn workspace_for_roots<P: AsRef<Path>>(
88 &self,
89 worktree_roots: &[P],
90 ) -> Option<SerializedWorkspace> {
91 let workspace_id: WorkspaceId = worktree_roots.into();
92
93 // Note that we re-assign the workspace_id here in case it's empty
94 // and we've grabbed the most recent workspace
95 let (workspace_id, dock_position): (WorkspaceId, DockPosition) = iife!({
96 if worktree_roots.len() == 0 {
97 self.select_row(indoc! {"
98 SELECT workspace_id, dock_visible, dock_anchor
99 FROM workspaces
100 ORDER BY timestamp DESC LIMIT 1"})?()?
101 } else {
102 self.select_row_bound(indoc! {"
103 SELECT workspace_id, dock_visible, dock_anchor
104 FROM workspaces
105 WHERE workspace_id = ?"})?(&workspace_id)?
106 }
107 .context("No workspaces found")
108 })
109 .warn_on_err()
110 .flatten()?;
111
112 Some(SerializedWorkspace {
113 workspace_id: workspace_id.clone(),
114 dock_pane: self
115 .get_dock_pane(&workspace_id)
116 .context("Getting dock pane")
117 .log_err()?,
118 center_group: self
119 .get_center_pane_group(&workspace_id)
120 .context("Getting center group")
121 .log_err()?,
122 dock_position,
123 })
124 }
125
126 /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
127 /// that used this workspace previously
128 pub fn save_workspace(
129 &self,
130 old_id: Option<WorkspaceId>,
131 workspace: &SerializedWorkspace,
132 ) {
133 self.with_savepoint("update_worktrees", || {
134 if let Some(old_id) = old_id {
135 self.exec_bound(indoc! {"
136 DELETE FROM pane_groups WHERE workspace_id = ?"})?(&old_id)?;
137
138 // If collision, delete
139
140 self.exec_bound(indoc! {"
141 UPDATE OR REPLACE workspaces
142 SET workspace_id = ?,
143 dock_visible = ?,
144 dock_anchor = ?,
145 timestamp = CURRENT_TIMESTAMP
146 WHERE workspace_id = ?"})?((
147 &workspace.workspace_id,
148 workspace.dock_position,
149 &old_id,
150 ))?;
151 } else {
152 self.exec_bound(indoc! {"
153 DELETE FROM pane_groups WHERE workspace_id = ?"})?(&workspace.workspace_id)?;
154 self.exec_bound(
155 "INSERT OR REPLACE INTO workspaces(workspace_id, dock_visible, dock_anchor) VALUES (?, ?, ?)",
156 )?((&workspace.workspace_id, workspace.dock_position))?;
157 }
158
159 // Save center pane group and dock pane
160 self.save_pane_group(&workspace.workspace_id, &workspace.center_group, None)?;
161 self.save_pane(&workspace.workspace_id, &workspace.dock_pane, None)?;
162
163 Ok(())
164 })
165 .with_context(|| {
166 format!(
167 "Update workspace with roots {:?} failed.",
168 workspace.workspace_id.paths()
169 )
170 })
171 .log_err();
172 }
173
174 /// Returns the previous workspace ids sorted by last modified along with their opened worktree roots
175 pub fn recent_workspaces(&self, limit: usize) -> Vec<Arc<Vec<PathBuf>>> {
176 iife!({
177 // TODO, upgrade anyhow: https://docs.rs/anyhow/1.0.66/anyhow/fn.Ok.html
178 Ok::<_, anyhow::Error>(
179 self.select_bound::<usize, WorkspaceId>(
180 "SELECT workspace_id FROM workspaces ORDER BY timestamp DESC LIMIT ?",
181 )?(limit)?
182 .into_iter()
183 .map(|id| id.paths())
184 .collect::<Vec<Arc<Vec<PathBuf>>>>(),
185 )
186 })
187 .log_err()
188 .unwrap_or_default()
189 }
190
191 pub(crate) fn get_center_pane_group(
192 &self,
193 workspace_id: &WorkspaceId,
194 ) -> Result<SerializedPaneGroup> {
195 self.get_pane_group_children(workspace_id, None)?
196 .into_iter()
197 .next()
198 .context("No center pane group")
199 .map(|pane_group| {
200 // Rewrite the special case of the root being a leaf node
201 if let SerializedPaneGroup::Group { axis: Axis::Horizontal, ref children } = pane_group {
202 if children.len() == 1 {
203 if let Some(SerializedPaneGroup::Pane(pane)) = children.get(0) {
204 return SerializedPaneGroup::Pane(pane.clone())
205 }
206 }
207 }
208 pane_group
209 })
210 }
211
212 fn get_pane_group_children<'a>(
213 &self,
214 workspace_id: &WorkspaceId,
215 group_id: Option<GroupId>,
216 ) -> Result<Vec<SerializedPaneGroup>> {
217 self.select_bound::<(Option<GroupId>, &WorkspaceId), (Option<GroupId>, Option<Axis>, Option<PaneId>)>(indoc! {"
218 SELECT group_id, axis, pane_id
219 FROM (SELECT group_id, axis, NULL as pane_id, position, parent_group_id, workspace_id
220 FROM pane_groups
221 UNION
222 SELECT NULL, NULL, pane_id, position, parent_group_id, workspace_id
223 FROM panes
224 -- Remove the dock panes from the union
225 WHERE parent_group_id IS NOT NULL and position IS NOT NULL)
226 WHERE parent_group_id IS ? AND workspace_id = ?
227 ORDER BY position
228 "})?((group_id, workspace_id))?
229 .into_iter()
230 .map(|(group_id, axis, pane_id)| {
231 if let Some((group_id, axis)) = group_id.zip(axis) {
232 Ok(SerializedPaneGroup::Group {
233 axis,
234 children: self.get_pane_group_children(
235 workspace_id,
236 Some(group_id),
237 )?,
238 })
239 } else if let Some(pane_id) = pane_id {
240 Ok(SerializedPaneGroup::Pane(SerializedPane {
241 children: self.get_items( pane_id)?,
242 }))
243 } else {
244 bail!("Pane Group Child was neither a pane group or a pane");
245 }
246 })
247 .collect::<Result<_>>()
248 }
249
250 pub(crate) fn save_pane_group(
251 &self,
252 workspace_id: &WorkspaceId,
253 pane_group: &SerializedPaneGroup,
254 parent: Option<(GroupId, usize)>,
255 ) -> Result<()> {
256 // Rewrite the root node to fit with the database
257 let pane_group = if parent.is_none() && matches!(pane_group, SerializedPaneGroup::Pane { .. }) {
258 SerializedPaneGroup::Group { axis: Axis::Horizontal, children: vec![pane_group.clone()] }
259 } else {
260 pane_group.clone()
261 };
262
263 match pane_group {
264 SerializedPaneGroup::Group { axis, children } => {
265 let (parent_id, position) = unzip_option(parent);
266
267 let group_id = self.select_row_bound::<_, i64>(indoc!{"
268 INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis)
269 VALUES (?, ?, ?, ?)
270 RETURNING group_id"})?
271 ((workspace_id, parent_id, position, axis))?
272 .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
273
274 for (position, group) in children.iter().enumerate() {
275 self.save_pane_group(workspace_id, group, Some((group_id, position)))?
276 }
277 Ok(())
278 }
279 SerializedPaneGroup::Pane(pane) => {
280 self.save_pane(workspace_id, &pane, parent)
281 },
282 }
283 }
284
285 pub(crate) fn get_dock_pane(&self, workspace_id: &WorkspaceId) -> Result<SerializedPane> {
286 let pane_id = self.select_row_bound(indoc! {"
287 SELECT pane_id FROM panes
288 WHERE workspace_id = ? AND parent_group_id IS NULL AND position IS NULL"})?(
289 workspace_id,
290 )?
291 .context("No dock pane for workspace")?;
292
293 Ok(SerializedPane::new(
294 self.get_items(pane_id).context("Reading items")?,
295 ))
296 }
297
298 pub(crate) fn save_pane(
299 &self,
300 workspace_id: &WorkspaceId,
301 pane: &SerializedPane,
302 parent: Option<(GroupId, usize)>,
303 ) -> Result<()> {
304 let (parent_id, order) = unzip_option(parent);
305
306 let pane_id = self.select_row_bound::<_, i64>(indoc!{"
307 INSERT INTO panes(workspace_id, parent_group_id, position)
308 VALUES (?, ?, ?)
309 RETURNING pane_id"},
310 )?((workspace_id, parent_id, order))?
311 .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
312
313 self.save_items(workspace_id, pane_id, &pane.children)
314 .context("Saving items")
315 }
316
317 pub(crate) fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
318 Ok(self.select_bound(indoc! {"
319 SELECT kind, item_id FROM items
320 WHERE pane_id = ?
321 ORDER BY position"})?(pane_id)?)
322 }
323
324 pub(crate) fn save_items(
325 &self,
326 workspace_id: &WorkspaceId,
327 pane_id: PaneId,
328 items: &[SerializedItem],
329 ) -> Result<()> {
330 let mut insert = self.exec_bound(
331 "INSERT INTO items(workspace_id, pane_id, position, kind, item_id) VALUES (?, ?, ?, ?, ?)",
332 ).context("Preparing insertion")?;
333 for (position, item) in items.iter().enumerate() {
334 insert((workspace_id, pane_id, position, item))?;
335 }
336
337 Ok(())
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use db::{open_memory_db, write_db_to};
344 use settings::DockAnchor;
345
346 use super::*;
347
348 #[test]
349 fn test_full_workspace_serialization() {
350 env_logger::try_init().ok();
351
352 let db = WorkspaceDb(open_memory_db(Some("test_full_workspace_serialization")));
353
354 let dock_pane = crate::persistence::model::SerializedPane {
355 children: vec![
356 SerializedItem::new("Terminal", 1),
357 SerializedItem::new("Terminal", 2),
358 SerializedItem::new("Terminal", 3),
359 SerializedItem::new("Terminal", 4),
360
361 ],
362 };
363
364 // -----------------
365 // | 1,2 | 5,6 |
366 // | - - - | |
367 // | 3,4 | |
368 // -----------------
369 let center_group = SerializedPaneGroup::Group {
370 axis: gpui::Axis::Horizontal,
371 children: vec![
372 SerializedPaneGroup::Group {
373 axis: gpui::Axis::Vertical,
374 children: vec![
375 SerializedPaneGroup::Pane(SerializedPane {
376 children: vec![
377 SerializedItem::new("Terminal", 5),
378 SerializedItem::new("Terminal", 6),
379 ],
380 }),
381 SerializedPaneGroup::Pane(SerializedPane {
382 children: vec![
383 SerializedItem::new("Terminal", 7),
384 SerializedItem::new("Terminal", 8),
385
386 ],
387 }),
388 ],
389 },
390 SerializedPaneGroup::Pane(SerializedPane {
391 children: vec![
392 SerializedItem::new("Terminal", 9),
393 SerializedItem::new("Terminal", 10),
394
395 ],
396 }),
397 ],
398 };
399
400 let workspace = SerializedWorkspace {
401 workspace_id: (["/tmp", "/tmp2"]).into(),
402 dock_position: DockPosition::Shown(DockAnchor::Bottom),
403 center_group,
404 dock_pane,
405 };
406
407 db.save_workspace(None, &workspace);
408 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
409
410 assert_eq!(workspace, round_trip_workspace.unwrap());
411
412 // Test guaranteed duplicate IDs
413 db.save_workspace(None, &workspace);
414 db.save_workspace(None, &workspace);
415
416 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
417 assert_eq!(workspace, round_trip_workspace.unwrap());
418
419
420 }
421
422 #[test]
423 fn test_workspace_assignment() {
424 env_logger::try_init().ok();
425
426 let db = WorkspaceDb(open_memory_db(Some("test_basic_functionality")));
427
428 let workspace_1 = SerializedWorkspace {
429 workspace_id: (["/tmp", "/tmp2"]).into(),
430 dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
431 center_group: Default::default(),
432 dock_pane: Default::default(),
433 };
434
435 let mut workspace_2 = SerializedWorkspace {
436 workspace_id: (["/tmp"]).into(),
437 dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
438 center_group: Default::default(),
439 dock_pane: Default::default(),
440 };
441
442 db.save_workspace(None, &workspace_1);
443 db.save_workspace(None, &workspace_2);
444
445 // Test that paths are treated as a set
446 assert_eq!(
447 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
448 workspace_1
449 );
450 assert_eq!(
451 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
452 workspace_1
453 );
454
455 // Make sure that other keys work
456 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
457 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
458
459 // Test 'mutate' case of updating a pre-existing id
460 workspace_2.workspace_id = (["/tmp", "/tmp2"]).into();
461 db.save_workspace(Some((&["/tmp"]).into()), &workspace_2);
462 assert_eq!(
463 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
464 workspace_2
465 );
466
467 // Test other mechanism for mutating
468 let mut workspace_3 = SerializedWorkspace {
469 workspace_id: (&["/tmp", "/tmp2"]).into(),
470 dock_position: DockPosition::Shown(DockAnchor::Right),
471 center_group: Default::default(),
472 dock_pane: Default::default(),
473 };
474
475
476 db.save_workspace(None, &workspace_3);
477 assert_eq!(
478 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
479 workspace_3
480 );
481
482 // Make sure that updating paths differently also works
483 workspace_3.workspace_id = (["/tmp3", "/tmp4", "/tmp2"]).into();
484 db.save_workspace(
485 Some((&["/tmp", "/tmp2"]).into()),
486 &workspace_3,
487 );
488 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
489 assert_eq!(
490 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
491 .unwrap(),
492 workspace_3
493 );
494
495
496 }
497
498 use crate::dock::DockPosition;
499 use crate::persistence::model::SerializedWorkspace;
500 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
501
502 fn default_workspace<P: AsRef<Path>>(
503 workspace_id: &[P],
504 dock_pane: SerializedPane,
505 center_group: &SerializedPaneGroup,
506 ) -> SerializedWorkspace {
507 SerializedWorkspace {
508 workspace_id: workspace_id.into(),
509 dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
510 center_group: center_group.clone(),
511 dock_pane,
512 }
513 }
514
515 #[test]
516 fn test_basic_dock_pane() {
517 env_logger::try_init().ok();
518
519 let db = WorkspaceDb(open_memory_db(Some("basic_dock_pane")));
520
521 let dock_pane = crate::persistence::model::SerializedPane {
522 children: vec![
523 SerializedItem::new("Terminal", 1),
524 SerializedItem::new("Terminal", 4),
525 SerializedItem::new("Terminal", 2),
526 SerializedItem::new("Terminal", 3),
527 ],
528 };
529
530 let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default());
531
532 db.save_workspace(None, &workspace);
533 write_db_to(&db, "dest.db").unwrap();
534 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
535
536 assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
537 }
538
539 #[test]
540 fn test_simple_split() {
541 // env_logger::try_init().ok();
542
543 let db = WorkspaceDb(open_memory_db(Some("simple_split")));
544
545 // -----------------
546 // | 1,2 | 5,6 |
547 // | - - - | |
548 // | 3,4 | |
549 // -----------------
550 let center_pane = SerializedPaneGroup::Group {
551 axis: gpui::Axis::Horizontal,
552 children: vec![
553 SerializedPaneGroup::Group {
554 axis: gpui::Axis::Vertical,
555 children: vec![
556 SerializedPaneGroup::Pane(SerializedPane {
557 children: vec![
558 SerializedItem::new("Terminal", 1),
559 SerializedItem::new("Terminal", 2),
560 ],
561 }),
562 SerializedPaneGroup::Pane(SerializedPane {
563 children: vec![
564 SerializedItem::new("Terminal", 4),
565 SerializedItem::new("Terminal", 3),
566 ],
567 }),
568 ],
569 },
570 SerializedPaneGroup::Pane(SerializedPane {
571 children: vec![
572 SerializedItem::new("Terminal", 5),
573 SerializedItem::new("Terminal", 6),
574 ],
575 }),
576 ],
577 };
578
579 let workspace = default_workspace(&["/tmp"], Default::default(), ¢er_pane);
580
581 db.save_workspace(None, &workspace);
582
583 assert_eq!(workspace.center_group, center_pane);
584 }
585}