Skip to main content

common/database/
rope_store.rs

1//! Ropey-backed storage backend.
2//!
3//! Holds character data in a single document-wide `ropey::Rope`,
4//! with structural entities (Frames, Tables, Lists, Resources) in
5//! `im::HashMap` tables and per-block character formatting in
6//! `format_runs`. See the migration plan §1.5 for the relationship
7//! inlining and §1.6 for the rope layout (block boundary `\n` +
8//! U+FFFC table anchor).
9
10use crate::database::block_offset_index::BlockOffsetIndex;
11use crate::entities::*;
12use crate::format_runs::{FootnoteRefAnchor, FormatRun, ImageAnchor};
13use crate::snapshot::{StoreSnapshot, StoreSnapshotTrait};
14use crate::types::EntityId;
15use im::HashMap;
16use parking_lot::RwLock;
17use ropey::Rope;
18use std::collections::HashMap as StdHashMap;
19
20// ─────────────────────────────────────────────────────────────────────────────
21// The Store
22// ─────────────────────────────────────────────────────────────────────────────
23
24#[derive(Debug, Default)]
25pub struct RopeStore {
26    // ── Character content (shared across all blocks, including cells) ──
27    pub rope: RwLock<Rope>,
28
29    // ── Structural entity tables ──────────────────────────────────────
30    pub roots: RwLock<HashMap<EntityId, Root>>,
31    pub documents: RwLock<HashMap<EntityId, Document>>,
32    pub frames: RwLock<HashMap<EntityId, Frame>>,
33    pub blocks: RwLock<HashMap<EntityId, Block>>,
34    pub lists: RwLock<HashMap<EntityId, List>>,
35    pub resources: RwLock<HashMap<EntityId, Resource>>,
36    pub tables: RwLock<HashMap<EntityId, Table>>,
37    pub table_cells: RwLock<HashMap<EntityId, TableCell>>,
38
39    // ── Per-block character formatting + image anchors ────────────────
40    pub format_runs: RwLock<HashMap<EntityId, Vec<FormatRun>>>,
41    pub block_images: RwLock<HashMap<EntityId, Vec<ImageAnchor>>>,
42    /// Footnote references anchored in each block, byte-ordered.
43    ///
44    /// Beside `block_images` rather than merged with it: the two are written by
45    /// different editing paths and read together only through
46    /// `format_runs::block_anchors`, which is the one place their interleaving is
47    /// decided.
48    pub block_footnote_refs: RwLock<HashMap<EntityId, Vec<FootnoteRefAnchor>>>,
49    /// Host-supplied markers by footnote label — what a reference *prints*.
50    ///
51    /// Ambient and presentation-only, like a syntax highlighter: never serialised,
52    /// never part of the document, and consulted when a reference is rendered.
53    ///
54    /// It exists because the number is a fact about the **host's** manuscript, not
55    /// about this document. Skribisto compiles one chapter into a document of its
56    /// own, and that document's reading order would number the chapter's notes from
57    /// one — disagreeing with the badge the writer sees in the editor, for the same
58    /// note, at the same moment. Empty means "number them yourself", which is the
59    /// right answer for a document that *is* the whole text.
60    pub footnote_markers: RwLock<StdHashMap<String, String>>,
61
62    // ── Document-wide block ordering (sorted by rope position) ────────
63    pub block_offsets: RwLock<BlockOffsetIndex>,
64
65    // ── ID counters ───────────────────────────────────────────────────
66    // Never restored by undo (only by transaction rollback).
67    pub counters: RwLock<StdHashMap<String, EntityId>>,
68
69    // ── Savepoints (in-memory, transaction-scoped) ────────────────────
70    savepoints: RwLock<StdHashMap<u64, RopeStoreSnapshot>>,
71    next_savepoint_id: RwLock<u64>,
72}
73
74impl RopeStore {
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    /// O(1) snapshot of the entire store (rope is Arc-shared, all
80    /// `im::HashMap`s are HAMT-shared; `BlockOffsetIndex` is a small
81    /// `Vec` cloned outright).
82    pub fn snapshot(&self) -> RopeStoreSnapshot {
83        RopeStoreSnapshot {
84            rope: self.rope.read().clone(),
85            roots: self.roots.read().clone(),
86            documents: self.documents.read().clone(),
87            frames: self.frames.read().clone(),
88            blocks: self.blocks.read().clone(),
89            lists: self.lists.read().clone(),
90            resources: self.resources.read().clone(),
91            tables: self.tables.read().clone(),
92            table_cells: self.table_cells.read().clone(),
93            format_runs: self.format_runs.read().clone(),
94            block_images: self.block_images.read().clone(),
95            block_footnote_refs: self.block_footnote_refs.read().clone(),
96            footnote_markers: self.footnote_markers.read().clone(),
97            block_offsets: self.block_offsets.read().clone(),
98            counters: self.counters.read().clone(),
99        }
100    }
101
102    /// Restore from a snapshot. Overwrites counters too — used for
103    /// transaction rollback (`Drop` of an uncommitted write txn).
104    pub fn restore(&self, snap: &RopeStoreSnapshot) {
105        *self.rope.write() = snap.rope.clone();
106        *self.roots.write() = snap.roots.clone();
107        *self.documents.write() = snap.documents.clone();
108        *self.frames.write() = snap.frames.clone();
109        *self.blocks.write() = snap.blocks.clone();
110        *self.lists.write() = snap.lists.clone();
111        *self.resources.write() = snap.resources.clone();
112        *self.tables.write() = snap.tables.clone();
113        *self.table_cells.write() = snap.table_cells.clone();
114        *self.format_runs.write() = snap.format_runs.clone();
115        *self.block_images.write() = snap.block_images.clone();
116        *self.block_footnote_refs.write() = snap.block_footnote_refs.clone();
117        *self.footnote_markers.write() = snap.footnote_markers.clone();
118        *self.block_offsets.write() = snap.block_offsets.clone();
119        *self.counters.write() = snap.counters.clone();
120    }
121
122    /// Restore everything *except* counters — used for undo, where IDs
123    /// must remain monotonically increasing across undo/redo cycles.
124    pub fn restore_without_counters(&self, snap: &RopeStoreSnapshot) {
125        *self.rope.write() = snap.rope.clone();
126        *self.roots.write() = snap.roots.clone();
127        *self.documents.write() = snap.documents.clone();
128        *self.frames.write() = snap.frames.clone();
129        *self.blocks.write() = snap.blocks.clone();
130        *self.lists.write() = snap.lists.clone();
131        *self.resources.write() = snap.resources.clone();
132        *self.tables.write() = snap.tables.clone();
133        *self.table_cells.write() = snap.table_cells.clone();
134        *self.format_runs.write() = snap.format_runs.clone();
135        *self.block_images.write() = snap.block_images.clone();
136        *self.block_footnote_refs.write() = snap.block_footnote_refs.clone();
137        *self.footnote_markers.write() = snap.footnote_markers.clone();
138        *self.block_offsets.write() = snap.block_offsets.clone();
139        // counters intentionally not restored
140    }
141
142    pub fn create_savepoint(&self) -> u64 {
143        let snap = self.snapshot();
144        let mut id_counter = self.next_savepoint_id.write();
145        let id = *id_counter;
146        *id_counter += 1;
147        self.savepoints.write().insert(id, snap);
148        id
149    }
150
151    pub fn restore_savepoint(&self, savepoint_id: u64) {
152        let snap = self
153            .savepoints
154            .read()
155            .get(&savepoint_id)
156            .expect("savepoint not found")
157            .clone();
158        self.restore(&snap);
159    }
160
161    pub fn discard_savepoint(&self, savepoint_id: u64) {
162        self.savepoints.write().remove(&savepoint_id);
163    }
164
165    /// Get-and-increment counter for an entity type.
166    pub(crate) fn next_id(&self, entity_name: &str) -> EntityId {
167        let mut counters = self.counters.write();
168        let counter = counters.entry(entity_name.to_string()).or_insert(1);
169        let id = *counter;
170        *counter += 1;
171        id
172    }
173
174    /// Type-erased store snapshot (for the generic undo path).
175    pub fn store_snapshot(&self) -> StoreSnapshot {
176        StoreSnapshot::new(self.snapshot())
177    }
178
179    /// Restore from a type-erased store snapshot (undo semantic —
180    /// counters preserved).
181    pub fn restore_store_snapshot(&self, snap: &StoreSnapshot) {
182        let s = snap
183            .downcast_ref::<RopeStoreSnapshot>()
184            .expect("StoreSnapshot must contain RopeStoreSnapshot");
185        self.restore_without_counters(s);
186    }
187}
188
189// ─────────────────────────────────────────────────────────────────────────────
190// Snapshot
191// ─────────────────────────────────────────────────────────────────────────────
192
193/// O(1)-clone snapshot. `Rope::clone()` shares the Arc-d B+ tree root;
194/// every `im::HashMap::clone()` is HAMT-structural.
195#[derive(Debug, Clone, Default)]
196pub struct RopeStoreSnapshot {
197    pub(crate) rope: Rope,
198    pub(crate) roots: HashMap<EntityId, Root>,
199    pub(crate) documents: HashMap<EntityId, Document>,
200    pub(crate) frames: HashMap<EntityId, Frame>,
201    pub(crate) blocks: HashMap<EntityId, Block>,
202    pub(crate) lists: HashMap<EntityId, List>,
203    pub(crate) resources: HashMap<EntityId, Resource>,
204    pub(crate) tables: HashMap<EntityId, Table>,
205    pub(crate) table_cells: HashMap<EntityId, TableCell>,
206    pub(crate) format_runs: HashMap<EntityId, Vec<FormatRun>>,
207    pub(crate) block_images: HashMap<EntityId, Vec<ImageAnchor>>,
208    pub(crate) block_footnote_refs: HashMap<EntityId, Vec<FootnoteRefAnchor>>,
209    pub(crate) footnote_markers: StdHashMap<String, String>,
210    pub(crate) block_offsets: BlockOffsetIndex,
211    pub(crate) counters: StdHashMap<String, EntityId>,
212}
213
214impl StoreSnapshotTrait for RopeStoreSnapshot {
215    fn clone_box(&self) -> Box<dyn StoreSnapshotTrait> {
216        Box::new(self.clone())
217    }
218
219    fn as_any(&self) -> &dyn std::any::Any {
220        self
221    }
222}