Skip to main content

strop_core/buffer/
mutation.rs

1//! The only mutable text capability. Every mutation publishes pre-edit geometry.
2use super::Buffer;
3use crate::history::{Edit, EditKind};
4use crate::id::{BufferRevision, ByteOffset};
5use crate::{InputEdit, Range};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
8pub enum ChangeOrigin {
9    User,
10    History,
11    System,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct Change {
16    pub revision: BufferRevision,
17    pub origin: ChangeOrigin,
18    pub edit: InputEdit,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
22pub enum EditError {
23    #[error("readonly buffer")]
24    ReadOnly,
25    #[error("invalid edit range")]
26    InvalidRange,
27    #[error("overlapping replacements in pre-edit coordinates")]
28    Overlap,
29    #[error("prepared replacement belongs to another buffer")]
30    WrongBuffer,
31    #[error("stale revision: expected {expected}, found {found}")]
32    StaleRevision {
33        expected: BufferRevision,
34        found: BufferRevision,
35    },
36    #[error("buffer revision exhausted")]
37    RevisionExhausted,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct Replacement {
42    pub range: Range,
43    pub text: String,
44}
45impl Replacement {
46    pub fn new(range: Range, text: impl Into<String>) -> Self {
47        Self {
48            range,
49            text: text.into(),
50        }
51    }
52}
53
54/// Validated, sorted replacements bound to one exact buffer incarnation/revision.
55/// The editor may restore prompt selection state before acquiring its write lease.
56pub struct PreparedReplacements {
57    buffer: crate::diagnostics::BufferTraceId,
58    revision: BufferRevision,
59    edits: Vec<Replacement>,
60}
61impl PreparedReplacements {
62    pub fn is_empty(&self) -> bool {
63        self.edits.is_empty()
64    }
65}
66
67pub struct UserEdit<'a> {
68    buffer: &'a mut Buffer,
69}
70pub struct SystemEdit<'a> {
71    buffer: &'a mut Buffer,
72}
73
74impl Buffer {
75    pub fn edit(&mut self) -> UserEdit<'_> {
76        UserEdit { buffer: self }
77    }
78    pub fn system_edit(&mut self) -> SystemEdit<'_> {
79        SystemEdit { buffer: self }
80    }
81    pub fn changes(&self) -> &[Change] {
82        &self.changes
83    }
84    pub fn clear_changes(&mut self) {
85        self.changes.clear();
86    }
87    pub fn begin_undo_group(&mut self) {
88        self.history.begin();
89    }
90    pub fn commit_undo_group(&mut self) {
91        self.history.commit();
92    }
93
94    fn validate_range(&self, range: Range) -> Result<(), EditError> {
95        if range.start > range.end || !self.is_boundary(range.start) || !self.is_boundary(range.end)
96        {
97            return Err(EditError::InvalidRange);
98        }
99        Ok(())
100    }
101
102    /// All replacements refer to one pre-edit snapshot. Validate the whole batch
103    /// before opening history or touching text; reversed application preserves coordinates.
104    pub fn prepare_replacements(
105        &self,
106        base: BufferRevision,
107        mut replacements: Vec<Replacement>,
108    ) -> Result<PreparedReplacements, EditError> {
109        if self.readonly {
110            return Err(EditError::ReadOnly);
111        }
112        if base != self.revision() {
113            return Err(EditError::StaleRevision {
114                expected: base,
115                found: self.revision(),
116            });
117        }
118        replacements.retain(|edit| !edit.range.is_empty() || !edit.text.is_empty());
119        for edit in &replacements {
120            self.validate_range(edit.range)?;
121        }
122        // The verified geometry kernel (0045, crate::editmap): sorted by
123        // start, strictly non-overlapping, in-bounds — proven, and the
124        // error here can only be an overlap (bounds checked above).
125        let pairs: Vec<(usize, usize)> = replacements
126            .iter()
127            .map(|edit| (edit.range.start.get(), edit.range.end.get()))
128            .collect();
129        let sorted = crate::editmap::check_batch(self.len_bytes(), pairs)
130            .map_err(|()| EditError::Overlap)?;
131        // Validated batches carry unique ranges, so each pair names its
132        // Replacement unambiguously.
133        let mut rest = replacements;
134        let mut ordered = Vec::with_capacity(sorted.len());
135        for pair in sorted {
136            let at = rest
137                .iter()
138                .position(|edit| edit.range.start.get() == pair.0 && edit.range.end.get() == pair.1)
139                .expect("a validated batch contains only input ranges");
140            ordered.push(rest.remove(at));
141        }
142        replacements = ordered;
143        self.epoch
144            .checked_add(replacements.len() as u64)
145            .ok_or(EditError::RevisionExhausted)?;
146        Ok(PreparedReplacements {
147            buffer: self.trace_id(),
148            revision: base,
149            edits: replacements,
150        })
151    }
152
153    pub fn apply_prepared(
154        &mut self,
155        prepared: PreparedReplacements,
156        undo_open: bool,
157    ) -> Result<BufferRevision, EditError> {
158        if prepared.buffer != self.trace_id() {
159            return Err(EditError::WrongBuffer);
160        }
161        if self.readonly {
162            return Err(EditError::ReadOnly);
163        }
164        if prepared.revision != self.revision() {
165            return Err(EditError::StaleRevision {
166                expected: prepared.revision,
167                found: self.revision(),
168            });
169        }
170        if prepared.is_empty() {
171            return Ok(self.revision());
172        }
173        self.history.begin();
174        for replacement in prepared.edits.into_iter().rev() {
175            self.replace_validated(replacement.range, &replacement.text, ChangeOrigin::User);
176        }
177        if !undo_open {
178            self.history.commit();
179        }
180        Ok(self.revision())
181    }
182
183    fn replace_validated(&mut self, range: Range, text: &str, origin: ChangeOrigin) {
184        debug_assert!(self.validate_range(range).is_ok());
185        debug_assert!(self.epoch < u64::MAX);
186        let (start_byte, end_byte) = (range.start.get(), range.end.get());
187        let start_point = self.point_of(start_byte);
188        let old_end_point = self.point_of(end_byte);
189        let extent = Self::point_extent(text);
190        let new_end_point = if extent.0 == 0 {
191            (start_point.0, start_point.1 + extent.1)
192        } else {
193            (start_point.0 + extent.0, extent.1)
194        };
195        let edit = InputEdit {
196            start_byte,
197            old_end_byte: end_byte,
198            new_end_byte: start_byte + text.len(),
199            start_point,
200            old_end_point,
201            new_end_point,
202        };
203        if origin == ChangeOrigin::User {
204            if !range.is_empty() {
205                let removed = self.rope.byte_slice(start_byte..end_byte).to_string();
206                self.history.record(
207                    Edit {
208                        at: start_byte,
209                        text: removed.clone(),
210                        kind: EditKind::Insert,
211                    },
212                    Edit {
213                        at: start_byte,
214                        text: removed,
215                        kind: EditKind::Delete,
216                    },
217                );
218            }
219            if !text.is_empty() {
220                self.history.record(
221                    Edit {
222                        at: start_byte,
223                        text: text.into(),
224                        kind: EditKind::Delete,
225                    },
226                    Edit {
227                        at: start_byte,
228                        text: text.into(),
229                        kind: EditKind::Insert,
230                    },
231                );
232            }
233        }
234        let start = self.rope.byte_to_char(start_byte);
235        let end = self.rope.byte_to_char(end_byte);
236        if start != end {
237            self.rope.remove(start..end);
238        }
239        if !text.is_empty() {
240            self.rope.insert(start, text);
241        }
242        self.publish_change(edit, origin);
243        self.trace_edit(origin, start_byte, range.len(), text);
244    }
245
246    fn publish_change(&mut self, edit: InputEdit, origin: ChangeOrigin) {
247        debug_assert!(self.epoch < u64::MAX);
248        self.epoch += 1;
249        self.invalidate_line_layouts(&edit);
250        self.dirty |= origin != ChangeOrigin::System;
251        self.changes.push(Change {
252            revision: self.revision(),
253            origin,
254            edit,
255        });
256    }
257}
258
259impl UserEdit<'_> {
260    pub fn insert(&mut self, at: impl Into<ByteOffset>, text: &str) -> Result<(), EditError> {
261        let at = at.into();
262        self.replace(Range::charwise(at, at), text)
263    }
264    pub fn delete(&mut self, range: Range) -> Result<String, EditError> {
265        if self.buffer.readonly {
266            return Err(EditError::ReadOnly);
267        }
268        self.buffer.validate_range(range)?;
269        let removed = self.buffer.slice_string(range);
270        self.replace(range, "")?;
271        Ok(removed)
272    }
273    pub fn replace(&mut self, range: Range, text: &str) -> Result<(), EditError> {
274        if self.buffer.readonly {
275            return Err(EditError::ReadOnly);
276        }
277        self.buffer.validate_range(range)?;
278        if range.is_empty() && text.is_empty() {
279            return Ok(());
280        }
281        self.buffer
282            .epoch
283            .checked_add(1)
284            .ok_or(EditError::RevisionExhausted)?;
285        self.buffer
286            .replace_validated(range, text, ChangeOrigin::User);
287        Ok(())
288    }
289}
290
291impl SystemEdit<'_> {
292    /// A partial generated-surface edit (0049 §5 collection view
293    /// splices): system origin, never undoable, and it never dirties the
294    /// buffer.
295    pub fn replace(&mut self, range: Range, text: &str) -> Result<(), EditError> {
296        self.buffer.validate_range(range)?;
297        if range.is_empty() && text.is_empty() {
298            return Ok(());
299        }
300        self.buffer
301            .epoch
302            .checked_add(1)
303            .ok_or(EditError::RevisionExhausted)?;
304        self.buffer
305            .replace_validated(range, text, ChangeOrigin::System);
306        Ok(())
307    }
308
309    /// A generated surface is a new system snapshot, never an undoable user edit.
310    pub fn replace_all(&mut self, text: &str) -> Result<(), EditError> {
311        self.buffer
312            .epoch
313            .checked_add(1)
314            .ok_or(EditError::RevisionExhausted)?;
315        self.buffer.replace_validated(
316            Range::charwise(0, self.buffer.len_bytes()),
317            text,
318            ChangeOrigin::System,
319        );
320        self.buffer.history = Default::default();
321        Ok(())
322    }
323
324    /// Publish worker-built text without flattening or rebuilding a rope on the
325    /// event loop. Identity, readonly policy and monotonic revision stay local.
326    pub fn replace_rope(&mut self, rope: ropey::Rope) -> Result<(), EditError> {
327        self.buffer
328            .epoch
329            .checked_add(1)
330            .ok_or(EditError::RevisionExhausted)?;
331        let old_end_byte = self.buffer.len_bytes();
332        let old_end_point = self.buffer.point_of(old_end_byte);
333        let new_end_byte = rope.len_bytes();
334        let line = rope.byte_to_line(new_end_byte);
335        let new_end_point = (line, new_end_byte - rope.line_to_byte(line));
336        self.buffer.rope = rope;
337        self.buffer.publish_change(
338            InputEdit {
339                start_byte: 0,
340                old_end_byte,
341                new_end_byte,
342                start_point: (0, 0),
343                old_end_point,
344                new_end_point,
345            },
346            ChangeOrigin::System,
347        );
348        self.buffer.history = Default::default();
349        self.buffer.trace_snapshot(old_end_byte);
350        Ok(())
351    }
352}
353
354#[derive(Debug, Clone, Copy)]
355pub struct HistoryMove {
356    pub start: ByteOffset,
357    pub end: ByteOffset,
358}
359
360impl Buffer {
361    pub fn undo(&mut self) -> Result<Option<HistoryMove>, EditError> {
362        self.move_history(crate::history::HistoryAction::Undo)
363    }
364    pub fn redo(&mut self) -> Result<Option<HistoryMove>, EditError> {
365        self.move_history(crate::history::HistoryAction::Redo)
366    }
367    pub fn restore_revision(&mut self, revision: usize) -> Result<Option<HistoryMove>, EditError> {
368        self.move_history(crate::history::HistoryAction::Jump(revision))
369    }
370    fn move_history(
371        &mut self,
372        action: crate::history::HistoryAction,
373    ) -> Result<Option<HistoryMove>, EditError> {
374        if self.readonly {
375            return Err(EditError::ReadOnly);
376        }
377        let Some(cost) = self.history.movement_cost(action) else {
378            return Ok(None);
379        };
380        self.epoch
381            .checked_add(cost as u64)
382            .ok_or(EditError::RevisionExhausted)?;
383        // A valid history is guaranteed by construction and validated at restore.
384        let Some(ops) = self.history.navigate(action) else {
385            return Ok(None);
386        };
387        debug_assert_eq!(cost, ops.len());
388        let start = ops.iter().map(|edit| edit.at).min().unwrap_or(0);
389        let end = ops
390            .last()
391            .map(|edit| {
392                edit.at
393                    + if edit.kind == EditKind::Insert {
394                        edit.text.len()
395                    } else {
396                        0
397                    }
398            })
399            .unwrap_or(start);
400        for op in &ops {
401            let (range, text) = match op.kind {
402                EditKind::Insert => (Range::charwise(op.at, op.at), op.text.as_str()),
403                EditKind::Delete => (Range::charwise(op.at, op.at + op.text.len()), ""),
404            };
405            debug_assert!(self.validate_range(range).is_ok(), "sealed history range");
406            debug_assert!(
407                op.kind != EditKind::Delete
408                    || self.rope.byte_slice(range.start.get()..range.end.get()) == op.text,
409                "sealed history content"
410            );
411            self.replace_validated(range, text, ChangeOrigin::History);
412        }
413        self.trace_history(&ops);
414        Ok(Some(HistoryMove {
415            start: start.into(),
416            end: end.into(),
417        }))
418    }
419}