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