pane.rs

  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}