Skip to main content

markdown_editor/
document.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use super::{
5    id::BlockId,
6    inline::InlineText,
7    kind::{BlockKind, BlockKindData},
8    order::OrderIndex,
9    position::BlockPosition,
10    step::DetachedBlock,
11};
12use gpui_kit::base::text::{MarkdownExtensions, MarkdownNode};
13
14/// Payload of an atomic block.
15#[derive(Clone, Debug, PartialEq)]
16pub(crate) enum AtomData {
17    None,
18    Custom(MarkdownNode),
19}
20
21#[derive(Clone, Debug, PartialEq)]
22pub(crate) enum BlockContent {
23    Children(Vec<BlockId>),
24    /// Paragraph, Heading, TableCell.
25    Inline(InlineText),
26    /// CodeBlock (its language lives in [`BlockKindData`]).
27    Plain(String),
28    /// Custom, Definition, Break, HorizontalRule, Unknown.
29    Atom(AtomData),
30}
31
32#[derive(Clone, Debug, PartialEq)]
33pub(crate) struct BlockEntry {
34    pub(crate) parent: Option<BlockId>,
35    pub(crate) kind: BlockKindData,
36    pub(crate) content: BlockContent,
37}
38
39/// The editing model: block tree with stable ids (design §4.2, D4).
40pub(crate) struct EditorDocument {
41    pub(crate) revision: u64,
42    pub(crate) root: BlockId,
43    pub(crate) blocks: HashMap<BlockId, BlockEntry>,
44    pub(crate) order: OrderIndex,
45    /// Set by structural steps; the order index is rebuilt at commit.
46    pub(crate) structure_dirty: bool,
47    pub(crate) extensions: Arc<MarkdownExtensions>,
48    /// Per-block content revision, bumped for a block and all its ancestors
49    /// by every step that touches it; absent means never edited. The view
50    /// re-projects a top-level block only when its revision changes.
51    pub(crate) block_revisions: HashMap<BlockId, u64>,
52    edit_counter: u64,
53}
54
55impl EditorDocument {
56    /// An empty document: a root holding one empty paragraph.
57    pub(crate) fn empty(extensions: Arc<MarkdownExtensions>) -> Self {
58        let root = BlockId::next();
59        let paragraph = BlockId::next();
60        let mut blocks = HashMap::new();
61        blocks.insert(
62            root,
63            BlockEntry {
64                parent: None,
65                kind: BlockKindData::Root,
66                content: BlockContent::Children(vec![paragraph]),
67            },
68        );
69        blocks.insert(
70            paragraph,
71            BlockEntry {
72                parent: Some(root),
73                kind: BlockKindData::Paragraph,
74                content: BlockContent::Inline(InlineText::new()),
75            },
76        );
77        let mut doc = Self {
78            revision: 0,
79            root,
80            blocks,
81            order: OrderIndex::default(),
82            structure_dirty: true,
83            extensions,
84            block_revisions: HashMap::new(),
85            edit_counter: 0,
86        };
87        doc.refresh_order();
88        doc
89    }
90
91    /// Bump the content revision of `id` and its ancestors.
92    pub(crate) fn touch(&mut self, id: BlockId) {
93        self.edit_counter += 1;
94        let mut next = Some(id);
95        while let Some(id) = next {
96            self.block_revisions.insert(id, self.edit_counter);
97            next = self.parent(id);
98        }
99    }
100
101    pub(crate) fn block_revision(&self, id: BlockId) -> u64 {
102        self.block_revisions.get(&id).copied().unwrap_or(0)
103    }
104
105    /// Number of blocks, including the root.
106    pub(crate) fn len(&self) -> usize {
107        self.blocks.len()
108    }
109
110    pub(crate) fn get(&self, id: BlockId) -> Option<&BlockEntry> {
111        self.blocks.get(&id)
112    }
113
114    pub(crate) fn get_mut(&mut self, id: BlockId) -> Option<&mut BlockEntry> {
115        self.blocks.get_mut(&id)
116    }
117
118    pub(crate) fn children(&self, id: BlockId) -> &[BlockId] {
119        match self.blocks.get(&id).map(|entry| &entry.content) {
120            Some(BlockContent::Children(children)) => children,
121            _ => &[],
122        }
123    }
124
125    pub(crate) fn parent(&self, id: BlockId) -> Option<BlockId> {
126        self.blocks.get(&id)?.parent
127    }
128
129    pub(crate) fn index_in_parent(&self, id: BlockId) -> Option<(BlockId, usize)> {
130        let parent = self.parent(id)?;
131        let index = self.children(parent).iter().position(|c| *c == id)?;
132        Some((parent, index))
133    }
134
135    pub(crate) fn kind(&self, id: BlockId) -> Option<&BlockKindData> {
136        self.blocks.get(&id).map(|entry| &entry.kind)
137    }
138
139    pub(crate) fn inline(&self, id: BlockId) -> Option<&InlineText> {
140        match &self.blocks.get(&id)?.content {
141            BlockContent::Inline(inline) => Some(inline),
142            _ => None,
143        }
144    }
145
146    /// Whether the block can hold a caret.
147    pub(crate) fn is_caret_leaf(&self, id: BlockId) -> bool {
148        self.blocks
149            .get(&id)
150            .is_some_and(|entry| !matches!(entry.content, BlockContent::Children(_)))
151    }
152
153    /// Length of a caret leaf in caret units: bytes of text, or 1 for an atom
154    /// or code block (0 = before, 1 = after) in the spike.
155    pub(crate) fn leaf_len(&self, id: BlockId) -> Option<usize> {
156        match &self.blocks.get(&id)?.content {
157            BlockContent::Inline(inline) => Some(inline.len()),
158            BlockContent::Plain(code) => Some(code.len()),
159            BlockContent::Atom(_) => Some(1),
160            BlockContent::Children(_) => None,
161        }
162    }
163
164    /// A code block's text.
165    pub(crate) fn plain(&self, id: BlockId) -> Option<&str> {
166        match &self.blocks.get(&id)?.content {
167            BlockContent::Plain(code) => Some(code),
168            _ => None,
169        }
170    }
171
172    /// Caret-leaf text: inline text (atoms as U+FFFC) or code.
173    pub(crate) fn leaf_text(&self, id: BlockId) -> Option<&str> {
174        match &self.blocks.get(&id)?.content {
175            BlockContent::Inline(inline) => Some(inline.text()),
176            BlockContent::Plain(code) => Some(code),
177            _ => None,
178        }
179    }
180
181    /// Path from the root down to `id`, inclusive.
182    pub(crate) fn path(&self, id: BlockId) -> Vec<BlockId> {
183        let mut path = vec![id];
184        let mut current = id;
185        while let Some(parent) = self.parent(current) {
186            path.push(parent);
187            current = parent;
188        }
189        path.reverse();
190        path
191    }
192
193    /// Caret leaves in document order, by walking the tree. Used inside
194    /// transactions, where the order index may be stale.
195    pub(crate) fn walk_leaves(&self) -> Vec<BlockId> {
196        let mut out = Vec::new();
197        let mut stack = vec![self.root];
198        while let Some(id) = stack.pop() {
199            match self.blocks.get(&id).map(|entry| &entry.content) {
200                Some(BlockContent::Children(children)) => stack.extend(children.iter().rev()),
201                Some(_) => out.push(id),
202                None => {}
203            }
204        }
205        out
206    }
207
208    pub(crate) fn refresh_order(&mut self) {
209        if self.structure_dirty {
210            self.order = OrderIndex::build(self);
211            self.structure_dirty = false;
212        }
213    }
214
215    /// Leaves in document order from the (fresh) order index.
216    pub(crate) fn leaves(&self) -> &[BlockId] {
217        debug_assert!(!self.structure_dirty, "order index read while stale");
218        self.order.leaves()
219    }
220
221    pub(crate) fn first_caret(&self) -> BlockPosition {
222        let leaf = self.walk_leaves().first().copied().unwrap_or(self.root);
223        BlockPosition::new(leaf, 0)
224    }
225
226    pub(crate) fn last_caret(&self) -> BlockPosition {
227        let leaf = self.walk_leaves().last().copied().unwrap_or(self.root);
228        BlockPosition::new(leaf, self.leaf_len(leaf).unwrap_or(0))
229    }
230
231    /// Insert a detached subtree (keeping its ids) under `parent` at `index`.
232    pub(crate) fn attach(&mut self, parent: BlockId, index: usize, block: DetachedBlock) -> bool {
233        let Some(BlockEntry {
234            content: BlockContent::Children(children),
235            ..
236        }) = self.blocks.get_mut(&parent)
237        else {
238            return false;
239        };
240        if index > children.len() {
241            return false;
242        }
243        children.insert(index, block.id);
244        self.attach_entry(Some(parent), block);
245        self.structure_dirty = true;
246        true
247    }
248
249    fn attach_entry(&mut self, parent: Option<BlockId>, block: DetachedBlock) {
250        let content = match block.content {
251            super::step::DetachedContent::Children(children) => {
252                let ids = children.iter().map(|child| child.id).collect();
253                for child in children {
254                    self.attach_entry(Some(block.id), child);
255                }
256                BlockContent::Children(ids)
257            }
258            super::step::DetachedContent::Leaf(content) => content,
259        };
260        self.blocks.insert(
261            block.id,
262            BlockEntry {
263                parent,
264                kind: block.kind,
265                content,
266            },
267        );
268    }
269
270    /// Remove the child at `index` of `parent`, with its subtree.
271    pub(crate) fn detach(&mut self, parent: BlockId, index: usize) -> Option<DetachedBlock> {
272        let Some(BlockEntry {
273            content: BlockContent::Children(children),
274            ..
275        }) = self.blocks.get_mut(&parent)
276        else {
277            return None;
278        };
279        if index >= children.len() {
280            return None;
281        }
282        let id = children.remove(index);
283        self.structure_dirty = true;
284        self.detach_entry(id)
285    }
286
287    fn detach_entry(&mut self, id: BlockId) -> Option<DetachedBlock> {
288        let entry = self.blocks.remove(&id)?;
289        let content = match entry.content {
290            BlockContent::Children(children) => super::step::DetachedContent::Children(
291                children
292                    .into_iter()
293                    .filter_map(|child| self.detach_entry(child))
294                    .collect(),
295            ),
296            other => super::step::DetachedContent::Leaf(other),
297        };
298        Some(DetachedBlock {
299            id,
300            kind: entry.kind,
301            content,
302        })
303    }
304
305    /// Structural invariants (design §5.1) plus N1–N3 for every inline leaf.
306    pub(crate) fn check_invariants(&self) -> Result<(), String> {
307        let mut seen = 0;
308        let mut stack = vec![(self.root, None)];
309        while let Some((id, parent)) = stack.pop() {
310            seen += 1;
311            let entry = self.blocks.get(&id).ok_or(format!("missing {id:?}"))?;
312            if entry.parent != parent {
313                return Err(format!("bad parent link on {id:?}"));
314            }
315            match &entry.content {
316                BlockContent::Children(children) => {
317                    if !entry.kind.is_container() {
318                        return Err(format!("{:?} has children", entry.kind));
319                    }
320                    if children.is_empty() && entry.kind != BlockKindData::Root {
321                        return Err(format!("empty container {:?}", entry.kind));
322                    }
323                    if let BlockKindData::List { .. } = entry.kind
324                        && children
325                            .iter()
326                            .any(|c| !matches!(self.kind(*c), Some(BlockKindData::ListItem { .. })))
327                    {
328                        return Err("list holds a non-item".into());
329                    }
330                    if let BlockKindData::ListItem { .. } = entry.kind
331                        && !children.first().is_some_and(|c| self.is_caret_leaf(*c))
332                    {
333                        return Err("list item does not start with a leaf".into());
334                    }
335                    stack.extend(children.iter().map(|c| (*c, Some(id))));
336                }
337                BlockContent::Inline(inline) if !inline.is_normalized() => {
338                    return Err(format!("inline text of {id:?} not normalized"));
339                }
340                _ => {}
341            }
342        }
343        if seen != self.blocks.len() {
344            return Err("orphaned entries".into());
345        }
346        if self.walk_leaves().is_empty() {
347            return Err("no caret leaf".into());
348        }
349        Ok(())
350    }
351}
352
353/// Borrowed read projection of the document, like the dock's `PaneRef`.
354#[derive(Clone, Copy)]
355pub struct DocumentRef<'a> {
356    pub(crate) doc: &'a EditorDocument,
357}
358
359impl<'a> DocumentRef<'a> {
360    pub fn root(&self) -> BlockRef<'a> {
361        BlockRef {
362            doc: self.doc,
363            id: self.doc.root,
364        }
365    }
366
367    pub fn block(&self, id: BlockId) -> Option<BlockRef<'a>> {
368        self.doc.get(id).map(|_| BlockRef { doc: self.doc, id })
369    }
370
371    pub fn first_caret(&self) -> BlockPosition {
372        self.doc.first_caret()
373    }
374
375    pub fn last_caret(&self) -> BlockPosition {
376        self.doc.last_caret()
377    }
378
379    /// Caret-bearing leaves in document order (O(1) per step, from the order
380    /// index).
381    pub fn leaves(&self) -> impl Iterator<Item = BlockRef<'a>> + 'a {
382        let doc = self.doc;
383        doc.leaves().iter().map(move |id| BlockRef { doc, id: *id })
384    }
385}
386
387/// Borrowed read projection of one block.
388#[derive(Clone, Copy)]
389pub struct BlockRef<'a> {
390    doc: &'a EditorDocument,
391    id: BlockId,
392}
393
394impl<'a> BlockRef<'a> {
395    pub fn id(&self) -> BlockId {
396        self.id
397    }
398
399    pub fn kind(&self) -> BlockKind {
400        self.doc
401            .kind(self.id)
402            .map(BlockKindData::public)
403            .unwrap_or(BlockKind::Unknown)
404    }
405
406    /// The flat text of a text-bearing leaf (inline atoms show as U+FFFC).
407    pub fn text(&self) -> Option<&'a str> {
408        match &self.doc.get(self.id)?.content {
409            BlockContent::Inline(inline) => Some(inline.text()),
410            BlockContent::Plain(text) => Some(text),
411            _ => None,
412        }
413    }
414
415    pub fn parent(&self) -> Option<BlockRef<'a>> {
416        self.doc
417            .parent(self.id)
418            .map(|id| BlockRef { doc: self.doc, id })
419    }
420
421    pub fn children(&self) -> impl Iterator<Item = BlockRef<'a>> + 'a {
422        let doc = self.doc;
423        doc.children(self.id)
424            .iter()
425            .map(move |id| BlockRef { doc, id: *id })
426    }
427}