1use std::str::FromStr;
2
3use gpui::Axis;
4use indoc::indoc;
5use sqlez::{
6 bindable::{Bind, Column},
7 migrations::Migration,
8 statement::Statement,
9};
10use util::{iife, ResultExt};
11
12use crate::{items::ItemId, workspace::WorkspaceId};
13
14use super::Db;
15
16pub(crate) const PANE_MIGRATIONS: Migration = Migration::new(
17 "pane",
18 &[indoc! {"
19CREATE TABLE dock_panes(
20 dock_pane_id INTEGER PRIMARY KEY,
21 workspace_id INTEGER NOT NULL,
22 anchor_position TEXT NOT NULL, -- Enum: 'Bottom' / 'Right' / 'Expanded'
23 visible INTEGER NOT NULL, -- Boolean
24 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
25) STRICT;
26
27CREATE TABLE pane_groups( -- Inner nodes
28 group_id INTEGER PRIMARY KEY,
29 workspace_id INTEGER NOT NULL,
30 parent_group INTEGER, -- NULL indicates that this is a root node
31 axis TEXT NOT NULL, -- Enum: 'Vertical' / 'Horizontal'
32 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
33 FOREIGN KEY(parent_group) REFERENCES pane_groups(group_id) ON DELETE CASCADE
34) STRICT;
35
36
37CREATE TABLE grouped_panes( -- Leaf nodes
38 pane_id INTEGER PRIMARY KEY,
39 workspace_id INTEGER NOT NULL,
40 group_id INTEGER NOT NULL,
41 idx INTEGER NOT NULL,
42 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
43 FOREIGN KEY(group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
44) STRICT;
45
46CREATE TABLE items(
47 item_id INTEGER PRIMARY KEY,
48 workspace_id INTEGER NOT NULL,
49 kind TEXT NOT NULL,
50 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
51) STRICT;
52
53CREATE TABLE group_items(
54 workspace_id INTEGER NOT NULL,
55 pane_id INTEGER NOT NULL,
56 item_id INTEGER NOT NULL,
57 idx INTEGER NOT NULL,
58 PRIMARY KEY (workspace_id, pane_id, item_id)
59 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
60 FOREIGN KEY(pane_id) REFERENCES grouped_panes(pane_id) ON DELETE CASCADE,
61 FOREIGN KEY(item_id) REFERENCES items(item_id) ON DELETE CASCADE
62) STRICT;
63
64CREATE TABLE dock_items(
65 workspace_id INTEGER NOT NULL,
66 dock_pane_id INTEGER NOT NULL,
67 item_id INTEGER NOT NULL,
68 idx INTEGER NOT NULL,
69 PRIMARY KEY (workspace_id, dock_pane_id, item_id)
70 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
71 FOREIGN KEY(dock_pane_id) REFERENCES dock_panes(dock_pane_id) ON DELETE CASCADE,
72 FOREIGN KEY(item_id) REFERENCES items(item_id)ON DELETE CASCADE
73) STRICT;
74"}],
75);
76
77// We have an many-branched, unbalanced tree with three types:
78// Pane Groups
79// Panes
80// Items
81
82// The root is always a Pane Group
83// Pane Groups can have 0 (or more) Panes and/or Pane Groups as children
84// Panes can have 0 or more items as children
85// Panes can be their own root
86// Items cannot have children
87// References pointing down is hard (SQL doesn't like arrays)
88// References pointing up is easy (1-1 item / parent relationship) but is harder to query
89//
90
91#[derive(Debug, PartialEq, Eq, Copy, Clone)]
92pub struct PaneId {
93 workspace_id: WorkspaceId,
94 pane_id: usize,
95}
96
97#[derive(Debug, PartialEq, Eq, Copy, Clone)]
98pub struct PaneGroupId {
99 workspace_id: WorkspaceId,
100 group_id: usize,
101}
102
103impl PaneGroupId {
104 pub fn root(workspace_id: WorkspaceId) -> Self {
105 Self {
106 workspace_id,
107 group_id: 0,
108 }
109 }
110}
111
112#[derive(Debug, PartialEq, Eq)]
113pub struct SerializedPaneGroup {
114 group_id: PaneGroupId,
115 axis: Axis,
116 children: Vec<PaneGroupChild>,
117}
118
119impl SerializedPaneGroup {
120 pub fn empty_root(workspace_id: WorkspaceId) -> Self {
121 Self {
122 group_id: PaneGroupId::root(workspace_id),
123 axis: Default::default(),
124 children: Default::default(),
125 }
126 }
127}
128
129struct PaneGroupChildRow {
130 child_pane_id: Option<usize>,
131 child_group_id: Option<usize>,
132 index: usize,
133}
134
135#[derive(Debug, PartialEq, Eq)]
136pub enum PaneGroupChild {
137 Pane(SerializedPane),
138 Group(SerializedPaneGroup),
139}
140
141#[derive(Debug, PartialEq, Eq)]
142pub struct SerializedPane {
143 pane_id: PaneId,
144 children: Vec<ItemId>,
145}
146
147//********* CURRENTLY IN USE TYPES: *********
148
149#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
150pub enum DockAnchor {
151 #[default]
152 Bottom,
153 Right,
154 Expanded,
155}
156
157impl ToString for DockAnchor {
158 fn to_string(&self) -> String {
159 match self {
160 DockAnchor::Bottom => "Bottom".to_string(),
161 DockAnchor::Right => "Right".to_string(),
162 DockAnchor::Expanded => "Expanded".to_string(),
163 }
164 }
165}
166
167impl FromStr for DockAnchor {
168 type Err = anyhow::Error;
169
170 fn from_str(s: &str) -> anyhow::Result<Self> {
171 match s {
172 "Bottom" => Ok(DockAnchor::Bottom),
173 "Right" => Ok(DockAnchor::Right),
174 "Expanded" => Ok(DockAnchor::Expanded),
175 _ => anyhow::bail!("Not a valid dock anchor"),
176 }
177 }
178}
179
180impl Bind for DockAnchor {
181 fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
182 statement.bind(self.to_string(), start_index)
183 }
184}
185
186impl Column for DockAnchor {
187 fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
188 <String as Column>::column(statement, start_index)
189 .and_then(|(str, next_index)| Ok((DockAnchor::from_str(&str)?, next_index)))
190 }
191}
192
193#[derive(Default, Debug, PartialEq, Eq)]
194pub struct SerializedDockPane {
195 pub anchor_position: DockAnchor,
196 pub visible: bool,
197}
198
199impl SerializedDockPane {
200 fn to_row(&self, workspace: &WorkspaceId) -> DockRow {
201 DockRow {
202 workspace_id: *workspace,
203 anchor_position: self.anchor_position,
204 visible: self.visible,
205 }
206 }
207}
208
209impl Column for SerializedDockPane {
210 fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
211 <(DockAnchor, bool) as Column>::column(statement, start_index).map(
212 |((anchor_position, visible), next_index)| {
213 (
214 SerializedDockPane {
215 anchor_position,
216 visible,
217 },
218 next_index,
219 )
220 },
221 )
222 }
223}
224
225#[derive(Default, Debug, PartialEq, Eq)]
226pub(crate) struct DockRow {
227 workspace_id: WorkspaceId,
228 anchor_position: DockAnchor,
229 visible: bool,
230}
231
232impl Bind for DockRow {
233 fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
234 statement.bind(
235 (
236 self.workspace_id,
237 self.anchor_position.to_string(),
238 self.visible,
239 ),
240 start_index,
241 )
242 }
243}
244
245impl Db {
246 pub fn get_pane_group(&self, pane_group_id: PaneGroupId) -> SerializedPaneGroup {
247 let axis = self.get_pane_group_axis(pane_group_id);
248 let mut children: Vec<(usize, PaneGroupChild)> = Vec::new();
249 for child_row in self.get_pane_group_children(pane_group_id) {
250 if let Some(child_pane_id) = child_row.child_pane_id {
251 children.push((
252 child_row.index,
253 PaneGroupChild::Pane(self.get_pane(PaneId {
254 workspace_id: pane_group_id.workspace_id,
255 pane_id: child_pane_id,
256 })),
257 ));
258 } else if let Some(child_group_id) = child_row.child_group_id {
259 children.push((
260 child_row.index,
261 PaneGroupChild::Group(self.get_pane_group(PaneGroupId {
262 workspace_id: pane_group_id.workspace_id,
263 group_id: child_group_id,
264 })),
265 ));
266 }
267 }
268 children.sort_by_key(|(index, _)| *index);
269
270 SerializedPaneGroup {
271 group_id: pane_group_id,
272 axis,
273 children: children.into_iter().map(|(_, child)| child).collect(),
274 }
275 }
276
277 fn get_pane_group_children(
278 &self,
279 _pane_group_id: PaneGroupId,
280 ) -> impl Iterator<Item = PaneGroupChildRow> {
281 Vec::new().into_iter()
282 }
283
284 fn get_pane_group_axis(&self, _pane_group_id: PaneGroupId) -> Axis {
285 unimplemented!();
286 }
287
288 pub fn save_pane_splits(&self, _center_pane_group: SerializedPaneGroup) {
289 // Delete the center pane group for this workspace and any of its children
290 // Generate new pane group IDs as we go through
291 // insert them
292 // Items garbage collect themselves when dropped
293 }
294
295 pub(crate) fn get_pane(&self, _pane_id: PaneId) -> SerializedPane {
296 unimplemented!();
297 }
298
299 pub fn get_dock_pane(&self, workspace: WorkspaceId) -> Option<SerializedDockPane> {
300 iife!({
301 self.prepare("SELECT anchor_position, visible FROM dock_panes WHERE workspace_id = ?")?
302 .with_bindings(workspace)?
303 .maybe_row::<SerializedDockPane>()
304 })
305 .log_err()
306 .flatten()
307 }
308
309 pub fn save_dock_pane(&self, workspace: &WorkspaceId, dock_pane: &SerializedDockPane) {
310 iife!({
311 self.prepare(
312 "INSERT INTO dock_panes (workspace_id, anchor_position, visible) VALUES (?, ?, ?);",
313 )?
314 .with_bindings(dock_pane.to_row(workspace))?
315 .insert()
316 })
317 .log_err();
318 }
319}
320
321#[cfg(test)]
322mod tests {
323
324 use crate::{pane::SerializedPane, Db};
325
326 use super::{DockAnchor, SerializedDockPane};
327
328 #[test]
329 fn test_basic_dock_pane() {
330 let db = Db::open_in_memory("basic_dock_pane");
331
332 let workspace = db.workspace_for_roots(&["/tmp"]);
333
334 let dock_pane = SerializedDockPane {
335 anchor_position: DockAnchor::Expanded,
336 visible: true,
337 };
338
339 db.save_dock_pane(&workspace.workspace_id, &dock_pane);
340
341 let new_workspace = db.workspace_for_roots(&["/tmp"]);
342
343 assert_eq!(new_workspace.dock_pane.unwrap(), dock_pane);
344 }
345
346 #[test]
347 fn test_dock_simple_split() {
348 let db = Db::open_in_memory("simple_split");
349
350 let workspace = db.workspace_for_roots(&["/tmp"]);
351
352 let center_pane = SerializedPane {
353 pane_id: crate::pane::PaneId {
354 workspace_id: workspace.workspace_id,
355 pane_id: 1,
356 },
357 children: vec![],
358 };
359
360 db.save_dock_pane(&workspace.workspace_id, &dock_pane);
361
362 let new_workspace = db.workspace_for_roots(&["/tmp"]);
363
364 assert_eq!(new_workspace.dock_pane.unwrap(), dock_pane);
365 }
366}