Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 97 additions & 4 deletions crates/helm-text/src/buffer_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};

use crate::{
Buffer, BufferId, HistoryError, Snapshot, TextBuffer, Transaction, TransactionError,
TransactionId, Version,
AmendError, Buffer, BufferId, HistoryError, Snapshot, TextBuffer, Transaction,
TransactionError, TransactionId, Version,
};

/// Buffers that edit, undo and redo through transactions, including
/// transactions that touch several of them at once.
///
/// The set owns its buffers. [`get`](Self::get) lends them out read-only,
/// and the only way to change one is [`apply`](Self::apply),
/// [`undo`](Self::undo) or [`redo`](Self::redo). So no buffer can change
/// behind the history's back, and the rules below hold by construction.
/// [`amend`](Self::amend), [`undo`](Self::undo) or [`redo`](Self::redo). So
/// no buffer can change behind the history's back, and the rules below hold
/// by construction.
///
/// # Applying a transaction
///
Expand Down Expand Up @@ -291,6 +292,98 @@ impl BufferSet {
Ok(TransactionId(id))
}

/// Folds `transaction`'s edits into the applied transaction `id`, so one
/// undo reverts both and one redo brings both back.
///
/// This is how a run of keystrokes becomes one undo step (ED-2): the
/// editor applies the first keystroke as a transaction and amends it
/// with each keystroke after that. Every keystroke changes the buffer at
/// once, so language servers and agents, which read the buffer, see it.
///
/// # When amending is allowed
///
/// `id` must be applied, not undone, and must be on top of the undo
/// history of every buffer `transaction` touches. `transaction` may only
/// touch buffers `id` touched, though it needn't touch all of them. A
/// later transaction on top of a buffer that `transaction` doesn't touch
/// doesn't stop the amend. It still blocks undoing `id`, as the
/// [type docs](Self) describe.
///
/// # Offsets
///
/// Every edit's range is in bytes of its buffer's text after `id`, which
/// is the buffer's current text, since `id` is on top of its undo
/// history. Within `transaction`, ranges refer to that text, not to the
/// text after the edits listed before them, and edits are placed and
/// must not overlap exactly as for [`apply`](Self::apply).
///
/// # Checks
///
/// `amend` checks everything before it changes anything, in this order,
/// and returns the first problem it finds:
///
/// 1. `id` must be in some history of the set
/// ([`AmendError::UnknownTransaction`]). An id from another set, or
/// of a transaction a later `apply` or `amend` dropped from the redo
/// histories, fails here.
/// 2. `id` must not be undone ([`AmendError::Undone`]).
/// 3. `transaction` must have edits
/// ([`AmendError::Transaction`] with [`TransactionError::Empty`]).
/// 4. For each edit in order: its buffer must be in the set
/// ([`TransactionError::UnknownBuffer`]); then `id` must have touched
/// it ([`AmendError::Untouched`]); then `id` must be on top of its
/// undo history ([`AmendError::NotOnTop`]); then its range must be
/// valid in the buffer's current text, checked the way
/// [`TextBuffer::slice`] documents
/// ([`TransactionError::InvalidRange`]); then it must not overlap an
/// earlier edit on the same buffer ([`TransactionError::Overlap`],
/// naming the earliest such edit). The [`TransactionError`]s come
/// wrapped in [`AmendError::Transaction`], with the same edit indexes
/// `apply` would give.
///
/// If any check fails, no buffer, history or version changes.
///
/// # What an amend does
///
/// - Each buffer `transaction` touches gets its edits, exactly as if
/// `apply` had applied them.
/// - `id` keeps its id and its place in every history.
/// [`undo`](Self::undo) and [`redo`](Self::redo) return `id` as
/// before, and `amend` uses up no new id.
/// - Undoing `id` from any buffer it touched restores every one of those
/// buffers, byte for byte, to its text from before `id` was first
/// applied. Redoing it restores each one to its text after the last
/// amend. Buffers `id` touched that no amend touched get back the text
/// `id` alone gave them.
/// - The redo histories of the buffers `transaction` touches are
/// dropped, with the same knock-on drops as for `apply`.
/// - The version of each buffer `transaction` touches goes up by exactly
/// one, however many edits it had there, so snapshots taken before go
/// stale. Other buffers keep their versions, including ones `id`
/// touched that this amend didn't. A later undo or redo of `id` adds
/// one to every buffer `id` touched, as for any transaction.
///
/// An amend whose edits leave the text as it was, such as an empty
/// insert, still counts: it drops redo and bumps versions.
///
/// # Cost
///
/// An amend costs about what applying `transaction` costs, plus work in
/// proportion to the changes `id` already holds for the buffers it
/// touches. It doesn't grow with the buffer's length, apart from the
/// text the edits replace, and it doesn't keep a list of amends to
/// replay: after any number of amends, undoing `id` is one change per
/// buffer, so a run of typed characters undoes like one insert.
///
/// [`TextBuffer::slice`]: crate::TextBuffer::slice
#[expect(
unused_variables,
reason = "T-110 interface stub; the implementation deletes this"
)]
pub fn amend(&mut self, id: TransactionId, transaction: Transaction) -> Result<(), AmendError> {
todo!()
}

/// Reverts the transaction on top of `buffer`'s undo history in every
/// buffer it touched, and returns its id. Returns `Ok(None)` if
/// `buffer`'s undo history is empty.
Expand Down
72 changes: 72 additions & 0 deletions crates/helm-text/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,75 @@ impl fmt::Display for HistoryError {
}

impl std::error::Error for HistoryError {}

/// Why [`BufferSet::amend`](crate::BufferSet::amend) rejected an amendment.
///
/// `edit` is an index into [`Transaction::edits`](crate::Transaction::edits)
/// of the transaction passed to `amend`. A rejected amendment changes
/// nothing.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AmendError {
/// The transaction isn't in any undo or redo history of the set. It was
/// never applied in this set, or a later transaction dropped it from the
/// redo histories.
UnknownTransaction {
/// The transaction that was asked for.
transaction: TransactionId,
},
/// The transaction was undone and hasn't been redone, so it sits in the
/// redo histories.
Undone {
/// The transaction that was asked for.
transaction: TransactionId,
},
/// An edit names a buffer in the set that the transaction didn't touch.
Untouched {
/// The edit's index.
edit: usize,
/// The buffer it names.
buffer: BufferId,
},
/// An edit names a buffer whose undo history has a later transaction on
/// top of the one being amended.
NotOnTop {
/// The edit's index.
edit: usize,
/// The buffer it names.
buffer: BufferId,
},
/// The edits fail one of the checks
/// [`BufferSet::apply`](crate::BufferSet::apply) makes, run against the
/// text after the transaction being amended.
Transaction(TransactionError),
}

impl fmt::Display for AmendError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::UnknownTransaction { transaction } => {
write!(f, "{transaction:?} isn't in any history of the set")
}
Self::Undone { transaction } => {
write!(f, "{transaction:?} was undone, so it can't be amended")
}
Self::Untouched { edit, buffer } => write!(
f,
"edit {edit} names {buffer:?}, which the transaction didn't touch"
),
Self::NotOnTop { edit, buffer } => write!(
f,
"edit {edit} names {buffer:?}, which has a later transaction on top"
),
Self::Transaction(error) => write!(f, "{error}"),
}
}
}

impl std::error::Error for AmendError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Transaction(error) => Some(error),
_ => None,
}
}
}
2 changes: 1 addition & 1 deletion crates/helm-text/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ pub use encoding::{
ByteOrder, DecodeError, Decoded, FileFormat, LineEnding, LineEndingStyle, LineEndings,
UTF16_SAMPLE_LEN,
};
pub use error::{HistoryError, TextError, TransactionError};
pub use error::{AmendError, HistoryError, TextError, TransactionError};
pub use snapshot::{Snapshot, Version};
pub use transaction::{BufferId, Edit, Transaction, TransactionId};

Expand Down
10 changes: 6 additions & 4 deletions crates/helm-text/src/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ use crate::{Buffer, BufferId, Point, TextBuffer, TextError, Utf16Point};
/// A buffer starts at version 0 when [`BufferSet::add`] adds it. Each
/// successful [`apply`](BufferSet::apply), [`undo`](BufferSet::undo) and
/// [`redo`](BufferSet::redo) adds exactly one to the version of every buffer
/// the transaction touched, however many edits it had there. A transaction
/// whose edits leave the text as it was still counts. Nothing else changes a
/// version: not a failed `apply`, not an undo or redo that returns
/// `Ok(None)` or an error, and not [`get`](BufferSet::get),
/// the transaction touched, however many edits it had there. Each
/// successful [`amend`](crate::BufferSet::amend) adds one to the version of
/// every buffer its new edits touched. A transaction or amend whose edits
/// leave the text as it was still counts. Nothing else changes a version: not a failed `apply` or
/// `amend`, not an undo or redo that returns `Ok(None)` or an error, and not
/// [`get`](BufferSet::get),
/// [`version`](BufferSet::version) or [`snapshot`](BufferSet::snapshot).
/// Buffers a transaction didn't touch keep their versions.
///
Expand Down
Loading
Loading