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        // The verified admission decision (0057 VF18, crate::mutguard):
139        // readonly refuses first, then a stale base.
140        match crate::mutguard::classify_edit(self.readonly, base.get(), self.revision().get()) {
141            crate::mutguard::EditAdmission::Admitted => {}
142            crate::mutguard::EditAdmission::Readonly => return Err(EditError::ReadOnly),
143            crate::mutguard::EditAdmission::Stale => {
144                return Err(EditError::StaleRevision {
145                    expected: base,
146                    found: self.revision(),
147                });
148            }
149        }
150        replacements.retain(|edit| !edit.range.is_empty() || !edit.text.is_empty());
151        for edit in &replacements {
152            self.validate_range(edit.range)?;
153        }
154        replacements.sort_unstable_by_key(|edit| (edit.range.start.get(), edit.range.end.get()));
155        // The verified geometry kernel (0045, crate::editmap): sorted by
156        // start, strictly non-overlapping, in-bounds — proven, and the
157        // error here can only be an overlap (bounds checked above).
158        let pairs: Vec<(usize, usize)> = replacements
159            .iter()
160            .map(|edit| (edit.range.start.get(), edit.range.end.get()))
161            .collect();
162        let sorted = crate::editmap::check_batch(self.len_bytes(), pairs)
163            .map_err(|()| EditError::Overlap)?;
164        // Keep the payloads beside their sorted geometry. Searching/removing
165        // each payload by range made large reviewed batches quadratic.
166        debug_assert!(replacements
167            .iter()
168            .zip(sorted)
169            .all(|(edit, pair)| (edit.range.start.get(), edit.range.end.get()) == pair));
170        self.epoch
171            .checked_add(replacements.len() as u64)
172            .ok_or(EditError::RevisionExhausted)?;
173        Ok(PreparedReplacements {
174            buffer: self.trace_id(),
175            revision: base,
176            edits: replacements,
177        })
178    }
179
180    pub fn apply_prepared(
181        &mut self,
182        prepared: PreparedReplacements,
183        undo_open: bool,
184    ) -> Result<BufferRevision, EditError> {
185        if prepared.buffer != self.trace_id() {
186            return Err(EditError::WrongBuffer);
187        }
188        // The same verified admission decision as preparation.
189        match crate::mutguard::classify_edit(
190            self.readonly,
191            prepared.revision.get(),
192            self.revision().get(),
193        ) {
194            crate::mutguard::EditAdmission::Admitted => {}
195            crate::mutguard::EditAdmission::Readonly => return Err(EditError::ReadOnly),
196            crate::mutguard::EditAdmission::Stale => {
197                return Err(EditError::StaleRevision {
198                    expected: prepared.revision,
199                    found: self.revision(),
200                });
201            }
202        }
203        if prepared.is_empty() {
204            return Ok(self.revision());
205        }
206        self.history.begin();
207        for replacement in prepared.edits.into_iter().rev() {
208            self.replace_validated(replacement.range, &replacement.text, ChangeOrigin::User);
209        }
210        if !undo_open {
211            self.history.commit();
212        }
213        Ok(self.revision())
214    }
215
216    fn replace_validated(&mut self, range: Range, text: &str, origin: ChangeOrigin) {
217        debug_assert!(self.validate_range(range).is_ok());
218        debug_assert!(self.epoch < u64::MAX);
219        let (start_byte, end_byte) = (range.start.get(), range.end.get());
220        let start_point = self.point_of(start_byte);
221        let old_end_point = self.point_of(end_byte);
222        let extent = Self::point_extent(text);
223        let new_end_point = if extent.0 == 0 {
224            (start_point.0, start_point.1 + extent.1)
225        } else {
226            (start_point.0 + extent.0, extent.1)
227        };
228        let edit = InputEdit {
229            start_byte,
230            old_end_byte: end_byte,
231            new_end_byte: start_byte + text.len(),
232            start_point,
233            old_end_point,
234            new_end_point,
235        };
236        let mut history = None;
237        if origin == ChangeOrigin::User {
238            if !range.is_empty() {
239                let removed = self.rope.byte_slice(start_byte..end_byte).to_string();
240                history = Some(self.history.record(
241                    Edit {
242                        at: start_byte,
243                        text: removed.clone(),
244                        kind: EditKind::Insert,
245                    },
246                    Edit {
247                        at: start_byte,
248                        text: removed,
249                        kind: EditKind::Delete,
250                    },
251                ));
252            }
253            if !text.is_empty() {
254                history = Some(self.history.record(
255                    Edit {
256                        at: start_byte,
257                        text: text.into(),
258                        kind: EditKind::Delete,
259                    },
260                    Edit {
261                        at: start_byte,
262                        text: text.into(),
263                        kind: EditKind::Insert,
264                    },
265                ));
266            }
267        }
268        let start = self.rope.byte_to_char(start_byte);
269        let end = self.rope.byte_to_char(end_byte);
270        if start != end {
271            self.rope.remove(start..end);
272        }
273        if !text.is_empty() {
274            self.rope.insert(start, text);
275        }
276        self.publish_change(edit, origin, history);
277        self.trace_edit(origin, start_byte, range.len(), text);
278    }
279
280    fn publish_change(
281        &mut self,
282        edit: InputEdit,
283        origin: ChangeOrigin,
284        history: Option<crate::history::EditRef>,
285    ) {
286        debug_assert!(self.epoch < u64::MAX);
287        self.epoch += 1;
288        self.invalidate_line_layouts(&edit);
289        self.dirty |= origin != ChangeOrigin::System;
290        self.changes.push(Change {
291            revision: self.revision(),
292            origin,
293            edit,
294            history,
295        });
296    }
297}
298
299impl UserEdit<'_> {
300    pub fn insert(&mut self, at: impl Into<ByteOffset>, text: &str) -> Result<(), EditError> {
301        let at = at.into();
302        self.replace(Range::charwise(at, at), text)
303    }
304    pub fn delete(&mut self, range: Range) -> Result<String, EditError> {
305        if !crate::mutguard::writable(self.buffer.readonly) {
306            return Err(EditError::ReadOnly);
307        }
308        self.buffer.validate_range(range)?;
309        let removed = self.buffer.slice_string(range);
310        self.replace(range, "")?;
311        Ok(removed)
312    }
313    pub fn replace(&mut self, range: Range, text: &str) -> Result<(), EditError> {
314        if !crate::mutguard::writable(self.buffer.readonly) {
315            return Err(EditError::ReadOnly);
316        }
317        self.buffer.validate_range(range)?;
318        if range.is_empty() && text.is_empty() {
319            return Ok(());
320        }
321        self.buffer
322            .epoch
323            .checked_add(1)
324            .ok_or(EditError::RevisionExhausted)?;
325        self.buffer
326            .replace_validated(range, text, ChangeOrigin::User);
327        Ok(())
328    }
329}
330
331impl SystemEdit<'_> {
332    /// A partial generated-surface edit (0049 §5 collection view
333    /// splices): system origin, never undoable, and it never dirties the
334    /// buffer.
335    pub fn replace(&mut self, range: Range, text: &str) -> Result<(), EditError> {
336        self.buffer.validate_range(range)?;
337        if range.is_empty() && text.is_empty() {
338            return Ok(());
339        }
340        self.buffer
341            .epoch
342            .checked_add(1)
343            .ok_or(EditError::RevisionExhausted)?;
344        self.buffer
345            .replace_validated(range, text, ChangeOrigin::System);
346        Ok(())
347    }
348
349    /// A generated surface is a new system snapshot, never an undoable user edit.
350    pub fn replace_all(&mut self, text: &str) -> Result<(), EditError> {
351        self.buffer
352            .epoch
353            .checked_add(1)
354            .ok_or(EditError::RevisionExhausted)?;
355        self.buffer.replace_validated(
356            Range::charwise(0, self.buffer.len_bytes()),
357            text,
358            ChangeOrigin::System,
359        );
360        self.buffer.adopt_history(Default::default());
361        Ok(())
362    }
363
364    /// Publish worker-built text without flattening or rebuilding a rope on the
365    /// event loop. Identity, readonly policy and monotonic revision stay local.
366    pub fn replace_rope(&mut self, rope: ropey::Rope) -> Result<(), EditError> {
367        self.buffer
368            .epoch
369            .checked_add(1)
370            .ok_or(EditError::RevisionExhausted)?;
371        let old_end_byte = self.buffer.len_bytes();
372        let old_end_point = self.buffer.point_of(old_end_byte);
373        let new_end_byte = rope.len_bytes();
374        let line = rope.byte_to_line(new_end_byte);
375        let new_end_point = (line, new_end_byte - rope.line_to_byte(line));
376        self.buffer.rope = rope;
377        self.buffer.publish_change(
378            InputEdit {
379                start_byte: 0,
380                old_end_byte,
381                new_end_byte,
382                start_point: (0, 0),
383                old_end_point,
384                new_end_point,
385            },
386            ChangeOrigin::System,
387            None,
388        );
389        self.buffer.adopt_history(Default::default());
390        self.buffer.trace_snapshot(old_end_byte);
391        Ok(())
392    }
393}
394
395#[derive(Debug, Clone, Copy)]
396pub struct HistoryMove {
397    pub start: ByteOffset,
398    pub end: ByteOffset,
399}
400
401impl Buffer {
402    pub fn undo(&mut self) -> Result<Option<HistoryMove>, EditError> {
403        self.move_history(crate::history::HistoryAction::Undo)
404    }
405    pub fn redo(&mut self) -> Result<Option<HistoryMove>, EditError> {
406        self.move_history(crate::history::HistoryAction::Redo)
407    }
408    pub fn restore_revision(&mut self, revision: usize) -> Result<Option<HistoryMove>, EditError> {
409        self.move_history(crate::history::HistoryAction::Jump(revision))
410    }
411    /// Preflight a grouped undo without changing history or buffer contents.
412    pub fn check_undo(&self) -> Result<bool, EditError> {
413        self.check_history_move(crate::history::HistoryAction::Undo)
414            .map(|cost| cost.is_some())
415    }
416
417    /// Preflight a grouped history restore, including authority and revision capacity.
418    pub fn check_restore_revision(&self, revision: usize) -> Result<bool, EditError> {
419        self.check_history_move(crate::history::HistoryAction::Jump(revision))
420            .map(|cost| cost.is_some())
421    }
422
423    fn check_history_move(
424        &self,
425        action: crate::history::HistoryAction,
426    ) -> Result<Option<usize>, EditError> {
427        if !crate::mutguard::writable(self.readonly) {
428            return Err(EditError::ReadOnly);
429        }
430        let cost = self.history.movement_cost(action);
431        if let Some(cost) = cost {
432            self.epoch
433                .checked_add(cost as u64)
434                .ok_or(EditError::RevisionExhausted)?;
435        }
436        Ok(cost)
437    }
438    fn move_history(
439        &mut self,
440        action: crate::history::HistoryAction,
441    ) -> Result<Option<HistoryMove>, EditError> {
442        let Some(cost) = self.check_history_move(action)? else {
443            return Ok(None);
444        };
445        // A valid history is guaranteed by construction and validated at restore.
446        let Some(ops) = self.history.navigate(action) else {
447            return Ok(None);
448        };
449        debug_assert_eq!(cost, ops.len());
450        let start = ops.iter().map(|edit| edit.at).min().unwrap_or(0);
451        let end = ops
452            .last()
453            .map(|edit| {
454                edit.at
455                    + if edit.kind == EditKind::Insert {
456                        edit.text.len()
457                    } else {
458                        0
459                    }
460            })
461            .unwrap_or(start);
462        for op in &ops {
463            let (range, text) = match op.kind {
464                EditKind::Insert => (Range::charwise(op.at, op.at), op.text.as_str()),
465                EditKind::Delete => (Range::charwise(op.at, op.at + op.text.len()), ""),
466            };
467            debug_assert!(self.validate_range(range).is_ok(), "sealed history range");
468            debug_assert!(
469                op.kind != EditKind::Delete
470                    || self.rope.byte_slice(range.start.get()..range.end.get()) == op.text,
471                "sealed history content"
472            );
473            self.replace_validated(range, text, ChangeOrigin::History);
474        }
475        self.trace_history(&ops);
476        Ok(Some(HistoryMove {
477            start: start.into(),
478            end: end.into(),
479        }))
480    }
481}