Skip to main content

markdown_editor/
host.rs

1//! Host edits use editor-native positions, never canonical-Markdown offsets.
2use gpui::SharedString;
3
4use super::{BlockEditorModel, BlockPosition, BlockSelection, CommandError, EditOrigin};
5
6/// Immutable canonical source derived once per document revision. Cloning this
7/// value shares the text; it is not another writable document owner.
8#[derive(Clone, Debug)]
9pub struct SourceSnapshot {
10    revision: u64,
11    text: SharedString,
12}
13impl SourceSnapshot {
14    pub fn revision(&self) -> u64 {
15        self.revision
16    }
17    pub fn text(&self) -> &SharedString {
18        &self.text
19    }
20}
21
22/// A read cache, never an editable source owner.
23pub(super) struct PrefixSnapshot {
24    pub(super) revision: u64,
25    pub(super) selection_epoch: u64,
26    pub(super) text: Option<SharedString>,
27}
28
29/// An opaque range tied to one editor, revision and navigation epoch. Successful
30/// replacement advances the capture to cover just the inserted text. Any other
31/// edit, load, undo/redo or navigation invalidates it, even if text is unchanged.
32#[derive(Clone, Debug)]
33pub struct CapturedRange {
34    owner: u64,
35    revision: u64,
36    selection_epoch: u64,
37    range: BlockSelection,
38    expected_selection: BlockSelection,
39}
40
41#[derive(Clone, Debug, PartialEq)]
42#[non_exhaustive]
43pub enum CapturedRangeError {
44    Stale,
45    NotText,
46    InvalidRange,
47    Edit(CommandError),
48}
49
50impl BlockEditorModel {
51    pub fn source_snapshot(&self) -> SourceSnapshot {
52        let revision = self.revision();
53        if let Some(snapshot) = &*self.source_cache.borrow()
54            && snapshot.revision == revision
55        {
56            return snapshot.clone();
57        }
58        let snapshot = SourceSnapshot {
59            revision,
60            text: super::serialize::serialize(&self.doc).to_owned().into(),
61        };
62        *self.source_cache.borrow_mut() = Some(snapshot.clone());
63        snapshot
64    }
65
66    pub(super) fn capture(&self, range: BlockSelection) -> CapturedRange {
67        CapturedRange {
68            owner: self.owner,
69            revision: self.revision(),
70            selection_epoch: self.selection_epoch,
71            range,
72            expected_selection: self.selection.clone(),
73        }
74    }
75
76    /// Capture the head/caret, inserting there even when text is selected.
77    pub fn capture_caret(&self) -> Result<CapturedRange, CapturedRangeError> {
78        let caret = self.caret().ok_or(CapturedRangeError::NotText)?;
79        Ok(self.capture(BlockSelection::caret(caret)))
80    }
81
82    /// Capture the current text selection for one atomic replacement.
83    pub fn capture_selection(&self) -> Result<CapturedRange, CapturedRangeError> {
84        if !matches!(self.selection, BlockSelection::Text { .. }) {
85            return Err(CapturedRangeError::NotText);
86        }
87        Ok(self.capture(self.selection.clone()))
88    }
89
90    /// Text before the caret in its editable leaf. Completion engines inspect
91    /// this plain text, not escaped or reformatted canonical source.
92    /// Revision/selection-keyed shared native prefix. Repeated query/render
93    /// reads do not clone a large caret leaf's bytes.
94    pub fn caret_prefix_snapshot(&self) -> Option<SharedString> {
95        let mut cache = self.prefix_cache.borrow_mut();
96        if cache.as_ref().is_none_or(|cached| {
97            cached.revision != self.revision() || cached.selection_epoch != self.selection_epoch
98        }) {
99            *cache = Some(PrefixSnapshot {
100                revision: self.revision(),
101                selection_epoch: self.selection_epoch,
102                text: self
103                    .caret_prefix()
104                    .map(|text| SharedString::from(text.to_owned())),
105            });
106        }
107        cache.as_ref().and_then(|cached| cached.text.clone())
108    }
109
110    pub fn caret_prefix(&self) -> Option<&str> {
111        let caret = self.caret()?;
112        self.doc.leaf_text(caret.block)?.get(..caret.offset)
113    }
114
115    /// Capture a UTF-8 suffix of the caret's leaf for completion acceptance.
116    pub fn capture_suffix(&self, len: usize) -> Result<CapturedRange, CapturedRangeError> {
117        let head = self.caret().ok_or(CapturedRangeError::NotText)?;
118        let offset = head
119            .offset
120            .checked_sub(len)
121            .ok_or(CapturedRangeError::InvalidRange)?;
122        let anchor = BlockPosition::new(head.block, offset);
123        self.doc
124            .validate(anchor)
125            .map_err(|_| CapturedRangeError::InvalidRange)?;
126        Ok(self.capture(BlockSelection::Text { anchor, head }))
127    }
128
129    /// Capture an exact UTF-8 range in the caret's editing leaf. This range is
130    /// measured in plain leaf text, never canonical Markdown. Useful when a
131    /// completion token ends before trailing text/newlines in that same leaf.
132    pub fn capture_leaf_range(
133        &self,
134        range: std::ops::Range<usize>,
135    ) -> Result<CapturedRange, CapturedRangeError> {
136        let caret = self.caret().ok_or(CapturedRangeError::NotText)?;
137        if range.start > range.end {
138            return Err(CapturedRangeError::InvalidRange);
139        }
140        let anchor = BlockPosition::new(caret.block, range.start);
141        let head = BlockPosition::new(caret.block, range.end);
142        self.doc
143            .validate(anchor)
144            .map_err(|_| CapturedRangeError::InvalidRange)?;
145        self.doc
146            .validate(head)
147            .map_err(|_| CapturedRangeError::InvalidRange)?;
148        Ok(self.capture(BlockSelection::Text { anchor, head }))
149    }
150
151    pub fn is_capture_current(&self, capture: &CapturedRange) -> bool {
152        capture.owner == self.owner
153            && capture.revision == self.revision()
154            && capture.selection_epoch == self.selection_epoch
155            && capture.expected_selection == self.selection
156    }
157
158    /// Replace only a captured range with plain text. One undoable transaction
159    /// per replacement; does not reparse, apply input rules or submit. Errors
160    /// leave both the document and capture intact. The caller retains domain
161    /// ownership (session/window/request) and rejects its own stale callbacks.
162    pub fn replace_captured(
163        &mut self,
164        capture: &mut CapturedRange,
165        text: &str,
166    ) -> Result<(), CapturedRangeError> {
167        if !self.is_capture_current(capture) {
168            return Err(CapturedRangeError::Stale);
169        }
170        let range = self
171            .doc
172            .resolve_selection(&capture.range)
173            .map_err(|_| CapturedRangeError::InvalidRange)?;
174        let before = self.selection.clone();
175        self.selection = capture.range.clone();
176        if let Err(error) = self.insert_text(text, EditOrigin::Api) {
177            self.selection = before;
178            return Err(CapturedRangeError::Edit(error));
179        }
180        let head = self.caret().ok_or(CapturedRangeError::NotText)?;
181        *capture = self.capture(BlockSelection::Text {
182            anchor: range.start,
183            head,
184        });
185        Ok(())
186    }
187
188    /// Invalidate outstanding host captures without changing document/source.
189    /// Hosts call this when the external owner (session/window/request) changes.
190    pub fn invalidate_captures(&mut self) {
191        self.selection_epoch += 1;
192    }
193
194    pub fn select_end(&mut self) -> Result<(), super::SelectionError> {
195        let block = *self.doc.leaves().last().unwrap();
196        let offset = self.doc.leaf_len(block).unwrap_or(0);
197        self.set_selection(BlockSelection::caret(BlockPosition::new(block, offset)))
198    }
199}