1pub(crate) mod items;
2pub mod model;
3pub(crate) mod pane;
4
5use anyhow::Context;
6use util::{iife, ResultExt};
7
8use std::path::{Path, PathBuf};
9
10use indoc::indoc;
11use sqlez::migrations::Migration;
12
13pub(crate) const WORKSPACES_MIGRATION: Migration = Migration::new(
14 "workspace",
15 &[indoc! {"
16 CREATE TABLE workspaces(
17 workspace_id BLOB PRIMARY KEY,
18 dock_anchor TEXT, -- Enum: 'Bottom' / 'Right' / 'Expanded'
19 dock_visible INTEGER, -- Boolean
20 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL
21 ) STRICT;
22 "}],
23);
24
25use self::model::{SerializedWorkspace, WorkspaceId, WorkspaceRow};
26
27use super::Db;
28
29impl Db {
30 /// Returns a serialized workspace for the given worktree_roots. If the passed array
31 /// is empty, the most recent workspace is returned instead. If no workspace for the
32 /// passed roots is stored, returns none.
33 pub fn workspace_for_roots<P: AsRef<Path>>(
34 &self,
35 worktree_roots: &[P],
36 ) -> Option<SerializedWorkspace> {
37 let workspace_id: WorkspaceId = worktree_roots.into();
38
39 // Note that we re-assign the workspace_id here in case it's empty
40 // and we've grabbed the most recent workspace
41 let (workspace_id, dock_anchor, dock_visible) = iife!({
42 if worktree_roots.len() == 0 {
43 self.prepare(indoc! {"
44 SELECT workspace_id, dock_anchor, dock_visible
45 FROM workspaces
46 ORDER BY timestamp DESC LIMIT 1"})?
47 .maybe_row::<WorkspaceRow>()
48 } else {
49 self.prepare(indoc! {"
50 SELECT workspace_id, dock_anchor, dock_visible
51 FROM workspaces
52 WHERE workspace_id = ?"})?
53 .with_bindings(&workspace_id)?
54 .maybe_row::<WorkspaceRow>()
55 }
56 })
57 .log_err()
58 .flatten()?;
59
60 Some(SerializedWorkspace {
61 dock_pane: self
62 .get_dock_pane(&workspace_id)
63 .context("Getting dock pane")
64 .log_err()?,
65 center_group: self
66 .get_center_pane_group(&workspace_id)
67 .context("Getting center group")
68 .log_err()?,
69 dock_anchor,
70 dock_visible,
71 })
72 }
73
74 /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
75 /// that used this workspace previously
76 pub fn save_workspace<P: AsRef<Path>>(
77 &self,
78 worktree_roots: &[P],
79 old_roots: Option<&[P]>,
80 workspace: &SerializedWorkspace,
81 ) {
82 let workspace_id: WorkspaceId = worktree_roots.into();
83
84 self.with_savepoint("update_worktrees", || {
85 if let Some(old_roots) = old_roots {
86 let old_id: WorkspaceId = old_roots.into();
87
88 self.prepare("DELETE FROM WORKSPACES WHERE workspace_id = ?")?
89 .with_bindings(&old_id)?
90 .exec()?;
91 }
92
93 // Delete any previous workspaces with the same roots. This cascades to all
94 // other tables that are based on the same roots set.
95 // Insert new workspace into workspaces table if none were found
96 self.prepare("DELETE FROM workspaces WHERE workspace_id = ?;")?
97 .with_bindings(&workspace_id)?
98 .exec()?;
99
100 self.prepare(
101 "INSERT INTO workspaces(workspace_id, dock_anchor, dock_visible) VALUES (?, ?, ?)",
102 )?
103 .with_bindings((&workspace_id, workspace.dock_anchor, workspace.dock_visible))?
104 .exec()?;
105
106 // Save center pane group and dock pane
107 self.save_pane_group(&workspace_id, &workspace.center_group, None)?;
108 self.save_pane(&workspace_id, &workspace.dock_pane, None)?;
109
110 Ok(())
111 })
112 .with_context(|| {
113 format!(
114 "Update workspace with roots {:?}",
115 worktree_roots
116 .iter()
117 .map(|p| p.as_ref())
118 .collect::<Vec<_>>()
119 )
120 })
121 .log_err();
122 }
123
124 /// Returns the previous workspace ids sorted by last modified along with their opened worktree roots
125 pub fn recent_workspaces(&self, limit: usize) -> Vec<Vec<PathBuf>> {
126 iife!({
127 // TODO, upgrade anyhow: https://docs.rs/anyhow/1.0.66/anyhow/fn.Ok.html
128 Ok::<_, anyhow::Error>(
129 self.prepare(
130 "SELECT workspace_id FROM workspaces ORDER BY timestamp DESC LIMIT ?",
131 )?
132 .with_bindings(limit)?
133 .rows::<WorkspaceId>()?
134 .into_iter()
135 .map(|id| id.paths())
136 .collect::<Vec<Vec<PathBuf>>>(),
137 )
138 })
139 .log_err()
140 .unwrap_or_default()
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use crate::{
147 model::{
148 DockAnchor::{Bottom, Expanded, Right},
149 SerializedWorkspace,
150 },
151 Db,
152 };
153
154 #[test]
155 fn test_workspace_assignment() {
156 env_logger::try_init().ok();
157
158 let db = Db::open_in_memory("test_basic_functionality");
159
160 let workspace_1 = SerializedWorkspace {
161 dock_anchor: Bottom,
162 dock_visible: true,
163 center_group: Default::default(),
164 dock_pane: Default::default(),
165 };
166
167 let workspace_2 = SerializedWorkspace {
168 dock_anchor: Expanded,
169 dock_visible: false,
170 center_group: Default::default(),
171 dock_pane: Default::default(),
172 };
173
174 let workspace_3 = SerializedWorkspace {
175 dock_anchor: Right,
176 dock_visible: true,
177 center_group: Default::default(),
178 dock_pane: Default::default(),
179 };
180
181 db.save_workspace(&["/tmp", "/tmp2"], None, &workspace_1);
182 db.save_workspace(&["/tmp"], None, &workspace_2);
183
184 db.write_to("test.db").unwrap();
185
186 // Test that paths are treated as a set
187 assert_eq!(
188 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
189 workspace_1
190 );
191 assert_eq!(
192 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
193 workspace_1
194 );
195
196 // Make sure that other keys work
197 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
198 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
199
200 // Test 'mutate' case of updating a pre-existing id
201 db.save_workspace(&["/tmp", "/tmp2"], Some(&["/tmp", "/tmp2"]), &workspace_2);
202 assert_eq!(
203 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
204 workspace_2
205 );
206
207 // Test other mechanism for mutating
208 db.save_workspace(&["/tmp", "/tmp2"], None, &workspace_3);
209 assert_eq!(
210 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
211 workspace_3
212 );
213
214 // Make sure that updating paths differently also works
215 db.save_workspace(
216 &["/tmp3", "/tmp4", "/tmp2"],
217 Some(&["/tmp", "/tmp2"]),
218 &workspace_3,
219 );
220 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
221 assert_eq!(
222 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
223 .unwrap(),
224 workspace_3
225 );
226 }
227}