Skip to main content

markdown_editor/
editor.rs

1use std::{
2    cell::RefCell,
3    sync::{
4        Arc,
5        atomic::{AtomicU64, Ordering},
6    },
7};
8
9static NEXT_EDITOR_OWNER: AtomicU64 = AtomicU64::new(1);
10
11use gpui::SharedString;
12
13use super::{
14    command::CommandError,
15    document::{DocumentRef, EditorDocument},
16    import::parse_document,
17    kind::BlockKind,
18    marks::{MarkKind, MarkSet, MarkState},
19    position::BlockPosition,
20    selection::{BlockSelection, SelectionError},
21    transaction::{EditOrigin, Transaction, TransactionBuilder},
22    undo::BlockUndoManager,
23};
24use gpui_kit::base::text::{MarkdownExtensions, editor_host::TextMark};
25
26/// Headless editor facade (spike name). Phase 3 wraps this in the
27/// `BlockEditorState` entity.
28pub struct BlockEditorModel {
29    pub(crate) doc: EditorDocument,
30    pub(super) owner: u64,
31    pub(super) selection_epoch: u64,
32    pub(super) source_cache: RefCell<Option<super::SourceSnapshot>>,
33    pub(super) prefix_cache: RefCell<Option<super::host::PrefixSnapshot>>,
34    pub(crate) selection: BlockSelection,
35    /// Toolbar marks at a collapsed caret; cleared when the caret moves.
36    pub(crate) stored_marks: Option<TextMark>,
37    pub(crate) history: BlockUndoManager,
38}
39
40impl BlockEditorModel {
41    /// Parse Markdown into a model.
42    pub fn from_markdown(
43        source: &str,
44        extensions: MarkdownExtensions,
45    ) -> Result<Self, SharedString> {
46        let extensions = super::import::preserve_source_extensions(extensions);
47        let doc = parse_document(source, Arc::new(extensions))?;
48        let selection = BlockSelection::caret(doc.first_caret());
49        Ok(Self {
50            doc,
51            owner: NEXT_EDITOR_OWNER.fetch_add(1, Ordering::Relaxed),
52            selection_epoch: 0,
53            source_cache: RefCell::default(),
54            prefix_cache: RefCell::default(),
55            selection,
56            stored_marks: None,
57            history: BlockUndoManager::default(),
58        })
59    }
60
61    /// Replace the content. Fresh ids (old positions fail with
62    /// `UnknownBlock`), history cleared.
63    pub fn set_source(&mut self, source: &str) -> Result<(), SharedString> {
64        let revision = self.doc.revision;
65        self.doc = parse_document(source, self.doc.extensions.clone())?;
66        self.doc.revision = revision + 1;
67        self.selection = BlockSelection::caret(self.doc.first_caret());
68        self.stored_marks = None;
69        self.history.clear();
70        Ok(())
71    }
72
73    /// The document as canonical Markdown.
74    pub fn source(&self) -> String {
75        self.source_snapshot().text().to_string()
76    }
77
78    /// Document revision; bumped by every committed transaction.
79    pub fn revision(&self) -> u64 {
80        self.doc.revision
81    }
82
83    /// Read-only view of the block tree.
84    pub fn document(&self) -> DocumentRef<'_> {
85        DocumentRef { doc: &self.doc }
86    }
87
88    /// The current selection.
89    pub fn selection(&self) -> &BlockSelection {
90        &self.selection
91    }
92
93    /// Set the selection after validating it against the document.
94    pub fn set_selection(&mut self, selection: BlockSelection) -> Result<(), SelectionError> {
95        self.doc.validate_selection(&selection)?;
96        if selection != self.selection {
97            self.selection_epoch += 1;
98            self.stored_marks = None;
99            self.history.break_coalescing();
100        }
101        self.selection = selection;
102        Ok(())
103    }
104
105    /// Delete `anchor..head` in one leaf (D21 word / line deletes) without
106    /// an undo boundary, so a run of word deletes coalesces like Backspace.
107    pub(crate) fn delete_span(
108        &mut self,
109        anchor: BlockPosition,
110        head: BlockPosition,
111        forward: bool,
112    ) -> Result<(), super::command::CommandError> {
113        let selection = BlockSelection::Text { anchor, head };
114        self.doc
115            .validate_selection(&selection)
116            .map_err(super::command::CommandError::Selection)?;
117        self.selection_epoch += 1;
118        self.stored_marks = None;
119        self.selection = selection;
120        self.delete_as(forward, true)
121    }
122
123    /// The caret (head of a text selection); `None` for a block selection.
124    pub fn caret(&self) -> Option<BlockPosition> {
125        match &self.selection {
126            BlockSelection::Text { head, .. } => Some(*head),
127            BlockSelection::Blocks { .. } | BlockSelection::Cells { .. } => None,
128        }
129    }
130
131    /// Marks the next typed text gets (stored marks, else inherited at the caret).
132    pub fn active_marks(&self) -> MarkSet {
133        if let Some(stored) = &self.stored_marks {
134            return MarkSet(stored.clone());
135        }
136        let Some(caret) = self.caret() else {
137            return MarkSet::default();
138        };
139        MarkSet(self.inherited_marks(caret))
140    }
141
142    /// Whether `kind` is on, off or mixed over the selection.
143    pub fn mark_state(&self, kind: MarkKind) -> MarkState {
144        let Ok(range) = self.doc.resolve_selection(&self.selection) else {
145            return MarkState::Off;
146        };
147        if range.is_collapsed() {
148            return if self.active_marks().has(kind) {
149                MarkState::On
150            } else {
151                MarkState::Off
152            };
153        }
154        let (mut on, mut off) = (false, false);
155        for (block, lo, hi) in self.text_ranges(range) {
156            if let Some(inline) = self.doc.inline(block) {
157                for (piece, marks, _) in inline.pieces() {
158                    if piece.start < hi && piece.end > lo {
159                        if kind.is_set(marks) {
160                            on = true
161                        } else {
162                            off = true
163                        }
164                    }
165                }
166            }
167        }
168        match (on, off) {
169            (true, false) => MarkState::On,
170            (true, true) => MarkState::Mixed,
171            _ => MarkState::Off,
172        }
173    }
174
175    /// Kind of the text block holding the caret, if any.
176    pub fn block_kind_at_caret(&self) -> Option<BlockKind> {
177        let caret = self.caret()?;
178        self.doc.kind(caret.block).map(|kind| kind.public())
179    }
180
181    /// End the current typing group, so the next edit is its own undo step.
182    pub fn break_coalescing(&mut self) {
183        self.history.break_coalescing();
184    }
185
186    /// Whether there is something to undo.
187    pub fn can_undo(&self) -> bool {
188        self.history.can_undo()
189    }
190
191    /// Whether there is something to redo.
192    pub fn can_redo(&self) -> bool {
193        self.history.can_redo()
194    }
195
196    /// Undo one group; returns whether anything changed.
197    pub fn undo(&mut self) -> bool {
198        let Some(entry) = self.history.pop_undo() else {
199            return false;
200        };
201        if Transaction::replay(&entry.inverse, &mut self.doc).is_err() {
202            return false;
203        }
204        self.selection = self.valid_or_start(entry.selection_before.clone());
205        self.stored_marks = None;
206        self.history.push_redo(entry);
207        true
208    }
209
210    /// Redo one group; returns whether anything changed.
211    pub fn redo(&mut self) -> bool {
212        let Some(entry) = self.history.pop_redo() else {
213            return false;
214        };
215        if Transaction::replay(&entry.steps, &mut self.doc).is_err() {
216            return false;
217        }
218        self.selection = self.valid_or_start(entry.selection_after.clone());
219        self.stored_marks = None;
220        self.history.push_undo(entry);
221        true
222    }
223
224    fn valid_or_start(&self, selection: BlockSelection) -> BlockSelection {
225        match self.doc.validate_selection(&selection) {
226            Ok(()) => selection,
227            Err(_) => match selection {
228                BlockSelection::Text { head, .. } => BlockSelection::caret(self.doc.clamp(head)),
229                BlockSelection::Cells { head, .. } if self.doc.get(head).is_some() => {
230                    BlockSelection::caret(BlockPosition::new(head, 0))
231                }
232                BlockSelection::Blocks { .. } | BlockSelection::Cells { .. } => {
233                    BlockSelection::caret(self.doc.first_caret())
234                }
235            },
236        }
237    }
238
239    /// Run `build` as one transaction: normalize, commit, record history.
240    /// `build` returns the caret to leave behind. On error nothing changes.
241    pub(crate) fn transact(
242        &mut self,
243        origin: EditOrigin,
244        build: impl FnOnce(&mut TransactionBuilder<'_>) -> Result<Option<BlockSelection>, CommandError>,
245    ) -> Result<bool, CommandError> {
246        let before = self.selection.clone();
247        let mut tx = TransactionBuilder::new(&mut self.doc);
248        let after = match build(&mut tx) {
249            Ok(after) => after,
250            Err(err) => {
251                tx.rollback();
252                return Err(err);
253            }
254        };
255        let tx = tx
256            .finish(origin, before.clone(), before.clone())
257            .map_err(CommandError::from)?;
258        let changed = !tx.is_empty();
259        // Without an explicit caret, map the old selection through the steps.
260        let after = after
261            .or_else(|| tx.map_selection(&before))
262            .unwrap_or(before);
263        let after = self.valid_or_start(after);
264        let mut tx = tx;
265        tx.selection_after = after.clone();
266        self.selection = after;
267        self.history.record(tx);
268        Ok(changed)
269    }
270
271    #[cfg(test)]
272    pub(crate) fn doc(&self) -> &EditorDocument {
273        &self.doc
274    }
275}