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