1#![allow(dead_code)]
2
3pub mod model;
4
5use std::ops::Deref;
6use std::path::{Path, PathBuf};
7
8use anyhow::{bail, Context, Result};
9use db::open_file_db;
10use gpui::Axis;
11use indoc::indoc;
12use lazy_static::lazy_static;
13
14use sqlez::thread_safe_connection::ThreadSafeConnection;
15use sqlez::{connection::Connection, domain::Domain, migrations::Migration};
16use util::{iife, unzip_option, ResultExt};
17
18use super::Workspace;
19
20use model::{
21 GroupId, PaneId, SerializedItem, SerializedItemKind, SerializedPane, SerializedPaneGroup,
22 SerializedWorkspace, WorkspaceId,
23};
24
25lazy_static! {
26 pub static ref DB: WorkspaceDb = WorkspaceDb(open_file_db());
27}
28
29pub struct WorkspaceDb(ThreadSafeConnection<Workspace>);
30
31impl Deref for WorkspaceDb {
32 type Target = ThreadSafeConnection<Workspace>;
33
34 fn deref(&self) -> &Self::Target {
35 &self.0
36 }
37}
38
39pub(crate) const WORKSPACES_MIGRATION: Migration = Migration::new(
40 "workspace",
41 &[indoc! {"
42 CREATE TABLE workspaces(
43 workspace_id BLOB PRIMARY KEY,
44 dock_anchor TEXT, -- Enum: 'Bottom' / 'Right' / 'Expanded'
45 dock_visible INTEGER, -- Boolean
46 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL
47 ) STRICT;
48
49 CREATE TABLE pane_groups(
50 group_id INTEGER PRIMARY KEY,
51 workspace_id BLOB NOT NULL,
52 parent_group_id INTEGER, -- NULL indicates that this is a root node
53 position INTEGER, -- NULL indicates that this is a root node
54 axis TEXT NOT NULL, -- Enum: 'Vertical' / 'Horizontal'
55 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
56 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
57 ) STRICT;
58
59 CREATE TABLE panes(
60 pane_id INTEGER PRIMARY KEY,
61 workspace_id BLOB NOT NULL,
62 parent_group_id INTEGER, -- NULL, this is a dock pane
63 position INTEGER, -- NULL, this is a dock pane
64 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
65 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
66 ) STRICT;
67
68 CREATE TABLE items(
69 item_id INTEGER NOT NULL, -- This is the item's view id, so this is not unique
70 workspace_id BLOB NOT NULL,
71 pane_id INTEGER NOT NULL,
72 kind TEXT NOT NULL,
73 position INTEGER NOT NULL,
74 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
75 FOREIGN KEY(pane_id) REFERENCES panes(pane_id) ON DELETE CASCADE
76 PRIMARY KEY(item_id, workspace_id)
77 ) STRICT;
78 "}],
79);
80
81impl Domain for Workspace {
82 fn migrate(conn: &Connection) -> anyhow::Result<()> {
83 WORKSPACES_MIGRATION.run(&conn)
84 }
85}
86
87impl WorkspaceDb {
88 /// Returns a serialized workspace for the given worktree_roots. If the passed array
89 /// is empty, the most recent workspace is returned instead. If no workspace for the
90 /// passed roots is stored, returns none.
91 pub fn workspace_for_roots<P: AsRef<Path>>(
92 &self,
93 worktree_roots: &[P],
94 ) -> Option<SerializedWorkspace> {
95 let workspace_id: WorkspaceId = worktree_roots.into();
96
97 // Note that we re-assign the workspace_id here in case it's empty
98 // and we've grabbed the most recent workspace
99 let (workspace_id, dock_anchor, dock_visible) = iife!({
100 if worktree_roots.len() == 0 {
101 self.select_row(indoc! {"
102 SELECT workspace_id, dock_anchor, dock_visible
103 FROM workspaces
104 ORDER BY timestamp DESC LIMIT 1"})?()?
105 } else {
106 self.select_row_bound(indoc! {"
107 SELECT workspace_id, dock_anchor, dock_visible
108 FROM workspaces
109 WHERE workspace_id = ?"})?(&workspace_id)?
110 }
111 .context("No workspaces found")
112 })
113 .warn_on_err()
114 .flatten()?;
115
116 Some(SerializedWorkspace {
117 dock_pane: self
118 .get_dock_pane(&workspace_id)
119 .context("Getting dock pane")
120 .log_err()?,
121 center_group: self
122 .get_center_pane_group(&workspace_id)
123 .context("Getting center group")
124 .log_err()?,
125 dock_anchor,
126 dock_visible,
127 })
128 }
129
130 /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
131 /// that used this workspace previously
132 pub fn save_workspace<P: AsRef<Path>>(
133 &self,
134 worktree_roots: &[P],
135 old_roots: Option<&[P]>,
136 workspace: &SerializedWorkspace,
137 ) {
138 let workspace_id: WorkspaceId = worktree_roots.into();
139
140 self.with_savepoint("update_worktrees", || {
141 if let Some(old_roots) = old_roots {
142 let old_id: WorkspaceId = old_roots.into();
143
144 self.exec_bound("DELETE FROM WORKSPACES WHERE workspace_id = ?")?(&old_id)?;
145 }
146
147 // Delete any previous workspaces with the same roots. This cascades to all
148 // other tables that are based on the same roots set.
149 // Insert new workspace into workspaces table if none were found
150 self.exec_bound("DELETE FROM workspaces WHERE workspace_id = ?;")?(&workspace_id)?;
151
152 self.exec_bound(
153 "INSERT INTO workspaces(workspace_id, dock_anchor, dock_visible) VALUES (?, ?, ?)",
154 )?((&workspace_id, workspace.dock_anchor, workspace.dock_visible))?;
155
156 // Save center pane group and dock pane
157 self.save_pane_group(&workspace_id, &workspace.center_group, None)?;
158 self.save_pane(&workspace_id, &workspace.dock_pane, None)?;
159
160 Ok(())
161 })
162 .with_context(|| {
163 format!(
164 "Update workspace with roots {:?}",
165 worktree_roots
166 .iter()
167 .map(|p| p.as_ref())
168 .collect::<Vec<_>>()
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<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<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 }
200
201 fn get_pane_group_children<'a>(
202 &self,
203 workspace_id: &WorkspaceId,
204 group_id: Option<GroupId>,
205 ) -> Result<Vec<SerializedPaneGroup>> {
206 self.select_bound::<(Option<GroupId>, &WorkspaceId), (Option<GroupId>, Option<Axis>, Option<PaneId>)>(indoc! {"
207 SELECT group_id, axis, pane_id
208 FROM (SELECT group_id, axis, NULL as pane_id, position, parent_group_id, workspace_id
209 FROM pane_groups
210 UNION
211 SELECT NULL, NULL, pane_id, position, parent_group_id, workspace_id
212 FROM panes
213 -- Remove the dock panes from the union
214 WHERE parent_group_id IS NOT NULL and position IS NOT NULL)
215 WHERE parent_group_id IS ? AND workspace_id = ?
216 ORDER BY position
217 "})?((group_id, workspace_id))?
218 .into_iter()
219 .map(|(group_id, axis, pane_id)| {
220 if let Some((group_id, axis)) = group_id.zip(axis) {
221 Ok(SerializedPaneGroup::Group {
222 axis,
223 children: self.get_pane_group_children(
224 workspace_id,
225 Some(group_id),
226 )?,
227 })
228 } else if let Some(pane_id) = pane_id {
229 Ok(SerializedPaneGroup::Pane(SerializedPane {
230 children: self.get_items( pane_id)?,
231 }))
232 } else {
233 bail!("Pane Group Child was neither a pane group or a pane");
234 }
235 })
236 .collect::<Result<_>>()
237 }
238
239 pub(crate) fn save_pane_group(
240 &self,
241 workspace_id: &WorkspaceId,
242 pane_group: &SerializedPaneGroup,
243 parent: Option<(GroupId, usize)>,
244 ) -> Result<()> {
245 if parent.is_none() && !matches!(pane_group, SerializedPaneGroup::Group { .. }) {
246 bail!("Pane groups must have a SerializedPaneGroup::Group at the root")
247 }
248
249 let (parent_id, position) = unzip_option(parent);
250
251 match pane_group {
252 SerializedPaneGroup::Group { axis, children } => {
253 let parent_id = self.insert_bound("INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) VALUES (?, ?, ?, ?)")?
254 ((workspace_id, parent_id, position, *axis))?;
255
256 for (position, group) in children.iter().enumerate() {
257 self.save_pane_group(workspace_id, group, Some((parent_id, position)))?
258 }
259 Ok(())
260 }
261 SerializedPaneGroup::Pane(pane) => self.save_pane(workspace_id, pane, parent),
262 }
263 }
264
265 pub(crate) fn get_dock_pane(&self, workspace_id: &WorkspaceId) -> Result<SerializedPane> {
266 let pane_id = self.select_row_bound(indoc! {"
267 SELECT pane_id FROM panes
268 WHERE workspace_id = ? AND parent_group_id IS NULL AND position IS NULL"})?(
269 workspace_id,
270 )?
271 .context("No dock pane for workspace")?;
272
273 Ok(SerializedPane::new(
274 self.get_items(pane_id).context("Reading items")?,
275 ))
276 }
277
278 pub(crate) fn save_pane(
279 &self,
280 workspace_id: &WorkspaceId,
281 pane: &SerializedPane,
282 parent: Option<(GroupId, usize)>,
283 ) -> Result<()> {
284 let (parent_id, order) = unzip_option(parent);
285
286 let pane_id = self.insert_bound(
287 "INSERT INTO panes(workspace_id, parent_group_id, position) VALUES (?, ?, ?)",
288 )?((workspace_id, parent_id, order))?;
289
290 self.save_items(workspace_id, pane_id, &pane.children)
291 .context("Saving items")
292 }
293
294 pub(crate) fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
295 Ok(self.select_bound(indoc! {"
296 SELECT item_id, kind FROM items
297 WHERE pane_id = ?
298 ORDER BY position"})?(pane_id)?
299 .into_iter()
300 .map(|(item_id, kind)| match kind {
301 SerializedItemKind::Terminal => SerializedItem::Terminal { item_id },
302 _ => unimplemented!(),
303 })
304 .collect())
305 }
306
307 pub(crate) fn save_items(
308 &self,
309 workspace_id: &WorkspaceId,
310 pane_id: PaneId,
311 items: &[SerializedItem],
312 ) -> Result<()> {
313 let mut delete_old = self
314 .exec_bound("DELETE FROM items WHERE workspace_id = ? AND pane_id = ? AND item_id = ?")
315 .context("Preparing deletion")?;
316 let mut insert_new = self.exec_bound(
317 "INSERT INTO items(item_id, workspace_id, pane_id, kind, position) VALUES (?, ?, ?, ?, ?)",
318 ).context("Preparing insertion")?;
319 for (position, item) in items.iter().enumerate() {
320 delete_old((workspace_id, pane_id, item.item_id()))?;
321 insert_new((item.item_id(), workspace_id, pane_id, item.kind(), position))?;
322 }
323
324 Ok(())
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use db::open_memory_db;
331 use settings::DockAnchor;
332
333 use super::*;
334
335 #[test]
336 fn test_workspace_assignment() {
337 // env_logger::try_init().ok();
338
339 let db = WorkspaceDb(open_memory_db("test_basic_functionality"));
340
341 let workspace_1 = SerializedWorkspace {
342 dock_anchor: DockAnchor::Bottom,
343 dock_visible: true,
344 center_group: Default::default(),
345 dock_pane: Default::default(),
346 };
347
348 let workspace_2 = SerializedWorkspace {
349 dock_anchor: DockAnchor::Expanded,
350 dock_visible: false,
351 center_group: Default::default(),
352 dock_pane: Default::default(),
353 };
354
355 let workspace_3 = SerializedWorkspace {
356 dock_anchor: DockAnchor::Right,
357 dock_visible: true,
358 center_group: Default::default(),
359 dock_pane: Default::default(),
360 };
361
362 db.save_workspace(&["/tmp", "/tmp2"], None, &workspace_1);
363 db.save_workspace(&["/tmp"], None, &workspace_2);
364
365 db::write_db_to(&db, "test.db").unwrap();
366
367 // Test that paths are treated as a set
368 assert_eq!(
369 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
370 workspace_1
371 );
372 assert_eq!(
373 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
374 workspace_1
375 );
376
377 // Make sure that other keys work
378 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
379 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
380
381 // Test 'mutate' case of updating a pre-existing id
382 db.save_workspace(&["/tmp", "/tmp2"], Some(&["/tmp", "/tmp2"]), &workspace_2);
383 assert_eq!(
384 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
385 workspace_2
386 );
387
388 // Test other mechanism for mutating
389 db.save_workspace(&["/tmp", "/tmp2"], None, &workspace_3);
390 assert_eq!(
391 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
392 workspace_3
393 );
394
395 // Make sure that updating paths differently also works
396 db.save_workspace(
397 &["/tmp3", "/tmp4", "/tmp2"],
398 Some(&["/tmp", "/tmp2"]),
399 &workspace_3,
400 );
401 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
402 assert_eq!(
403 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
404 .unwrap(),
405 workspace_3
406 );
407 }
408
409 use crate::persistence::model::SerializedWorkspace;
410 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
411
412 fn default_workspace(
413 dock_pane: SerializedPane,
414 center_group: &SerializedPaneGroup,
415 ) -> SerializedWorkspace {
416 SerializedWorkspace {
417 dock_anchor: DockAnchor::Right,
418 dock_visible: false,
419 center_group: center_group.clone(),
420 dock_pane,
421 }
422 }
423
424 #[test]
425 fn test_basic_dock_pane() {
426 // env_logger::try_init().ok();
427
428 let db = WorkspaceDb(open_memory_db("basic_dock_pane"));
429
430 let dock_pane = crate::persistence::model::SerializedPane {
431 children: vec![
432 SerializedItem::Terminal { item_id: 1 },
433 SerializedItem::Terminal { item_id: 4 },
434 SerializedItem::Terminal { item_id: 2 },
435 SerializedItem::Terminal { item_id: 3 },
436 ],
437 };
438
439 let workspace = default_workspace(dock_pane, &Default::default());
440
441 db.save_workspace(&["/tmp"], None, &workspace);
442
443 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
444
445 assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
446 }
447
448 #[test]
449 fn test_simple_split() {
450 // env_logger::try_init().ok();
451
452 let db = WorkspaceDb(open_memory_db("simple_split"));
453
454 // -----------------
455 // | 1,2 | 5,6 |
456 // | - - - | |
457 // | 3,4 | |
458 // -----------------
459 let center_pane = SerializedPaneGroup::Group {
460 axis: gpui::Axis::Horizontal,
461 children: vec![
462 SerializedPaneGroup::Group {
463 axis: gpui::Axis::Vertical,
464 children: vec![
465 SerializedPaneGroup::Pane(SerializedPane {
466 children: vec![
467 SerializedItem::Terminal { item_id: 1 },
468 SerializedItem::Terminal { item_id: 2 },
469 ],
470 }),
471 SerializedPaneGroup::Pane(SerializedPane {
472 children: vec![
473 SerializedItem::Terminal { item_id: 4 },
474 SerializedItem::Terminal { item_id: 3 },
475 ],
476 }),
477 ],
478 },
479 SerializedPaneGroup::Pane(SerializedPane {
480 children: vec![
481 SerializedItem::Terminal { item_id: 5 },
482 SerializedItem::Terminal { item_id: 6 },
483 ],
484 }),
485 ],
486 };
487
488 let workspace = default_workspace(Default::default(), ¢er_pane);
489
490 db.save_workspace(&["/tmp"], None, &workspace);
491
492 assert_eq!(workspace.center_group, center_pane);
493 }
494}