diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index 523c904c1dc7b..937911ca834c1 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -106,7 +106,7 @@ use super::{Res, ResMut, SystemState}; /// /// The implementor must ensure that the state returned /// from [`SystemParamBuilder::build`] is valid for `P`. -/// Note that the exact safety requiremensts depend on the implementation of [`SystemParam`], +/// Note that the exact safety requirements depend on the implementation of [`SystemParam`], /// so if `Self` is not a local type then you must call [`SystemParam::init_state`] /// or another [`SystemParamBuilder::build`]. pub unsafe trait SystemParamBuilder: Sized { diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index e8b237f5e691d..97bb05b00545e 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -10,6 +10,7 @@ keywords = ["bevy"] [features] default_font = [] +serde = [] [dependencies] # bevy @@ -40,9 +41,11 @@ serde = { version = "1", features = ["derive"] } smallvec = { version = "1", default-features = false } sys-locale = "0.3.0" tracing = { version = "0.1", default-features = false, features = ["std"] } +document-features = "0.2" [dev-dependencies] approx = "0.5.1" +serde_json = "1" [lints] workspace = true diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 56ff703ccaceb..2cb2e4c26cd44 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -42,6 +42,7 @@ mod pipeline; mod text; mod text2d; mod text_access; +pub mod undo_2; use bevy_camera::{visibility::VisibilitySystems, CameraUpdateSystems}; pub use bounds::*; diff --git a/crates/bevy_text/src/undo_2/README.md b/crates/bevy_text/src/undo_2/README.md new file mode 100644 index 0000000000000..e367e8f6a8c43 --- /dev/null +++ b/crates/bevy_text/src/undo_2/README.md @@ -0,0 +1,265 @@ +# Undo done the right way + +This code is from the library `undo_2` , which has a version `0.3.0` that is not released to `crates.io`. + +[Original source](https://gitlab.com/okannen/undo_2/-/tree/b32c34edb2c15c266b946f0d82188624f3aa3fdc) + +It is temporarily upstreamed to this repo, with the intention to use the original source in the future. + +## Introduction + +An undo crate that makes it so that, the instant you edit something you undid +to, instead of truncating the undo/redo history, it bakes the rewind onto the +end of the Undo history as a precursor to your new change. I found the idea in +[zaboople/klong][zaboople]. This crate is an implementation +of this idea with a minor variation explained below. + +As an example consider the following sequence of commands: + +| Command | State | +| ------- | ----- | +| Init | | +| Do A | A | +| Do B | A, B | +| Undo | A | +| Do C | A, C | + +With the **classical undo**, repeated undo would lead to the sequence: + +| Command | State | +|---------|-------| +| | A, C | +| Undo | A | +| Undo | | + +Starting from 5, with **undo_2**, repeating undo would lead to the sequence: + +| Command | State | +|---------|-------| +| | A, C | +| Undo | A | +| Undo | A,B | +| Undo | A | +| Undo | | + +**undo_2**'s undo navigates back in history, while classical undo navigates back +through the sequence of command that builds the state. + +This is actualy the way undo is often implemented in mac's (cocoa library), emacs +and it is similar to vim :earlier. + +## Features + + 1. historical undo sequence, no commands are lost. + 2. user-friendly compared to complex undo trees. + 3. optimized implementation: no commands are ever copied. + 4. very lightweight, dumb and simple. + 5. possibility to merge and splice commands. + +## How to use it + +Add the dependency to the cargo file: + +```toml +[dependencies] +undo_2 = "0.1" +``` + +Then add this to your source file: + +```ignore +use undo_2::Commands; +``` + +The example below implements a dumb text editor. *undo_2* does not perform +itself "undos" and "redos", rather, it returns a sequence of commands that must +be interpreted by the application. This design pattern makes implementation +easier because it is not necessary to borrow data within the stored list of +commands. + +```rs +use undo_2::{Commands,Action}; +use Action::{Do,Undo}; + +enum Command { + Add(char), + Delete(char), +} + +struct TextEditor { + text: String, + command: Commands, +} + +impl TextEditor { + pub fn new() -> Self { + Self { + text: String::new(), + command: Commands::new(), + } + } + pub fn add_char(&mut self, c: char) { + self.text.push(c); + self.command.push(Command::Add(c)); + } + pub fn delete_char(&mut self) { + if let Some(c) = self.text.pop() { + self.command.push(Command::Delete(c)); + } + } + pub fn undo(&mut self) { + for action in self.command.undo() { + interpret_action(&mut self.text, action) + } + } + pub fn redo(&mut self) { + for action in self.command.redo() { + interpret_action(&mut self.text, action) + } + } +} + +fn interpret_action(data: &mut String, action: Action<&Command>) { + use Command::*; + match action { + Do(Add(c)) | Undo(Delete(c)) => { + data.push(*c); + } + Undo(Add(_)) | Do(Delete(_)) => { + data.pop(); + } + } +} + +let mut editor = TextEditor::new(); +editor.add_char('a'); // :[1] +editor.add_char('b'); // :[2] +editor.add_char('d'); // :[3] +assert_eq!(editor.text, "abd"); + +editor.undo(); // first undo :[4] +assert_eq!(editor.text, "ab"); + +editor.add_char('c'); // :[5] +assert_eq!(editor.text, "abc"); + +editor.undo(); // Undo [5] :[6] +assert_eq!(editor.text, "ab"); +editor.undo(); // Undo the undo [4] :[7] +assert_eq!(editor.text, "abd"); +editor.undo(); // Undo [3] :[8] +assert_eq!(editor.text, "ab"); +editor.undo(); // Undo [2] :[9] +assert_eq!(editor.text, "a"); +``` + +## More information + +1. After a sequence of consecutive undo, if a new command is added, the undos + forming the sequence are merged. This makes the traversal of the undo + sequence more concise by avoiding state duplication. + +| Command | State | Comment | +|---------|------- |----------------------| +| Init | | | +| Do A | A | | +| Do B | A,B | | +| Do C | A, B, C | | +| Undo | A, B |Merged | +| Undo | A |Merged | +| Do D | A, D | | +| Undo | A |Redo the 2 Merged Undo| +| Undo | A, B, C | | +| Undo | A, B | | +| Undo | A | | +| Undo | | | + +1. Each execution of an undos or redo may lead to the execution of a sequence of + actions in the form `Undo(a)+Do(b)+Do(c)`. Basic arithmetic is implemented + assuming that `Do(a)+Undo(a)` is equivalent to not doing anything (here the + 2 `a`'s designate the same entity, not to equal objects). + +The piece of code below, which is the longer version of the code above, illustrates points 1 and 2. + +```ignore +let mut editor = TextEditor::new(); +editor.add_char('a'); // :[1] +editor.add_char('b'); // :[2] +editor.add_char('d'); // :[3] +assert_eq!(editor.text, "abd"); + +editor.undo(); // first undo :[4] +assert_eq!(editor.text, "ab"); + +editor.add_char('c'); // :[5] +assert_eq!(editor.text, "abc"); + +editor.undo(); // Undo [5] :[6] +assert_eq!(editor.text, "ab"); +editor.undo(); // Undo the undo [4] :[7] +assert_eq!(editor.text, "abd"); +editor.undo(); // Undo [3] :[8] +assert_eq!(editor.text, "ab"); +editor.undo(); // Undo [2] :[9] +assert_eq!(editor.text, "a"); + +editor.add_char('z'); // :[10] +assert_eq!(editor.text, "az"); +// when an action is performed after a sequence +// of undo, the undos are merged: undos [6] to [9] are merged now + +editor.undo(); // back to [10] +assert_eq!(editor.text, "a"); +editor.undo(); // back to [5]: reverses the consecutive sequence of undos in batch +assert_eq!(editor.text, "abc"); +editor.undo(); // back to [4] +assert_eq!(editor.text, "ab"); +editor.undo(); // back to [3] +assert_eq!(editor.text, "abd"); +editor.undo(); // back to [2] +assert_eq!(editor.text, "ab"); +editor.undo(); // back to [1] +assert_eq!(editor.text, "a"); +editor.undo(); // back to [0] +assert_eq!(editor.text, ""); + +editor.redo(); // back to [1] +assert_eq!(editor.text, "a"); +editor.redo(); // back to [2] +assert_eq!(editor.text, "ab"); +editor.redo(); // back to [3] +assert_eq!(editor.text, "abd"); +editor.redo(); // back to [4] +assert_eq!(editor.text, "ab"); +editor.redo(); // back to [5] +assert_eq!(editor.text, "abc"); +editor.redo(); // back to [9]: redo inner consecutive sequence of undos in batch + // (undo are merged only when they are not the last action) +assert_eq!(editor.text, "a"); +editor.redo(); // back to [10] +assert_eq!(editor.text, "az"); + +editor.add_char('1'); +editor.add_char('2'); +assert_eq!(editor.text, "az12"); +editor.undo(); +editor.undo(); +assert_eq!(editor.text, "az"); +editor.redo(); // undo is the last action, undo the undo only once +assert_eq!(editor.text, "az1"); +editor.redo(); +assert_eq!(editor.text, "az12"); +``` + +## Release note + +### Version 0.3 + +- [`Action`] is now an enum taking commands, the list of command to be +executed is of the form [`Action`]; +- added [`Commands::can_undo`] and [`Commands::can_redo`]; +- added [`Commands::rebuild`], which correspond to the classical redo; +- fixed a bug in [`Commands::undo_or_redo_to_index`] +- Added support for special commands that represent a state setting. See [`SetOrTransition`]. + +[zaboople]: https://github.com/zaboople/klonk/blob/master/TheGURQ.md diff --git a/crates/bevy_text/src/undo_2/lib.rs b/crates/bevy_text/src/undo_2/lib.rs new file mode 100644 index 0000000000000..ee712bdeca2cb --- /dev/null +++ b/crates/bevy_text/src/undo_2/lib.rs @@ -0,0 +1,2184 @@ +//! AA +#![doc = include_str!("./README.md")] +//! BB +// #![doc = document_features::document_features!()] +//! CC +use alloc::{slice, vec}; +use core::borrow::Borrow; +use core::cmp::min; +use core::fmt::Debug; +use core::iter::FusedIterator; +use core::ops::ControlFlow; +use core::ops::{Deref, Index}; +use core::option; +use core::slice::SliceIndex; +use std::hash::{DefaultHasher, Hash, Hasher as _}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Action that should be taken on the paired command +/// +/// [`ActionIter`] item type is a pair `(Action, C)` where +/// C is the client defined command type. `(Action::Do, t)` +/// signifies that the command `t` shall be executed. `(Action::Undo,t)` +/// means it shall be undone. +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +pub enum Action { + /// The client shall do the paired command. + Do(T), + /// The client shall undo the paired command. + Undo(T), +} + +/// The commands in that are listed in [Commands] shall +/// represent state *transition* so that this transitions can +/// be done and undone. For exemple the state transition "add the letter 'a' +/// after the cursor" can be done by adding the letter 'a' and moving the cursor left, +/// or undone by executing a backspace. +/// +/// Representing commands as *transitions* is necessary as commands executions will +/// depend of the current state of the program that is a concequence of the sequence +/// of execution of indetermined commands. +/// +/// But sometimes, the transition affects a piece of state of the program which is +/// absolutely indepent of other state of the program and of which no other command depends. Lets +/// call such state *independent state*. This could be for exemple the position of the view port of the text editor. Moving the +/// position of the view port does not affect the edition. On the other hand, the current color +/// of the pencil can not be considered an *independent state* as edition commands depend on this +/// color. +/// +/// For such *independ state* the resulting global state of the program is independent of the order +/// in which the transition to this state are applied, and it is independent of the way this +/// transition are interlieved with other transitions. +/// +/// So the set of all commands can be partitionned in two: the set of transitions affecting +/// indepent state, and the other transitions. [`SetOrTransition`] provides such a partition. +/// It is supposed to be used as the element of the [Commands]: +/// `Commands`. +/// +/// [Commands] provides dedicateds methods (see [`apply_actions`][Commands::apply_actions]) to simplify the use of +/// commands partitionned in independent state and transitions. +/// +/// State commands are supposed to represent the setting of an indepent state such as "set the view +/// port to `x`, `y`, `lx`, `ly`". When a independent state command is undone within the `apply_actions`, the +/// algorithm will look for the previous application of a command the same key [`IndepStateKey::key`] and will +/// command the application of it. +/// +/// # Example +/// +/// In this example we use commands that set the state `color` and `length`. +/// +/// In the commands registered the previous states of `color` and `length` are not stored +/// but [`Commands::apply_actions`] will retrieve automatically the value to set for this states. +/// ``` +/// use std::mem::{discriminant, Discriminant}; +/// +/// use bevy_text::undo_2::{Commands, IndepStateKey, SetOrTransition, SetTransAction}; +/// +/// #[derive(Copy, Clone, Debug)] +/// struct State { +/// color: f64, +/// length: f64, +/// } +/// impl State { +/// fn new() -> Self { +/// INIT_STATE +/// } +/// fn apply_set(&mut self, c: &SetCommands) { +/// match c { +/// SetCommands::Color(v) => self.color = *v, +/// SetCommands::Length(v) => self.length = *v, +/// }; +/// } +/// fn execute_action(&mut self, c: SetTransAction) { +/// match c { +/// SetTransAction::Do(_) => {} +/// SetTransAction::Undo(_) => {} +/// SetTransAction::Set(c) => self.apply_set(c), +/// SetTransAction::SetToInitial(d) => self.apply_set(SetCommands::new_initial(d)), +/// } +/// } +/// } +/// static INIT_STATE: State = State { +/// color: 0., +/// length: 0., +/// }; +/// +/// #[derive(Debug, Copy, Clone, PartialEq)] +/// enum SetCommands { +/// Color(f64), +/// Length(f64), +/// } +/// static INIT_COLOR: SetCommands = SetCommands::Color(INIT_STATE.color); +/// static INIT_LENGTH: SetCommands = SetCommands::Length(INIT_STATE.length); +/// +/// impl SetCommands { +/// fn new_initial(d: Discriminant) -> &'static Self { +/// if d == discriminant(&INIT_COLOR) { +/// &INIT_COLOR +/// } else if d == discriminant(&INIT_LENGTH) { +/// &INIT_LENGTH +/// } else { +/// unreachable!("SetCommands::new_initial is not exhaustive: please, adds lacking initial value to this method") +/// } +/// } +/// } +/// +/// impl IndepStateKey for SetCommands { +/// type KeyType = Discriminant; +/// +/// fn key(&self) -> Self::KeyType { +/// discriminant(self) +/// } +/// } +/// +/// #[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// enum TransitionCommand { +/// A, +/// B, +/// } +/// +/// let mut commands = Commands::new(); +/// let mut state = State::new(); +/// +/// let c = SetCommands::Color(1.); +/// state.apply_set(&c); +/// commands.push(SetOrTransition::Set(c)); +/// +/// commands.push(SetOrTransition::Transition(TransitionCommand::A)); +/// commands.push(SetOrTransition::Transition(TransitionCommand::B)); +/// +/// let c = SetCommands::Length(10.); +/// state.apply_set(&c); +/// commands.push(SetOrTransition::Set(c)); +/// +/// let c = SetCommands::Color(2.); +/// state.apply_set(&c); +/// commands.push(SetOrTransition::Set(c)); +/// +/// commands.apply_undo(|c| { +/// assert_eq!(c, SetTransAction::Set(&SetCommands::Color(1.))); +/// state.execute_action(c); +/// }); +/// assert_eq!(state.color, 1.); +/// assert_eq!(state.length, 10.); +/// commands.apply_redo(|c| { +/// assert_eq!(c, SetTransAction::Set(&SetCommands::Color(2.))); +/// state.execute_action(c); +/// }); +/// assert_eq!(state.color, 2.); +/// assert_eq!(state.length, 10.); +///``` +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum SetOrTransition { + /// A command that represent the setting of an independent state + Set(S), + /// Other commands + Transition(T), +} +/// A key discriminating the different type of +/// independent state commands. For most enum, +/// it should `KeyType=std::mem::Discriminant` +/// and the key is generated by `std::mem::discriminant(&self)` +/// +/// # Exemple +/// ```ignore +/// enum SetCommands{ +/// ViewPort{x:f64,y:f64,lx:f64,ly:f64}, +/// HideComment(bool), +/// } +/// +/// impl IndepStateKey for SetCommands { +/// type KeyType = std::mem::Discriminant +/// fn key(&self) -> Self::KeyType { +/// std::mem::discriminant(self) +/// } +/// } +/// ``` +pub trait IndepStateKey { + /// stub `KeyType` + type KeyType: PartialEq + Hash; + /// stub key + fn key(&self) -> Self::KeyType; +} + +/// The actions asked to be performed by the user +/// when calling [`Commands::apply_actions`] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum SetTransAction<'a, S: IndepStateKey, T> { + /// Ask the execution of the transition .0 + Do(&'a T), + /// Ask the reverse of the execution of the transition .0 + Undo(&'a T), + /// Ask to set state to .0 + Set(&'a S), + /// Ask to set the state corresponding to the key .0 to its initial + /// value: the value that it has before any command where applied to + /// it + SetToInitial(S::KeyType), +} + +impl Action { + /// stub `as_inner` + pub fn as_inner(&self) -> &T { + match self { + Action::Do(a) | Action::Undo(a) => a, + } + } + /// stub `as_inner_mut` + pub fn as_inner_mut(&mut self) -> &mut T { + match self { + Action::Do(a) | Action::Undo(a) => a, + } + } + /// stub `into_inner` + pub fn into_inner(self) -> T { + match self { + Action::Do(a) | Action::Undo(a) => a, + } + } +} + +/// The items stored in [Commands]. +/// +/// The list of `CommandItem` is accessible by dereferencing +/// the command list. +/// +/// *NB*: The value inside the Undo variant is the number +/// of time the undo command is repeated minus 1. +/// +/// # Example +/// +/// ``` +/// use bevy_text::undo_2::{Commands,CommandItem}; +/// +/// let mut commands = Commands::new(); +/// +/// #[derive(Debug,PartialEq)] +/// struct A; +/// +/// commands.push(A); +/// commands.undo(); +/// +/// assert_eq!(*commands, [CommandItem::Command(A),CommandItem::Undo(0)]); +/// +/// use CommandItem::Undo; +/// assert_eq!(*commands, [A.into(), Undo(0)]); +/// +/// commands.push(A); +/// commands.undo(); +/// commands.undo(); +/// assert_eq!(*commands, [A.into(), Undo(0),A.into(),Undo(1)]); +/// ``` +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum CommandItem { + /// A command typically created by [`Commands::push`](Commands#method.push) + Command(T), + /// Signify that `count` `CommandItem` previous to this item are undone. + /// + /// Where `count` refers to this variant field. + Undo(usize), +} + +/// Owns a slice of [commands](CommandItem) to undo and redo. +/// +/// Commands are added by invoking [`push`](#method.push) method. [`undo`](#method.undo) and +/// [`redo`](#method.redo) return a list of [`Action`] that the application must execute. +/// +/// To see a full functional example, read [How to use it](index.html#how-to-use-it). +/// +/// # Example +/// ``` +/// use bevy_text::undo_2::{Action, Commands}; +/// +/// #[derive(Debug, Eq, PartialEq)] +/// enum Command { +/// A, +/// B, +/// } +/// use Command::*; +/// use Action::*; +/// +/// let mut commands = Commands::new(); +/// +/// commands.push(A); +/// commands.push(B); +/// +/// let v: Vec<_> = commands.undo().collect(); +/// assert_eq!(v, [Undo(&B)]); +/// +/// let v: Vec<_> = commands.undo().collect(); +/// assert_eq!(v, [Undo(&A)]); +/// +/// commands.push(A); +/// +/// let v: Vec<_> = commands.undo().collect(); +/// assert_eq!(v, [Undo(&A)]); +/// +/// // undo the first 2 undos +/// let v: Vec<_> = commands.undo().collect(); +/// assert_eq!(v, [Do(&A), Do(&B)]); +/// ``` +/// +/// # Representation +/// +/// `Commands` owns a slice of [`CommandItem`] that is accesible +/// by dereferencing the command. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Eq)] +pub struct Commands { + commands: Vec>, + #[cfg_attr(feature = "serde", serde(skip))] + undo_cache: Vec, +} +impl Debug for Commands { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Commands") + .field("commands", &self.commands) + .finish() + } +} +impl PartialEq for Commands { + fn eq(&self, other: &Self) -> bool { + self.commands == other.commands + } +} +impl Hash for Commands { + fn hash(&self, state: &mut H) { + self.commands.hash(state); + } +} +impl Default for Commands { + fn default() -> Self { + Self { + commands: Default::default(), + undo_cache: Default::default(), + } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum CachedAction { + Do, + Undo, + Skip, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +struct IndexedAction { + action: CachedAction, + index: usize, +} + +/// Specify a merge when calling [`Commands::merge`](Commands#method.merge) +/// +/// The [`end`, `start`) bounds the slice of command that will +/// be removed during the merge. `end` and `start` are in reverse order +/// because [`IterRealized`] goes backward. +/// +/// If `command` is `None` then the slice will be removed, otherwise if +/// the `command` is `Some(c)` the slice will be replace by `c`. +#[derive(Debug)] +pub struct Merge<'a, T> { + /// start + pub start: IterRealized<'a, T>, + /// end + pub end: IterRealized<'a, T>, + /// command + pub command: Option, +} + +/// Specify a splice when calling [`Commands::splice`](Commands#method.splice) +/// +/// The [`end`, `start`) bounds the slice of command that will +/// be removed during the merge. `end` and `start` are in reverse order +/// because [`IterRealized`] goes backward. +/// +/// The removed slice is then replaced by the sequence (not reversed) of +/// commands denoted by `commands`. +#[derive(Debug)] +pub struct Splice<'a, T, I: IntoIterator> { + /// start + pub start: IterRealized<'a, T>, + /// end + pub end: IterRealized<'a, T>, + /// commands + pub commands: I, +} + +#[derive(Debug)] +/// Iterator of actions returned by [`Commands::undo`](Commands#method.undo) and +/// [`Commands::redo`](Commands#method.redo) +pub struct ActionIter<'a, T> { + commands: &'a [CommandItem], + to_do: slice::Iter<'a, IndexedAction>, +} +impl Clone for ActionIter<'_, T> { + fn clone(&self) -> Self { + Self { + commands: self.commands, + to_do: self.to_do.clone(), + } + } +} + +#[derive(Debug)] +/// The type of the iterator returned by [`Commands::iter_realized`](Commands#method.iter_realized). +pub struct IterRealized<'a, T> { + commands: &'a [CommandItem], + current: usize, +} +impl Clone for IterRealized<'_, T> { + fn clone(&self) -> Self { + Self { ..*self } + } +} + +impl Commands> { + /// Apply a series of actions represented by the type [`SetTransAction`] executed + /// by argument `f`. + /// + /// The list of action to be applied are provided by the call of `act` on + /// this [Commands]. This may be defined, for exemple as, `|c| c.redo()` + /// + /// This command only exist when the commands have the type [`SetOrTransition`]. + /// See the documentation of this type for an explanation. + /// + /// The function `f` will be called with [Do][SetTransAction::Do] and [Undo][SetTransAction::Undo] of commands that are + /// represented as [transitions][SetOrTransition::Transition], and [Set][SetTransAction::Set] or [`SetToInitial`][SetTransAction::SetToInitial] + /// (most probably only once) per [key][IndepStateKey::key] for commands that represent independent state setting. + pub fn apply_actions( + &mut self, + act: impl FnOnce(&mut Self) -> ActionIter<'_, SetOrTransition>, + mut f: impl FnMut(SetTransAction), + ) { + let mut state_commands = Vec::new(); + for command in act(self) { + Self::apply_action(command, &mut state_commands, &mut f); + } + self.restore_state(state_commands, f); + } + /// Equivalent to `apply_actions(|c| c.undo(),f)`. + pub fn apply_undo(&mut self, f: impl FnMut(SetTransAction)) { + self.apply_actions(Commands::undo, f); + } + /// Equivalent to `apply_actions(|c| c.redo(),f)`. + pub fn apply_redo(&mut self, f: impl FnMut(SetTransAction)) { + self.apply_actions(Commands::redo, f); + } + fn apply_action( + action: Action<&SetOrTransition>, + state_keys: &mut Vec>, + mut f: impl FnMut(SetTransAction), + ) { + match action { + Action::Do(SetOrTransition::Transition(tr)) => f(SetTransAction::Do(tr)), + Action::Undo(SetOrTransition::Transition(tr)) => f(SetTransAction::Undo(tr)), + Action::Do(SetOrTransition::Set(s)) | Action::Undo(SetOrTransition::Set(s)) => { + state_keys.push(Some(s.key())); + } + } + } + fn restore_state( + &self, + mut state_keys: Vec>, + mut f: impl FnMut(SetTransAction), + ) { + state_keys.sort_by_key(|v| { + let mut hasher = DefaultHasher::new(); + v.hash(&mut hasher); + hasher.finish() + }); + state_keys.dedup(); + let mut l = state_keys.len(); + if l == 0 { + return; + } + for command in self.iter_realized() { + if let SetOrTransition::Set(st) = command { + let st_key = st.key(); + if let Some(disc) = + state_keys.iter_mut().find( + |v| { + if let Some(d) = v { + *d == st_key + } else { + false + } + }, + ) + { + *disc = None; + f(SetTransAction::Set(st)); + l -= 1; + if l == 0 { + break; + } + } + } + } + if l > 0 { + for disc in state_keys.into_iter().flatten() { + f(SetTransAction::SetToInitial(disc)); + } + } + } +} + +impl Commands { + /// Create a new empty command sequence of type `T`. + pub fn new() -> Self { + Self { + commands: vec![], + undo_cache: vec![], + } + } + /// The capacity of the underlying storage + pub fn capacity(&self) -> usize { + self.commands.capacity() + } + /// Reserve space for new commands + pub fn reserve(&mut self, additional: usize) { + self.commands.reserve(additional); + } + + /// Return a reference to the last command + /// if it is not an Undo. + /// # Example + /// ``` + /// use bevy_text::undo_2::{Commands, CommandItem}; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// } + /// use Command::*; + /// + /// let mut commands = Commands::new(); + /// + /// commands.push(A); + /// + /// assert_eq!(commands.last_command().unwrap(), &A); + /// assert_eq!(commands.last().unwrap(), &CommandItem::Command(A)); + /// + /// commands.undo(); + /// assert!(commands.last_command().is_none()); + /// ``` + pub fn last_command(&self) -> Option<&T> { + self.commands.last().and_then(|v| match v { + CommandItem::Command(c) => Some(c), + CommandItem::Undo(_) => None, + }) + } + + /// Return a mutable reference to the last command + /// if it is not an Undo. + /// # Example + /// ``` + /// use bevy_text::undo_2::{Commands, CommandItem}; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// } + /// use Command::*; + /// + /// let mut commands = Commands::new(); + /// + /// commands.push(A); + /// + /// *commands.last_command_mut().unwrap() = Command::B; + /// + /// assert_eq!(commands.last_command().unwrap(), &B); + /// assert_eq!(commands.last().unwrap(), &CommandItem::Command(B)); + /// + /// commands.undo(); + /// assert!(commands.last_command_mut().is_none()); + /// ``` + pub fn last_command_mut(&mut self) -> Option<&mut T> { + self.commands.last_mut().and_then(|v| match v { + CommandItem::Command(c) => Some(c), + CommandItem::Undo(_) => None, + }) + } + /// Change in place the last command. + /// The update mail fail. + /// + /// It returns true if the update was possible and false otherwise. + /// + /// If the last command is accessible for modification, `updater` + /// will receive this last command to update this last command. If + /// updater shall return true if it modify the command and false otherwise + /// + /// # Example + /// ``` + /// use bevy_text::undo_2::{Commands, CommandItem}; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A(usize), + /// B, + /// C, + /// } + /// use Command::*; + /// + /// let mut commands = Commands::new(); + /// + /// commands.push(A(0)); + /// + /// let updater = |a:&mut Command| if let A(i) = a { + /// *i+=1; + /// true + /// } else { + /// false + /// }; + /// + /// assert_eq!(commands.last_command().unwrap(), &A(0)); + /// assert!(commands.update_last(updater)); + /// assert_eq!(commands.last_command().unwrap(), &A(1)); + /// commands.undo(); + /// assert!(!commands.update_last(updater)); + /// commands.push(B); + /// assert!(!commands.update_last(updater)); + /// ``` + pub fn update_last(&mut self, updater: impl FnOnce(&mut T) -> bool) -> bool { + if let Some(c) = self.last_command_mut() { + updater(c) + } else { + false + } + } + /// Change in place the last command or push a new command + /// + /// If the last command is accessible for modification, `updater` + /// will receive this last command to update this last command. Otherwise + /// it will receive none. + /// + /// If updater return `Some`, the value returned is then pushed in the command list. + /// + /// + /// # Example + /// ``` + /// use bevy_text::undo_2::{Commands, CommandItem}; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A(usize), + /// B, + /// C, + /// } + /// use Command::*; + /// + /// let mut commands = Commands::new(); + /// + /// commands.push(A(0)); + /// + /// let updater = |a:&mut Command| if let A(i) = a { + /// *i=10; + /// true + /// } else { + /// false + /// }; + /// let producer = || A(10); + /// + /// assert_eq!(commands.last_command().unwrap(), &A(0)); + /// commands.update_last_or_push(updater, producer); + /// assert_eq!(commands.last_command().unwrap(), &A(10)); + /// commands.undo(); + /// commands.update_last_or_push(updater, producer); + /// assert_eq!(commands.last_command().unwrap(), &A(10)); + /// commands.push(B); + /// commands.update_last_or_push(updater, producer); + /// assert_eq!(commands.last_command().unwrap(), &A(10)); + /// ``` + pub fn update_last_or_push( + &mut self, + updater: impl FnOnce(&mut T) -> bool, + producer: impl FnOnce() -> T, + ) { + if !self.update_last(updater) { + self.push(producer()); + } + } + + /// Add the command `T` + /// + /// # Example + /// ``` + /// use bevy_text::undo_2::Commands; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// } + /// use Command::*; + /// + /// let mut commands = Commands::new(); + /// + /// commands.push(A); + /// + /// let v: Vec<_> = commands.iter_realized().collect(); + /// assert_eq!(v, [&A]); + /// ``` + pub fn push(&mut self, command: T) { + self.commands.push(CommandItem::Command(command)); + } + + /// Return an iterator over a sequence of actions to be performed by the client application to + /// undo. + /// + /// # Example + /// ``` + /// use bevy_text::undo_2::{Action,Commands}; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// C + /// } + /// use Command::*; + /// use Action::*; + /// + /// let mut commands = Commands::new(); + /// + /// commands.push(A); + /// commands.push(B); + /// + /// let v: Vec<_> = commands.undo().collect(); + /// assert_eq!(v, [Undo(&B)]); + /// + /// commands.push(C); + /// + /// let v: Vec<_> = commands.undo().collect(); + /// assert_eq!(v, [Undo(&C)]); + /// + /// let v: Vec<_> = commands.undo().collect(); + /// assert_eq!(v, [Do(&B)]); + /// + /// let v: Vec<_> = commands.undo().collect(); + /// assert_eq!(v, [Undo(&B)]); + /// + /// let v: Vec<_> = commands.undo().collect(); + /// assert_eq!(v, [Undo(&A)]); + /// + /// let v: Vec<_> = commands.undo().collect(); + /// assert!(v.is_empty()) + /// ``` + #[must_use = "the returned actions should be realized"] + pub fn undo(&mut self) -> ActionIter<'_, T> { + self.undo_repeat(1) + } + /// Return an iterator over a sequence of actions to be performed by the client application to + /// undo `repeat` time. + /// + /// `undo_repeat(1)` is equivalent to `undo()` + /// + /// # Example + /// ``` + /// use bevy_text::undo_2::{Action,Commands}; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// C + /// } + /// use Command::*; + /// use Action::*; + /// + /// let mut commands = Commands::new(); + /// + /// commands.push(A); + /// commands.push(B); + /// + /// let v: Vec<_> = commands.undo().collect(); + /// assert_eq!(v, [Undo(&B)]); + /// + /// commands.push(C); + /// + /// let v: Vec<_> = commands.undo_repeat(4).collect(); + /// assert_eq!(v, [Undo(&C), Undo(&A)]); + /// ``` + #[must_use = "the returned actions should be realized"] + pub fn undo_repeat(&mut self, repeat: usize) -> ActionIter<'_, T> { + let Some(repeat) = repeat.checked_sub(1) else { + return ActionIter::new(); + }; + let l = self.len(); + match self.commands.last_mut() { + Some(CommandItem::Command(_)) => { + let count = min(repeat, l - 1); + self.commands.push(CommandItem::Undo(count)); + ActionIter::undo_at_count( + &self.commands, + &mut self.undo_cache, + l - 1 - count, + count, + ) + } + Some(CommandItem::Undo(i)) => { + if *i + 2 < l { + let count = min(l - *i - 3, repeat); + *i = *i + 1 + count; + let t = l - *i - 2; + ActionIter::undo_at_count(&self.commands, &mut self.undo_cache, t, count) + } else { + ActionIter::new() + } + } + None => ActionIter::new(), + } + } + /// An undo that skip undo branches. + /// + /// It returns the command that must be undone. + /// + /// It is equivalent to multiple successive call to `undo`. It behaves + /// as a classical undo. + /// + /// # Example + /// ``` + /// use bevy_text::undo_2::{Commands,Action}; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// C, + /// } + /// use Command::*; + /// use Action::*; + /// + /// let mut commands = Commands::new(); + /// + /// commands.push(A); + /// commands.push(B); + /// commands.undo(); + /// commands.push(C); + /// + /// let c: Vec<_> = commands.unbuild().collect(); + /// assert_eq!(c, &[Undo(&C)]); + /// + /// let c: Vec<_> = commands.unbuild().collect(); + /// assert_eq!(c, &[Undo(&A)]); + /// + /// let c: Vec<_> = commands.unbuild().collect(); + /// assert!(c.is_empty()); + /// ``` + #[must_use = "the returned command should be undone"] + pub fn unbuild(&mut self) -> ActionIter<'_, T> { + let mut it = self.iter_realized(); + if it.next().is_none() { + return ActionIter::new(); + } + let start = it.index(); + let to_undo = if it.next().is_some() { + start - it.index() + } else { + start + 1 + }; + self.undo_repeat(to_undo) + } + /// rebuild + #[must_use = "the returned command should be undone"] + pub fn rebuild(&mut self) -> ActionIter<'_, T> { + if !self.is_undoing() { + return ActionIter::new(); + } + let l = self.commands.len(); + let mut it = IterRealized { + commands: &self.commands[..l - 1], + current: l - 1, + }; + + let mut prev_i = l - 1; + match self.current_command_index() { + Some(cur_i) => { + while it.next().is_some() { + let n_i = it.index(); + if n_i <= cur_i { + break; + } + prev_i = n_i; + } + } + None => { + while it.next().is_some() { + prev_i = it.index(); + } + } + } + self.undo_or_redo_to_index(prev_i) + } + /// Return an iterator over a sequence of actions to be performed by the client application to + /// redo. + /// + /// # Example + /// ``` + /// use bevy_text::undo_2::{Action,Commands}; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// } + /// use Command::*; + /// use Action::*; + /// + /// let mut commands = Commands::new(); + /// + /// commands.push(A); + /// commands.push(B); + /// commands.undo(); + /// let v: Vec<_> = commands.redo().collect(); + /// + /// assert_eq!(v, [Do(&B)]); + /// ``` + #[must_use = "the returned actions should be realized"] + pub fn redo(&mut self) -> ActionIter<'_, T> { + self.redo_repeat(1) + } + /// Return an iterator over a sequence of actions to be performed by the client application to + /// redo `repeat` time. + /// + /// # Example + /// ``` + /// use bevy_text::undo_2::{Action,Commands}; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// } + /// use Command::*; + /// use Action::*; + /// + /// let mut commands = Commands::new(); + /// + /// commands.push(A); + /// commands.push(B); + /// commands.undo(); + /// commands.undo(); + /// let v: Vec<_> = commands.redo_repeat(2).collect(); + /// + /// assert_eq!(v, [Do(&A),Do(&B)]); + /// ``` + #[must_use = "the returned actions should be realized"] + pub fn redo_repeat(&mut self, repeat: usize) -> ActionIter<'_, T> { + let Some(repeat) = repeat.checked_sub(1) else { + return ActionIter::new(); + }; + let l = self.len(); + match self.commands.last_mut() { + Some(CommandItem::Undo(i)) => { + if let Some(ni) = i.checked_sub(repeat.checked_add(1).unwrap()) { + let t = l - 2 - *i; + *i = ni; + ActionIter::do_at_count(&self.commands, &mut self.undo_cache, t, repeat) + } else { + let count = *i; + self.commands.pop(); + ActionIter::do_at_count(&self.commands, &mut self.undo_cache, l - 2, count) + } + } + _ => ActionIter::new(), + } + } + /// Return an iterator over a sequence of actions to be performed by the client application to + /// undo all commands (to return to the initial state). + /// + /// # Example + /// ``` + /// use bevy_text::undo_2::{Action,Commands}; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// C, + /// } + /// use Command::*; + /// use Action::*; + /// + /// let mut commands = Commands::new(); + /// + /// commands.push(A); + /// commands.push(B); + /// + /// let v: Vec<_> = commands.undo_all().collect(); + /// assert_eq!(v, [Undo(&B), Undo(&A)]); + /// ``` + pub fn undo_all(&mut self) -> ActionIter<'_, T> { + use CommandItem::*; + let j = match self.last() { + None => return ActionIter::new(), + Some(Command(_)) => self.len(), + Some(Undo(i)) => self.len() - 2 - i, + }; + self.undo_repeat(j) + } + /// Return an iterator over a sequence of actions to be performed by the client application to + /// redo all undo. + /// + /// # Example + /// ``` + /// use bevy_text::undo_2::{Action,Commands}; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// C, + /// } + /// use Command::*; + /// use Action::*; + /// + /// let mut commands = Commands::new(); + /// + /// commands.push(A); + /// commands.push(B); + /// commands.undo(); + /// commands.undo(); + /// + /// let v: Vec<_> = commands.redo_all().collect(); + /// assert_eq!(v, [Do(&A), Do(&B)]); + /// ``` + #[must_use = "the returned actions should be realized"] + pub fn redo_all(&mut self) -> ActionIter<'_, T> { + let l = self.len(); + match self.commands.last_mut() { + Some(CommandItem::Undo(i)) => { + let count = *i; + self.commands.pop(); + ActionIter::do_at_count(&self.commands, &mut self.undo_cache, l - 2, count) + } + _ => ActionIter::new(), + } + } + /// Return `true` if the last action is an undo. + /// + /// # Example + /// ``` + /// use bevy_text::undo_2::Commands; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// } + /// use Command::*; + /// + /// let mut commands = Commands::new(); + /// assert!(!commands.is_undoing()); + /// + /// commands.push(A); + /// assert!(!commands.is_undoing()); + /// + /// commands.undo(); + /// assert!(commands.is_undoing()); + /// + /// commands.push(A); + /// commands.push(A); + /// commands.undo(); + /// commands.undo(); + /// assert!(commands.is_undoing()); + /// commands.redo(); + /// assert!(commands.is_undoing()); + /// commands.redo(); + /// assert!(!commands.is_undoing()); + /// ``` + pub fn is_undoing(&self) -> bool { + matches!(self.commands.last(), Some(CommandItem::Undo(_))) + } + + /// Check weither there are still command that can be undone + /// + /// # Example + /// ``` + /// use bevy_text::undo_2::Commands; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// } + /// use Command::*; + /// + /// let mut commands = Commands::new(); + /// assert!(!commands.can_undo()); + /// + /// commands.push(A); + /// assert!(commands.can_undo()); + /// + /// commands.undo(); + /// assert!(!commands.can_undo()); + /// ``` + pub fn can_undo(&self) -> bool { + match self.commands.last() { + None => false, + Some(CommandItem::Command(_)) => true, + Some(CommandItem::Undo(i)) => i + 2 < self.commands.len(), + } + } + /// Check weither there are still command that can be redone + /// + /// # Example + /// ``` + /// use bevy_text::undo_2::Commands; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// } + /// use Command::*; + /// + /// let mut commands = Commands::new(); + /// assert!(!commands.can_redo()); + /// + /// commands.push(A); + /// assert!(!commands.can_redo()); + /// + /// commands.undo(); + /// assert!(commands.can_redo()); + /// ``` + pub fn can_redo(&self) -> bool { + self.is_undoing() + } + + /// Return the index of the first realized [command item](CommandItem). + /// + /// # Example + /// ``` + /// use bevy_text::undo_2::*; + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// } + /// use Command::*; + /// + /// let mut c = Commands::new(); + /// + /// c.push(A); + /// c.push(B); + /// + /// let v: Vec<_> = c.iter().collect(); + /// assert_eq!(v, [&CommandItem::Command(A), &CommandItem::Command(B)]); + /// + /// assert_eq!(c.current_command_index(), Some(1)); + /// + /// c.undo(); + /// + /// let v: Vec<_> = c.iter().collect(); + /// assert_eq!(v, [&CommandItem::Command(A), &CommandItem::Command(B), &CommandItem::Undo(0)]); + /// + /// assert_eq!(c.current_command_index(), Some(0)); + /// ``` + pub fn current_command_index(&self) -> Option { + let mut it = self.iter_realized(); + it.next()?; + Some(it.current) + } + + /// Repeat undo or redo so that the last realiazed command correspond to + /// the [`CommandItem`] index passed `index`. + /// + /// ``` + /// use bevy_text::undo_2::{Action,Commands, CommandItem}; + /// use std::time::{Instant, Duration}; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// C, + /// D, + /// E, + /// } + /// use Command::*; + /// use Action::*; + /// + /// let mut commands = Commands::new(); + /// + /// let t0 = Instant::now(); + /// let t1 = t0 + Duration::from_secs(1); + /// let t2 = t0 + Duration::from_secs(2); + /// let t3 = t0 + Duration::from_secs(3); + /// let t4 = t0 + Duration::from_secs(4); + /// commands.push((t0,A)); + /// commands.push((t1,B)); + /// commands.undo(); + /// commands.push((t2,C)); + /// commands.push((t3,D)); + /// commands.undo(); + /// commands.undo(); + /// commands.push((t4,E)); + /// + /// + /// let v: Vec<_> = commands.iter_realized().collect(); + /// assert_eq!(v, [&(t4,E),&(t0,A)]); + /// + /// let index = commands.iter().position(|item| match item { + /// CommandItem::Command(item) => item.0 == t2, + /// _ => false + /// }).unwrap(); + /// + /// let actions = commands.undo_or_redo_to_index(index); + /// let a: Vec<_> = actions.collect(); + /// assert_eq!(a, [Undo(&(t4,E)), Do(&(t2,C))]); + /// + /// let v: Vec<_> = commands.iter_realized().collect(); + /// assert_eq!(v, [&(t2,C),&(t0,A)]); + /// ``` + pub fn undo_or_redo_to_index(&mut self, i: usize) -> ActionIter<'_, T> { + use CommandItem::*; + let j = match self.last() { + None => return ActionIter::new(), + Some(Command(_)) => self.len(), + Some(Undo(i)) => self.len() - 2 - i, + }; + if i >= j { + self.redo_repeat(i + 1 - j) + } else { + self.undo_repeat(j - i - 1) + } + } + /// Clear all the commands. + /// + /// # Example + /// + /// ``` + /// use bevy_text::undo_2::Commands; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// } + /// use Command::*; + /// + /// let mut commands = Commands::new(); + /// + /// commands.push(A); + /// commands.push(B); + /// + /// commands.clear(); + /// + /// let v: Vec<_> = commands.iter_realized().collect(); + /// assert_eq!(v.len(), 0); + /// ``` + pub fn clear(&mut self) { + self.commands.clear(); + } + /// Remove all removable commands that have been added before the + /// first item fulfilling the predicate. + /// + /// A command is removable if it was added before the predicate fulfilling item + /// and is not covered by any undo. + /// + /// Complexity: O(n) + /// + /// ``` + /// use bevy_text::undo_2::Commands; + /// use std::time::{Instant, Duration}; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// C, + /// D, + /// E, + /// } + /// use Command::*; + /// + /// let mut commands = Commands::new(); + /// + /// let t0 = Instant::now(); + /// let t1 = t0 + Duration::from_secs(1); + /// let t2 = t0 + Duration::from_secs(2); + /// let t3 = t0 + Duration::from_secs(3); + /// let t4 = t0 + Duration::from_secs(4); + /// commands.push((t0,A)); + /// commands.push((t1,B)); + /// commands.push((t2,C)); + /// commands.push((t3,D)); + /// commands.push((t4,E)); + /// + /// commands.remove_until(|(t, _)| *t > t1); + /// + /// let v: Vec<_> = commands.iter_realized().collect(); + /// assert_eq!(v, [&(t4,E),&(t3,D), &(t2,C)]); + /// + /// + /// let mut commands = Commands::new(); + /// + /// commands.push((t0,A)); + /// commands.push((t1,B)); //B + /// commands.push((t2,C)); + /// commands.undo(); + /// commands.undo();// undo covering B + /// commands.push((t3,D)); + /// commands.push((t4,E)); + /// + /// commands.remove_until(|(t, _)| *t > t1); + /// + /// commands.undo();//remove E + /// commands.undo();//remove D + /// commands.undo();//undo the 2 undos + /// + /// // B not removed because it is covered by an undo + /// let v: Vec<_> = commands.iter_realized().collect(); + /// assert_eq!(v, [&(t2,C),&(t1,B)]); + /// + /// ``` + pub fn remove_until(&mut self, mut stop_pred: impl FnMut(&T) -> bool) { + if let Some(i) = self.commands.iter().position(move |c| match c { + CommandItem::Undo(_) => false, + CommandItem::Command(c) => stop_pred(c), + }) { + self.remove_first(i); + } + } + /// Try to keep `count` most recent commands by dropping removable commands. + /// + /// A command is removable if it was added before the 'count' last commands + /// and is not covered by any undo. + /// + /// Complexity: O(n) + /// + /// ``` + /// use bevy_text::undo_2::Commands; + /// use std::time::{Instant, Duration}; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// C, + /// D, + /// E, + /// } + /// use Command::*; + /// + /// let mut commands = Commands::new(); + /// + /// let t0 = Instant::now(); + /// let t1 = t0 + Duration::from_secs(1); + /// let t2 = t0 + Duration::from_secs(2); + /// let t3 = t0 + Duration::from_secs(3); + /// let t4 = t0 + Duration::from_secs(4); + /// commands.push((t0,A)); + /// commands.push((t1,B)); + /// commands.push((t2,C)); + /// commands.push((t3,D)); + /// commands.push((t4,E)); + /// + /// commands.keep_last(2); + /// + /// let v: Vec<_> = commands.iter_realized().collect(); + /// assert_eq!(v, [&(t4,E),&(t3,D)]); + /// + /// + /// let mut commands = Commands::new(); + /// + /// commands.push((t0,A)); + /// commands.push((t1,B)); //B + /// commands.push((t2,C)); + /// commands.undo(); + /// commands.undo();// undo covering B + /// commands.push((t3,D)); + /// commands.push((t4,E)); + /// + /// // sequence of undo count for 1 + /// // so try to remove A and B + /// commands.keep_last(4); + /// + /// commands.undo();//remove E + /// commands.undo();//remove D + /// commands.undo();//undo the 2 undos + /// + /// // B not removed because it is covered by an undo + /// let v: Vec<_> = commands.iter_realized().collect(); + /// assert_eq!(v, [&(t2,C),&(t1,B)]); + /// ``` + pub fn keep_last(&mut self, count: usize) { + let i = self.len().saturating_sub(count); + self.remove_first(i); + } + /// Remove `count` or less of the oldest command. + /// + /// Less commands may be dropped to ensure that none of the dropped + /// command is covered by an undo within the recent non dropped commands. + /// + /// Complexity: O(n) + /// + /// ``` + /// use bevy_text::undo_2::Commands; + /// use std::time::{Instant, Duration}; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// C, + /// D, + /// E, + /// } + /// use Command::*; + /// + /// let mut commands = Commands::new(); + /// + /// let t0 = Instant::now(); + /// let t1 = t0 + Duration::from_secs(1); + /// let t2 = t0 + Duration::from_secs(2); + /// let t3 = t0 + Duration::from_secs(3); + /// let t4 = t0 + Duration::from_secs(4); + /// commands.push((t0,A)); + /// commands.push((t1,B)); + /// commands.push((t2,C)); + /// commands.push((t3,D)); + /// commands.push((t4,E)); + /// + /// commands.remove_first(3); + /// + /// let v: Vec<_> = commands.iter_realized().collect(); + /// assert_eq!(v, [&(t4,E),&(t3,D)]); + /// + /// + /// let mut commands = Commands::new(); + /// + /// commands.push((t0,A)); + /// commands.push((t1,B)); //B + /// commands.push((t2,C)); + /// commands.undo(); + /// commands.undo();// undo covering B + /// commands.push((t3,D)); + /// commands.push((t4,E)); + /// + /// // sequence of undo count for 1 + /// // so try to remove A and B + /// commands.remove_first(2); + /// + /// commands.undo();//remove E + /// commands.undo();//remove D + /// commands.undo();//undo the 2 undos + /// + /// // B not removed because it is covered by an undo + /// let v: Vec<_> = commands.iter_realized().collect(); + /// assert_eq!(v, [&(t2,C),&(t1,B)]); + /// ``` + /// + /// # Panic + /// + /// Panics if `i` is greater than `self.len()` + pub fn remove_first(&mut self, i: usize) { + let j = self.remove_i(i, self.commands.len()); + self.commands.drain(..j); + } + fn remove_i(&self, mut i: usize, end: usize) -> usize { + let i0 = i; + for j in i0..end { + if let CommandItem::Undo(count) = self.commands[j] + && j - count - 1 < i + { + i = self.remove_i(j - count - 1, i); + } + } + i + } + /// Iterate the sequence of [*realized commands*](Commands#method.iter_realized) from the + /// newest to the oldest. + /// + /// *Realized commands* are commands that are not undone. For example assuming + /// the folowing sequence of commands: + /// + /// | Command | State | + /// |---------|-------| + /// | Init | | + /// | Do A | A | + /// | Do B | A, B | + /// | Undo | A | + /// | Do C | A, C | + /// + /// The iterator would iterator over the sequence [C, A]. + /// + /// ``` + /// use bevy_text::undo_2::Commands; + /// + /// #[derive(Debug, Eq, PartialEq)] + /// enum Command { + /// A, + /// B, + /// C, + /// D, + /// E, + /// } + /// use Command::*; + /// + /// let mut commands = Commands::new(); + /// + /// commands.push(A); + /// commands.push(B); + /// commands.push(C); + /// commands.undo(); + /// commands.undo(); + /// commands.push(D); + /// commands.push(E); + /// + /// let v: Vec<_> = commands.iter_realized().collect(); + /// assert_eq!(v, [&E,&D, &A]); + /// ``` + pub fn iter_realized(&self) -> IterRealized<'_, T> { + IterRealized { + commands: &self.commands, + current: self.commands.len(), + } + } + /// Merge a sequence of [*realized commands*](Commands#method.iter_realized) into a single new + /// command or remove the sequence. + /// + /// The parameter `f` takes as an input a [`IterRealized`], and returns a + /// [`std::ops::ControlFlow, Option>`](std::ops::ControlFlow). If the + /// returned value contain a `Some(merge)`[Merge], the action specified by `merge` is then + /// inserted in place. + /// + /// The function is excuted recursively while it returns a `ControlFlow::Continue(_)` with a + /// [realized iterator](Commands#method.iter_realized) that is advanced by 1 if no merge + /// command is returned, or set to the element previous to the last merge command. + /// + /// The execution stops when the functions either returned `ControlFlow::Break` or after the + /// last element in the iteration. + /// + /// *Remember*: the element are iterated from the newest to the oldest (in reverse order). + /// + /// # Example + /// + /// ``` + /// use bevy_text::undo_2::{Commands, CommandItem, Merge, IterRealized}; + /// use std::ops::ControlFlow; + /// + /// #[derive(Eq, PartialEq, Debug)] + /// enum Command { + /// A, + /// B, + /// AB, + /// } + /// + /// use Command::*; + /// + /// fn is_ab<'a>(mut it: IterRealized<'a, Command>) -> (bool, IterRealized<'a, Command>) { + /// let cond = it.next() == Some(&B) && it.next() == Some(&A); + /// (cond, it) + /// } + /// + /// + /// let mut commands = Commands::new(); + /// commands.push(A); + /// commands.push(B); + /// + /// commands.merge(|start| { + /// if let (true, end) = is_ab(start.clone()) { + /// ControlFlow::Continue(Some(Merge { + /// start, + /// end, + /// command: Some(AB), + /// })) + /// } else { + /// ControlFlow::Continue(None) + /// } + /// }); + /// + /// assert_eq!(&*commands, &[AB.into()]); + /// ``` + pub fn merge(&mut self, mut f: F) + where + for<'a> F: + FnMut(IterRealized<'a, T>) -> ControlFlow>, Option>>, + { + use ControlFlow::*; + self.splice(|it| match f(it) { + Continue(c) => Continue(c.map(Into::into)), + Break(c) => Break(c.map(Into::into)), + }); + } + + /// Replace a sequence of command by an other. This is a generalization of + /// [`Commands::merge`](Commands#method.merge) + /// + /// The parameter `f` takes as an input a [`IterRealized`], and returns a + /// [`std::ops::ControlFlow, Option>`](std::ops::ControlFlow). If the returned value + /// contain a `Some(splice)`[Splice], the actions specified by `splice` are then inserted in + /// place. + /// + /// The function is excuted recursively while it returns a `ControlFlow::Continue(_)` with a + /// [realized iterator](Commands#method.iter_realized) that is advanced by 1 if no merge + /// command is returned, or set to the element previous to the last merge command. + /// + /// The execution stops when the functions either returned `ControlFlow::Break` or after the + /// last element in the iteration. + /// + /// *Remember*: the element are iterated from the newest to the oldest (in reverse order). + /// + /// # Example + /// + /// ``` + /// use bevy_text::undo_2::{Commands, CommandItem, Splice, IterRealized}; + /// use std::ops::ControlFlow; + /// + /// // we suppose that A, B, C is equivalent to D,E + /// #[derive(Eq, PartialEq, Debug)] + /// enum Command { + /// A, + /// B, + /// C, + /// D, + /// E, + /// } + /// + /// use Command::*; + /// + /// fn is_abc<'a>(mut it: IterRealized<'a, Command>) -> (bool, IterRealized<'a, Command>) { + /// let cond = it.next() == Some(&C) && it.next() == Some(&B) && it.next() == Some(&A); + /// (cond, it) + /// } + /// + /// + /// let mut commands = Commands::new(); + /// commands.push(A); + /// commands.push(B); + /// commands.push(C); + /// + /// commands.splice(|start| { + /// if let (true, end) = is_abc(start.clone()) { + /// ControlFlow::Continue(Some(Splice { + /// start, + /// end, + /// commands: [D,E], + /// })) + /// } else { + /// ControlFlow::Continue(None) + /// } + /// }); + /// + /// assert_eq!(&*commands, &[D.into(), E.into()]); + /// ``` + pub fn splice(&mut self, mut f: F) + where + F: for<'a> FnMut( + IterRealized<'a, T>, + ) + -> ControlFlow>, Option>>, + I: IntoIterator, + { + use ControlFlow::*; + let mut start = self.commands.len(); + while start != 0 { + let it = IterRealized { + commands: &self.commands, + current: start, + }; + match f(it) { + Continue(Some(m)) => { + let rev_start = m.start.current; + let rev_end = m.end.current; + let commands = m.commands; + self.do_splice(rev_start, rev_end, commands); + start = rev_end; + } + Break(Some(m)) => { + let rev_start = m.start.current; + let rev_end = m.end.current; + let commands = m.commands; + self.do_splice(rev_start, rev_end, commands); + break; + } + Break(None) => break, + Continue(None) => start -= 1, + } + } + } + fn do_splice(&mut self, rev_start: usize, rev_end: usize, commands: I) + where + I: IntoIterator, + { + let end_i = rev_start; + let start_i = rev_end; + self.commands + .splice(start_i..end_i, commands.into_iter().map(Into::into)); + } + + /// Clean up the history of all the undone commands. + /// + /// After this call the sequence of command will not contain + /// any `CommandItem::Undo` + /// + /// # Example + /// ``` + /// use bevy_text::undo_2::{CommandItem, Commands}; + /// + /// #[derive(Debug, PartialEq)] + /// enum Command { + /// A, + /// B, + /// C, + /// } + /// use Command::*; + /// let mut c = Commands::default(); + /// + /// c.push(A); + /// c.push(B); + /// c.undo(); + /// c.push(C); + /// assert_eq!(*c, [A.into(), B.into(), CommandItem::Undo(0), C.into()]); + /// + /// c.remove_all_undone(); + /// assert_eq!(*c, [A.into(), C.into()]); + /// ``` + pub fn remove_all_undone(&mut self) { + self.remove_undone(|i| i); + } + /// Clean up the history of all undone commands before a given + /// [realized iterator](Commands#method.iter_realized). + /// + /// # Example + /// ``` + /// use bevy_text::undo_2::{Commands,CommandItem}; + /// + /// #[derive(Debug, PartialEq)] + /// enum Command { + /// A, + /// B, + /// C, + /// D, + /// } + /// use Command::*; + /// let mut c = Commands::default(); + /// + /// c.push(A); + /// c.push(B); + /// c.undo(); + /// c.push(C); + /// c.push(C); + /// c.undo(); + /// c.push(D); + /// assert_eq!( + /// *c, + /// [ + /// A.into(), + /// B.into(), + /// CommandItem::Undo(0), + /// C.into(), + /// C.into(), + /// CommandItem::Undo(0), + /// D.into() + /// ] + /// ); + /// + /// let v: Vec<_> = c.iter_realized().collect(); + /// assert_eq!(*v, [&D, &C, &A]); + /// + /// c.remove_undone(|mut it| { + /// it.nth(1); + /// it + /// }); + /// assert_eq!( + /// *c, + /// [A.into(), C.into(), C.into(), CommandItem::Undo(0), D.into()] + /// ); + /// + /// // This operation does not change the sequence of realized commands: + /// let v: Vec<_> = c.iter_realized().collect(); + /// assert_eq!(*v, [&D, &C, &A]); + /// ``` + pub fn remove_undone(&mut self, from: F) + where + F: for<'a> FnOnce(IterRealized<'a, T>) -> IterRealized<'a, T>, + { + use CachedAction::*; + let from = from(self.iter_realized()); + + let mut it = IterRealized { + commands: &self.commands, + ..from + }; + + self.undo_cache.clear(); + + let start = it.current; + while let Some(_) = it.next() { + self.undo_cache.push(IndexedAction { + action: Do, + index: it.current, + }); + } + + let mut i = 0; + let mut shift = 0; + for u in self.undo_cache.iter().rev() { + let j = u.index; + self.commands.drain(i - shift..j - shift); + shift += j - i; + i = j + 1; + } + self.commands.drain(i - shift..start - shift); + } +} + +impl Clone for Commands { + fn clone(&self) -> Self { + Self { + commands: self.commands.clone(), + undo_cache: vec![], + } + } + fn clone_from(&mut self, source: &Self) { + self.commands.clone_from(&source.commands); + } +} + +impl Deref for Commands { + type Target = [CommandItem]; + fn deref(&self) -> &Self::Target { + &self.commands + } +} +impl AsRef<[CommandItem]> for Commands { + fn as_ref(&self) -> &[CommandItem] { + self + } +} +impl Borrow<[CommandItem]> for Commands { + fn borrow(&self) -> &[CommandItem] { + self + } +} +impl Extend for Commands { + fn extend>(&mut self, iter: I) { + self.commands.extend(iter.into_iter().map(Into::into)); + } +} +impl FromIterator for Commands { + fn from_iter>(iter: I) -> Self { + Self { + commands: iter.into_iter().map(Into::into).collect(), + undo_cache: vec![], + } + } +} +impl<'a, T> IntoIterator for &'a Commands { + type Item = &'a CommandItem; + type IntoIter = slice::Iter<'a, CommandItem>; + fn into_iter(self) -> Self::IntoIter { + self.commands.iter() + } +} +impl IntoIterator for Commands { + type Item = CommandItem; + type IntoIter = vec::IntoIter>; + fn into_iter(self) -> Self::IntoIter { + self.commands.into_iter() + } +} +impl Index for Commands +where + I: SliceIndex<[CommandItem]>, +{ + type Output = ]>>::Output; + fn index(&self, index: I) -> &Self::Output { + self.commands.index(index) + } +} + +impl From for CommandItem { + fn from(value: T) -> Self { + CommandItem::Command(value) + } +} + +impl<'a, T> From> for Splice<'a, T, option::IntoIter> { + fn from(m: Merge<'a, T>) -> Self { + Splice { + start: m.start, + end: m.end, + commands: m.command.into_iter(), + } + } +} + +impl IterRealized<'_, T> { + /// Returned the index of the command refered by the previous non `None` result of call to + /// `next`. + /// + /// This same command is accessible by indexing [Commands] at this returned index. + /// + /// This index can be used to set the first realized command with + /// [`Commands::undo_or_redo_to_index`](Commands#method.undo_or_redo_to_index). + pub fn index(&self) -> usize { + self.current + } +} + +impl<'a, T> Iterator for IterRealized<'a, T> { + type Item = &'a T; + fn next(&mut self) -> Option { + use CommandItem::*; + loop { + self.current = self.current.checked_sub(1)?; + match self.commands[self.current] { + Command(ref c) => break Some(c), + Undo(i) => self.current -= i + 1, + } + } + } + fn size_hint(&self) -> (usize, Option) { + (0, Some(self.current)) + } +} +impl FusedIterator for IterRealized<'_, T> {} + +impl<'a, T> Iterator for ActionIter<'a, T> { + type Item = Action<&'a T>; + fn next(&mut self) -> Option { + use CachedAction::*; + loop { + let a = self.to_do.next()?; + match a { + IndexedAction { action: Do, index } => { + break if let CommandItem::Command(v) = &self.commands[*index] { + Some(Action::Do(v)) + } else { + unreachable!() + } + } + IndexedAction { + action: Undo, + index, + } => { + break if let CommandItem::Command(v) = &self.commands[*index] { + Some(Action::Undo(v)) + } else { + unreachable!() + } + } + IndexedAction { action: Skip, .. } => (), + } + } + } + fn size_hint(&self) -> (usize, Option) { + self.to_do.size_hint() + } +} +impl FusedIterator for ActionIter<'_, T> {} +impl ExactSizeIterator for ActionIter<'_, T> {} + +impl IndexedAction { + fn is_reverse_of(&self, other: &Self) -> bool { + use CachedAction::*; + self.index == other.index + && (self.action == Do && other.action == Undo + || self.action == Undo && other.action == Do) + } +} + +impl<'a, T> ActionIter<'a, T> { + fn new() -> Self { + Self { + commands: &[], + to_do: [].iter(), + } + } + fn undo_at_count( + commands: &'a [CommandItem], + cache: &'a mut Vec, + i: usize, + count: usize, + ) -> Self { + cache.clear(); + cache_undo_indexes(commands, i + 1 + count, i, cache); + do_simplify(cache); + Self { + commands, + to_do: cache.iter(), + } + } + fn do_at_count( + commands: &'a [CommandItem], + cache: &'a mut Vec, + i: usize, + count: usize, + ) -> Self { + cache.clear(); + cache_do_indexes(commands, i - count, i + 1, cache); + do_simplify(cache); + Self { + commands, + to_do: cache.iter(), + } + } +} +fn cache_undo_indexes( + commands: &[CommandItem], + undo_from: usize, + undo_to: usize, + to_do: &mut Vec, +) { + use CachedAction::*; + for i in (undo_to..undo_from).rev() { + match commands[i] { + CommandItem::Command(_) => to_do.push(IndexedAction { + action: Undo, + index: i, + }), + CommandItem::Undo(count) => cache_do_indexes(commands, i - (count + 1), i, to_do), + } + } +} +fn cache_do_indexes( + commands: &[CommandItem], + do_from: usize, + do_to: usize, + to_do: &mut Vec, +) { + use CachedAction::*; + for i in do_from..do_to { + match commands[i] { + CommandItem::Command(_) => to_do.push(IndexedAction { + action: Do, + index: i, + }), + CommandItem::Undo(count) => cache_undo_indexes(commands, i, i - (count + 1), to_do), + } + } +} +fn do_simplify(to_do: &mut [IndexedAction]) { + use CachedAction::*; + if to_do.len() < 2 { + return; + } + let mut analyzed = to_do.len() - 1; + let mut cursor = to_do.len() - 1; + while analyzed > 0 { + analyzed -= 1; + let action = &to_do[analyzed]; + let l = to_do.len(); + if cursor < to_do.len() { + if to_do[cursor].is_reverse_of(action) { + cursor += 1; + while cursor < l && to_do[cursor].action == Skip { + cursor += 1; + } + } else { + to_do[analyzed + 1..cursor] + .iter_mut() + .for_each(|a| a.action = Skip); + if cursor == analyzed + 1 { + cursor = analyzed; + } else { + cursor = analyzed + 1; + analyzed += 1; + } + } + } else { + to_do[analyzed + 1..] + .iter_mut() + .for_each(|a| a.action = Skip); + cursor = analyzed; + } + } + to_do[..cursor].iter_mut().for_each(|a| a.action = Skip); +} + +#[cfg(test)] +mod test { + use super::CachedAction::*; + use super::IndexedAction; + #[test] + fn simplify() { + use super::do_simplify; + fn simplify(mut to_do: Vec) -> Vec { + do_simplify(&mut to_do); + to_do.iter().filter(|c| c.action != Skip).copied().collect() + } + fn _do(i: usize) -> IndexedAction { + IndexedAction { + action: Do, + index: i, + } + } + fn undo(i: usize) -> IndexedAction { + IndexedAction { + action: Undo, + index: i, + } + } + { + let v = vec![]; + assert_eq!(simplify(v), vec![]); + } + { + let v = vec![_do(1)]; + assert_eq!(simplify(v), vec![_do(1)]); + } + { + let v = vec![undo(1)]; + assert_eq!(simplify(v), vec![undo(1)]); + } + { + let v = vec![_do(1), undo(1)]; + assert_eq!(simplify(v), vec![]); + } + { + let v = vec![_do(0), _do(1), undo(1)]; + assert_eq!(simplify(v), vec![_do(0)]); + } + { + let v = vec![_do(1), undo(1), _do(2)]; + assert_eq!(simplify(v), vec![_do(2)]); + } + { + let v = vec![_do(0), _do(1), undo(1), _do(2)]; + assert_eq!(simplify(v), vec![_do(0), _do(2)]); + } + { + let v = vec![_do(1), _do(2), undo(2), undo(1)]; + assert_eq!(simplify(v), vec![]); + } + { + let v = vec![_do(0), _do(1), _do(2), undo(2), undo(1)]; + assert_eq!(simplify(v), vec![_do(0)]); + } + { + let v = vec![_do(1), _do(2), undo(2), undo(1), _do(3)]; + assert_eq!(simplify(v), vec![_do(3)]); + } + { + let v = vec![_do(0), _do(1), _do(2), undo(2), undo(1), _do(3)]; + assert_eq!(simplify(v), vec![_do(0), _do(3)]); + } + { + let v = vec![_do(0), _do(1), _do(2), undo(2), undo(1), undo(0)]; + assert_eq!(simplify(v), vec![]); + } + { + let v = vec![ + _do(0), + _do(1), + _do(2), + undo(2), + undo(1), + _do(10), + undo(10), + undo(0), + ]; + assert_eq!(simplify(v), vec![]); + } + } +} diff --git a/crates/bevy_text/src/undo_2/mod.rs b/crates/bevy_text/src/undo_2/mod.rs new file mode 100644 index 0000000000000..3db4fc569c309 --- /dev/null +++ b/crates/bevy_text/src/undo_2/mod.rs @@ -0,0 +1,6 @@ +//! Support for undo + +mod lib; +mod tests; + +pub use lib::*; diff --git a/crates/bevy_text/src/undo_2/tests/iter_realized.rs b/crates/bevy_text/src/undo_2/tests/iter_realized.rs new file mode 100644 index 0000000000000..f2c20a771f488 --- /dev/null +++ b/crates/bevy_text/src/undo_2/tests/iter_realized.rs @@ -0,0 +1,72 @@ +#![allow(unused, reason = "tests")] + +use crate::undo_2::*; + +#[derive(PartialEq, Debug)] +enum Command { + A, + B, + C, + D, + E, +} +use Command::*; + +#[test] +fn iter_realized() { + let mut c = Commands::default(); + c.push(A); + c.push(B); + + c.undo(); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&A]); + + c.push(C); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&C, &A]); + + c.undo(); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&A]); + + c.undo(); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&B, &A]); + + c.push(D); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&D, &B, &A]); + + c.push(E); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&E, &D, &B, &A]); + + c.undo(); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&D, &B, &A]); + + c.undo(); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&B, &A]); + + c.undo(); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&C, &A]); + + c.undo(); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&A]); + + c.undo(); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(v, [&B, &A]); + + c.undo(); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(v, [&A]); + + c.undo(); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(v, Vec::<&Command>::new()); +} diff --git a/crates/bevy_text/src/undo_2/tests/merge.rs b/crates/bevy_text/src/undo_2/tests/merge.rs new file mode 100644 index 0000000000000..d2348c0e94dfa --- /dev/null +++ b/crates/bevy_text/src/undo_2/tests/merge.rs @@ -0,0 +1,95 @@ +#[test] +fn merge() { + use crate::undo_2::CommandItem; + use crate::undo_2::Commands; + use crate::undo_2::IterRealized; + use crate::undo_2::Merge; + use core::ops::ControlFlow; + #[derive(Eq, PartialEq, Debug)] + enum Command { + A, + B, + C, + AB, + } + use Command::*; + fn is_ab(mut it: IterRealized<'_, Command>) -> (bool, IterRealized<'_, Command>) { + let cond = it.next() == Some(&B) && it.next() == Some(&A); + (cond, it) + } + fn parse( + start: IterRealized<'_, Command>, + ) -> ControlFlow>, Option>> { + if let (true, end) = is_ab(start.clone()) { + ControlFlow::Continue(Some(Merge { + start, + end, + command: Some(AB), + })) + } else { + ControlFlow::Continue(None) + } + } + { + let mut commands = Commands::new(); + commands.push(A); + commands.push(B); + + commands.merge(parse); + assert_eq!(*commands, [CommandItem::Command(AB)]); + } + { + let mut commands = Commands::new(); + commands.push(A); + commands.push(C); + commands.push(B); + commands.push(A); + commands.push(B); + + commands.merge(parse); + assert_eq!( + *commands, + [ + CommandItem::Command(A), + CommandItem::Command(C), + CommandItem::Command(B), + CommandItem::Command(AB) + ] + ); + } + { + let mut commands = Commands::new(); + commands.push(A); + commands.push(C); + commands.push(A); + commands.push(B); + commands.push(B); + commands.push(A); + commands.push(B); + + commands.merge(parse); + assert_eq!( + *commands, + [ + CommandItem::Command(A), + CommandItem::Command(C), + CommandItem::Command(AB), + CommandItem::Command(B), + CommandItem::Command(AB) + ] + ); + } + { + let mut commands = Commands::new(); + commands.push(A); + commands.push(B); + commands.push(A); + commands.push(B); + + commands.merge(parse); + assert_eq!( + &*commands, + &[CommandItem::Command(AB), CommandItem::Command(AB)] + ); + } +} diff --git a/crates/bevy_text/src/undo_2/tests/mod.rs b/crates/bevy_text/src/undo_2/tests/mod.rs new file mode 100644 index 0000000000000..5b18d9370d989 --- /dev/null +++ b/crates/bevy_text/src/undo_2/tests/mod.rs @@ -0,0 +1,9 @@ +mod iter_realized; +mod merge; +mod redo; +mod remove_undone; +mod set_and_transition; +mod splice; +mod unbuild; +mod undo; +mod undo_tests; diff --git a/crates/bevy_text/src/undo_2/tests/redo.rs b/crates/bevy_text/src/undo_2/tests/redo.rs new file mode 100644 index 0000000000000..1bc520691da33 --- /dev/null +++ b/crates/bevy_text/src/undo_2/tests/redo.rs @@ -0,0 +1,83 @@ +#![allow(unused, reason = "tests")] +use crate::undo_2::{Action, Commands}; + +#[derive(Debug, Eq, PartialEq)] +enum Command { + A, + B, + C, + D, +} +use Action::*; +use Command::*; + +#[test] +fn redo() { + { + let mut commands = Commands::new(); + + commands.push(A); // A + commands.push(B); // A B + commands.undo(); // A + commands.undo(); // + commands.push(C); // C + commands.undo(); // + commands.undo(); // A B + + let v: Vec<_> = commands.redo().collect(); + assert_eq!(v, [Undo(&B), Undo(&A)]); + + let v: Vec<_> = commands.redo().collect(); + assert_eq!(v, [Do(&C)]); + } + { + let mut commands = Commands::new(); + + commands.push(A); // A + commands.push(B); // A B + commands.undo(); // A + commands.undo(); // + // + commands.push(C); // C + // + commands.undo(); // + commands.undo(); // A B + commands.undo(); // A + commands.undo(); // + + commands.push(D); // D + + commands.undo(); // + commands.undo(); // C + commands.undo(); // + commands.undo(); // A B + + let v: Vec<_> = commands.redo().collect(); + assert_eq!(v, [Undo(&B), Undo(&A)]); + + let v: Vec<_> = commands.redo().collect(); + assert_eq!(v, [Do(&C)]); + + let v: Vec<_> = commands.redo().collect(); + assert_eq!(v, [Undo(&C)]); + + let v: Vec<_> = commands.redo().collect(); + assert_eq!(v, [Do(&D)]); + + let v: Vec<_> = commands.redo().collect(); + assert_eq!(v, []); + } +} + +#[test] +fn redo_all() { + let mut commands = Commands::new(); + + commands.push(A); + commands.push(B); + commands.undo(); + commands.undo(); + + let v: Vec<_> = commands.redo_all().collect(); + assert_eq!(v, [Do(&A), Do(&B)]); +} diff --git a/crates/bevy_text/src/undo_2/tests/remove_undone.rs b/crates/bevy_text/src/undo_2/tests/remove_undone.rs new file mode 100644 index 0000000000000..37de3f0b7b676 --- /dev/null +++ b/crates/bevy_text/src/undo_2/tests/remove_undone.rs @@ -0,0 +1,188 @@ +#![allow(unused, reason = "tests")] +use crate::undo_2::*; + +#[test] +fn remove_undone() { + #[derive(Debug, PartialEq)] + enum Command { + A, + B, + C, + } + use Command::*; + { + let mut c = Commands::default(); + + c.push(A); + c.push(B); + c.undo(); + c.push(C); + assert_eq!(*c, [A.into(), B.into(), CommandItem::Undo(0), C.into()]); + + c.remove_all_undone(); + assert_eq!(*c, [A.into(), C.into()]); + } + { + let mut c = Commands::default(); + + c.push(A); + c.push(B); + c.push(C); + c.undo(); + + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&B, &A]); + + c.remove_all_undone(); + assert_eq!(*c, [A.into(), B.into()]); + } + { + let mut c = Commands::default(); + + c.push(A); + c.push(B); + c.undo(); + c.push(C); + c.push(C); + c.undo(); + + c.remove_all_undone(); + assert_eq!(*c, [A.into(), C.into()]); + } + { + let mut c = Commands::default(); + + c.push(A); + c.push(B); + c.undo(); + c.push(C); + c.push(C); + c.undo(); + c.undo(); + + c.remove_all_undone(); + assert_eq!(*c, [A.into()]); + } + { + let mut c = Commands::default(); + + c.push(A); + c.push(B); + c.undo(); + c.push(C); + c.push(C); + c.undo(); + c.undo(); + c.undo(); + + c.remove_all_undone(); + assert_eq!(*c, [A.into(), B.into()]); + } +} +fn remove_undo_from() { + #[derive(Debug, PartialEq)] + enum Command { + A, + B, + C, + D, + } + use Command::*; + { + let mut c = Commands::default(); + + c.push(A); + c.push(B); + c.undo(); + c.push(C); + c.push(C); + c.undo(); + c.push(D); + assert_eq!( + *c, + [ + A.into(), + B.into(), + CommandItem::Undo(0), + C.into(), + C.into(), + CommandItem::Undo(0), + D.into() + ] + ); + + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&D, &C, &A]); + + c.remove_undone(|mut it| { + it.nth(1); + it + }); + assert_eq!( + *c, + [A.into(), C.into(), C.into(), CommandItem::Undo(0), D.into()] + ); + + // This operation does not change the sequence of realized commands: + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&D, &C, &A]); + } + { + let mut c = Commands::default(); + + c.push(A); + c.push(B); + c.undo(); + c.push(C); + c.push(C); + c.undo(); + c.push(D); + assert_eq!( + *c, + [ + A.into(), + B.into(), + CommandItem::Undo(0), + C.into(), + C.into(), + CommandItem::Undo(0), + D.into() + ] + ); + + c.remove_undone(|mut it| { + it.next(); + it + }); + assert_eq!(*c, [A.into(), C.into(), D.into()]); + } + { + let mut c = Commands::default(); + + c.push(A); + c.push(B); + c.undo(); + c.push(C); + c.push(C); + c.undo(); + c.push(D); + assert_eq!( + *c, + [ + A.into(), + B.into(), + CommandItem::Undo(0), + C.into(), + C.into(), + CommandItem::Undo(0), + D.into() + ] + ); + + c.remove_undone(|mut it| { + it.nth(2); + it + }); + assert_eq!(*c, []); + } +} diff --git a/crates/bevy_text/src/undo_2/tests/set_and_transition.rs b/crates/bevy_text/src/undo_2/tests/set_and_transition.rs new file mode 100644 index 0000000000000..c93b1702096f5 --- /dev/null +++ b/crates/bevy_text/src/undo_2/tests/set_and_transition.rs @@ -0,0 +1,100 @@ +use core::mem::{discriminant, Discriminant}; + +use crate::undo_2::{IndepStateKey, SetTransAction}; + +#[derive(Copy, Clone, Debug)] +struct State { + color: f64, + length: f64, +} +impl State { + fn new() -> Self { + INIT_STATE + } + fn apply_set(&mut self, c: &SetCommands) { + match c { + SetCommands::Color(v) => self.color = *v, + SetCommands::Length(v) => self.length = *v, + }; + } + fn execute_action(&mut self, c: SetTransAction) { + match c { + SetTransAction::Do(_) | SetTransAction::Undo(_) => {} + SetTransAction::Set(c) => self.apply_set(c), + SetTransAction::SetToInitial(d) => self.apply_set(SetCommands::new_initial(d)), + } + } +} +static INIT_STATE: State = State { + color: 0., + length: 0., +}; + +#[derive(Debug, Copy, Clone, PartialEq)] +enum SetCommands { + Color(f64), + Length(f64), +} +static INIT_COLOR: SetCommands = SetCommands::Color(INIT_STATE.color); +static INIT_LENGTH: SetCommands = SetCommands::Length(INIT_STATE.length); + +impl SetCommands { + fn new_initial(d: Discriminant) -> &'static Self { + if d == discriminant(&INIT_COLOR) { + &INIT_COLOR + } else if d == discriminant(&INIT_LENGTH) { + &INIT_LENGTH + } else { + unreachable!("SetCommands::new_initial is not exhaustive: please, adds lacking initial value to this method") + } + } +} + +impl IndepStateKey for SetCommands { + type KeyType = Discriminant; + + fn key(&self) -> Self::KeyType { + discriminant(self) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum TransitionCommand { + A, + B, +} + +#[test] +fn set_and_transition() { + use crate::undo_2::{Commands, SetOrTransition::*}; + let mut commands = Commands::new(); + let mut state = State::new(); + + let c = SetCommands::Color(1.); + state.apply_set(&c); + commands.push(Set(c)); + + commands.push(Transition(TransitionCommand::A)); + commands.push(Transition(TransitionCommand::B)); + + let c = SetCommands::Length(10.); + state.apply_set(&c); + commands.push(Set(c)); + + let c = SetCommands::Color(2.); + state.apply_set(&c); + commands.push(Set(c)); + + commands.apply_undo(|c| { + assert_eq!(c, SetTransAction::Set(&SetCommands::Color(1.))); + state.execute_action(c); + }); + assert_eq!(state.color, 1.); + assert_eq!(state.length, 10.); + commands.apply_redo(|c| { + assert_eq!(c, SetTransAction::Set(&SetCommands::Color(2.))); + state.execute_action(c); + }); + assert_eq!(state.color, 2.); + assert_eq!(state.length, 10.); +} diff --git a/crates/bevy_text/src/undo_2/tests/splice.rs b/crates/bevy_text/src/undo_2/tests/splice.rs new file mode 100644 index 0000000000000..c906b9a3bd9bc --- /dev/null +++ b/crates/bevy_text/src/undo_2/tests/splice.rs @@ -0,0 +1,198 @@ +#![allow(unused, reason = "tests")] + +use crate::undo_2::*; +use core::ops::ControlFlow; + +#[derive(PartialEq, Debug)] +enum Command { + A, + B, + C, + D, + E, +} +use Command::*; + +fn is_abc(mut it: IterRealized<'_, Command>) -> (bool, IterRealized<'_, Command>) { + let cond = it.next() == Some(&C) && it.next() == Some(&B) && it.next() == Some(&A); + (cond, it) +} + +fn do_splice(c: &mut Commands) { + c.splice(|start| { + if let (true, end) = is_abc(start.clone()) { + ControlFlow::Continue(Some(Splice { + start, + end, + commands: [D, E], + })) + } else { + ControlFlow::Continue(None) + } + }); +} + +#[test] +fn splice_without_undos() { + { + let mut c = Commands::default(); + + do_splice(&mut c); + assert_eq!(*c, []); + + c.push(A); + + do_splice(&mut c); + assert_eq!(*c, [A.into()]); + + c.push(B); + + do_splice(&mut c); + assert_eq!(*c, [A.into(), B.into()]); + + c.push(C); + + do_splice(&mut c); + assert_eq!(*c, [D.into(), E.into()]); + + c.push(A); + + do_splice(&mut c); + assert_eq!(*c, [D.into(), E.into(), A.into()]); + + c.push(B); + + do_splice(&mut c); + assert_eq!(*c, [D.into(), E.into(), A.into(), B.into()]); + + c.push(C); + } + { + let mut c = Commands::default(); + + do_splice(&mut c); + assert_eq!(*c, []); + + c.push(A); + + do_splice(&mut c); + assert_eq!(*c, [A.into()]); + + c.push(B); + + do_splice(&mut c); + assert_eq!(*c, [A.into(), B.into()]); + + c.push(C); + + do_splice(&mut c); + assert_eq!(*c, [D.into(), E.into()]); + + c.push(D); + + do_splice(&mut c); + assert_eq!(*c, [D.into(), E.into(), D.into()]); + + c.push(A); + + do_splice(&mut c); + assert_eq!(*c, [D.into(), E.into(), D.into(), A.into()]); + + c.push(B); + + do_splice(&mut c); + assert_eq!(*c, [D.into(), E.into(), D.into(), A.into(), B.into()]); + + c.push(C); + do_splice(&mut c); + assert_eq!(*c, [D.into(), E.into(), D.into(), D.into(), E.into()]); + } + { + let mut c = Commands::default(); + + do_splice(&mut c); + assert_eq!(*c, []); + + c.push(A); + c.push(B); + c.push(C); + c.push(D); + c.push(A); + c.push(B); + c.push(C); + + do_splice(&mut c); + assert_eq!(*c, [D.into(), E.into(), D.into(), D.into(), E.into()]); + } +} + +#[test] +fn splice_with_undos() { + { + let mut c = Commands::default(); + + do_splice(&mut c); + let v: Vec<_> = c.iter_realized().collect(); + assert!(v.is_empty()); + + c.push(A); + do_splice(&mut c); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&A]); + + c.push(D); + do_splice(&mut c); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&D, &A]); + + c.undo(); + do_splice(&mut c); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&A]); + + c.push(B); + do_splice(&mut c); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&B, &A]); + + c.push(C); + do_splice(&mut c); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&E, &D]); + assert_eq!(*c, [D.into(), E.into()]); + } + { + let mut c = Commands::default(); + + c.push(A); + c.push(B); + c.push(C); + c.undo(); + c.undo(); + c.push(D); + c.push(E); + c.undo(); + c.undo(); + c.undo(); + c.push(E); + + do_splice(&mut c); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&E, &E, &D]); + assert_eq!(*c, [D.into(), E.into(), E.into()]); + } + { + let mut c = Commands::default(); + + c.push(A); + c.push(B); + c.push(C); + c.push(C); + c.undo(); + + do_splice(&mut c); + let v: Vec<_> = c.iter_realized().collect(); + assert_eq!(*v, [&E, &D]); + assert_eq!(*c, [D.into(), E.into()]); + } +} diff --git a/crates/bevy_text/src/undo_2/tests/unbuild.rs b/crates/bevy_text/src/undo_2/tests/unbuild.rs new file mode 100644 index 0000000000000..dd80720cd93cb --- /dev/null +++ b/crates/bevy_text/src/undo_2/tests/unbuild.rs @@ -0,0 +1,37 @@ +#![allow(unused, reason = "tests")] +use crate::undo_2::{Action, Commands}; + +#[derive(Debug, Eq, PartialEq)] +enum Command { + A, + B, + C, +} +use Command::*; + +#[test] +fn unbuild() { + use Action::*; + let mut commands = Commands::new(); + + commands.push(A); + commands.push(B); + commands.undo(); + commands.push(C); + + let c: Vec<_> = commands.unbuild().collect(); + assert_eq!(c, [Undo(&C)]); + + let c: Vec<_> = commands.unbuild().collect(); + assert_eq!(c, &[Undo(&A)]); + + let c: Vec<_> = commands.unbuild().collect(); + assert!(c.is_empty()); + + dbg!(&commands); + let v: Vec<_> = commands.rebuild().collect(); + assert_eq!(v, &[Do(&A)]); + + let v: Vec<_> = commands.rebuild().collect(); + assert_eq!(v, &[Do(&C)]); +} diff --git a/crates/bevy_text/src/undo_2/tests/undo.rs b/crates/bevy_text/src/undo_2/tests/undo.rs new file mode 100644 index 0000000000000..6a1728671d7eb --- /dev/null +++ b/crates/bevy_text/src/undo_2/tests/undo.rs @@ -0,0 +1,61 @@ +#![allow(unused, reason = "tests")] +use crate::undo_2::{Action, Commands}; + +#[derive(Debug, Eq, PartialEq)] +enum Command { + A, + B, + C, + D, +} +use Command::*; + +#[test] +fn undo() { + use Action::*; + { + let mut c = Commands::default(); + + c.push(A); + c.push(B); + + let v: Vec<_> = c.undo().collect(); + assert_eq!(v, [Undo(&B)]); + + let v: Vec<_> = c.undo().collect(); + assert_eq!(v, [Undo(&A)]); + } + { + let mut c = Commands::default(); + + c.push(A); + c.push(B); + c.undo(); + c.undo(); + c.push(C); + + let v: Vec<_> = c.undo().collect(); + assert_eq!(v, [Undo(&C)]); + + let v: Vec<_> = c.undo().collect(); + assert_eq!(v, [Do(&A), Do(&B)]); + } + { + let mut c = Commands::default(); + + c.push(A); // A + c.push(B); // A B + c.undo(); // A + c.undo(); // + c.push(C); // C + c.undo(); // + c.undo(); // A B + c.push(D); // A B D + + let v: Vec<_> = c.undo().collect(); + assert_eq!(v, [Undo(&D)]); + + let v: Vec<_> = c.undo().collect(); + assert_eq!(v, [Undo(&B), Undo(&A), Do(&C)]); + } +} diff --git a/crates/bevy_text/src/undo_2/tests/undo_tests.rs b/crates/bevy_text/src/undo_2/tests/undo_tests.rs new file mode 100644 index 0000000000000..a700d3b06ffe6 --- /dev/null +++ b/crates/bevy_text/src/undo_2/tests/undo_tests.rs @@ -0,0 +1,205 @@ +#![allow(unused, reason = "tests")] +use crate::undo_2::Action; +use crate::undo_2::Action::*; +use crate::undo_2::Commands; +#[test] +fn application() { + enum Command { + Add(char), + Delete(char), + } + struct TextEditor { + text: String, + command: Commands, + } + impl TextEditor { + pub fn new() -> Self { + Self { + text: String::new(), + command: Commands::new(), + } + } + pub fn add_char(&mut self, c: char) { + self.text.push(c); + self.command.push(Command::Add(c)); + } + pub fn delete_char(&mut self) { + if let Some(c) = self.text.pop() { + self.command.push(Command::Delete(c)); + } + } + pub fn undo(&mut self) { + for action in self.command.undo() { + interpret_action(&mut self.text, action); + } + } + pub fn redo(&mut self) { + for action in self.command.redo() { + interpret_action(&mut self.text, action); + } + } + } + fn interpret_action(data: &mut String, action: Action<&Command>) { + use Action::*; + use Command::*; + match action { + Do(Add(c)) | Undo(Delete(c)) => { + data.push(*c); + } + Undo(Add(_)) | Do(Delete(_)) => { + data.pop(); + } + } + } + let mut editor = TextEditor::new(); + editor.add_char('a'); // :[1] + editor.add_char('b'); // :[2] + editor.add_char('d'); // :[3] + assert_eq!(editor.text, "abd"); + + editor.undo(); // first undo :[4] + assert_eq!(editor.text, "ab"); + + editor.add_char('c'); // :[5] + assert_eq!(editor.text, "abc"); + + editor.undo(); // Undo [5] :[6] + assert_eq!(editor.text, "ab"); + editor.undo(); // Undo the undo [4] :[7] + assert_eq!(editor.text, "abd"); + editor.undo(); // Undo [3] :[8] + assert_eq!(editor.text, "ab"); + editor.undo(); // Undo [2] :[9] + assert_eq!(editor.text, "a"); + + editor.add_char('z'); // :[10] + assert_eq!(editor.text, "az"); + // when an action is performed after a sequence + // of undo, the undos are merged: undos [6] to [9] are merged now + + editor.undo(); // back to [10] + assert_eq!(editor.text, "a"); + editor.undo(); // back to [5]: reverses the consecutive sequence of undos in batch + assert_eq!(editor.text, "abc"); + editor.undo(); // back to [4] + assert_eq!(editor.text, "ab"); + editor.undo(); // back to [3] + assert_eq!(editor.text, "abd"); + editor.undo(); // back to [2] + assert_eq!(editor.text, "ab"); + editor.undo(); // back to [1] + assert_eq!(editor.text, "a"); + editor.undo(); // back to [0] + assert_eq!(editor.text, ""); + + editor.redo(); // back to [1] + assert_eq!(editor.text, "a"); + editor.redo(); // back to [2] + assert_eq!(editor.text, "ab"); + editor.redo(); // back to [3] + assert_eq!(editor.text, "abd"); + editor.redo(); // back to [4] + assert_eq!(editor.text, "ab"); + editor.redo(); // back to [5] + assert_eq!(editor.text, "abc"); + editor.redo(); // back to [9]: redo inner consecutive sequence of undos in batch + // (undo are merged only when they are not the last action) + assert_eq!(editor.text, "a"); + editor.redo(); // back to [10] + assert_eq!(editor.text, "az"); + + editor.add_char('1'); + editor.add_char('2'); + assert_eq!(editor.text, "az12"); + editor.undo(); + editor.undo(); + assert_eq!(editor.text, "az"); + editor.redo(); // undo is the last action, undo the undo only once + assert_eq!(editor.text, "az1"); + editor.redo(); + assert_eq!(editor.text, "az12"); +} +struct CommandString(Vec<(usize, char)>, Commands<(usize, char)>); +impl CommandString { + fn new() -> Self { + CommandString(vec![], Commands::new()) + } + fn push(&mut self, c: char) { + let l = self.0.len(); + self.0.push((l, c)); + self.1.push((l, c)); + } + #[track_caller] + fn undo(&mut self) { + Self::apply(&mut self.0, self.1.undo()); + } + #[track_caller] + fn redo(&mut self) { + Self::apply(&mut self.0, self.1.redo()); + } + #[track_caller] + fn apply<'a>(s: &mut Vec<(usize, char)>, it: impl Iterator>) { + for c in it { + match c { + Do(i) => { + assert_eq!(s.len(), i.0, "inconsitent push"); + s.push(*i); + } + Undo(i) => { + assert_eq!(s.pop(), Some(*i), "inconsistent pop"); + } + } + } + } +} +#[test] +fn command_sequence() { + let mut c = CommandString::new(); + c.push('a'); + assert!(c.0 == [(0, 'a')]); + assert!(c.1.len() == 1); + c.undo(); + assert!(c.0.is_empty()); + c.redo(); + assert!(c.0 == [(0, 'a')]); + c.push('b'); + c.push('c'); + assert!(c.0 == [(0, 'a'), (1, 'b'), (2, 'c')]); + c.redo(); + assert!(c.0 == [(0, 'a'), (1, 'b'), (2, 'c')]); + c.undo(); + assert!(c.0 == [(0, 'a'), (1, 'b')]); + c.push('d'); + assert!(c.0 == [(0, 'a'), (1, 'b'), (2, 'd')]); + c.push('e'); + assert!(c.0 == [(0, 'a'), (1, 'b'), (2, 'd'), (3, 'e')]); + c.undo(); + assert!(c.0 == [(0, 'a'), (1, 'b'), (2, 'd')]); + c.undo(); + assert!(c.0 == [(0, 'a'), (1, 'b')]); + c.undo(); + assert!(c.0 == [(0, 'a'), (1, 'b'), (2, 'c')]); + c.undo(); + assert!(c.0 == [(0, 'a'), (1, 'b')]); + c.undo(); + assert!(c.0 == [(0, 'a')]); + c.push('f'); + assert!(c.0 == [(0, 'a'), (1, 'f')]); + c.undo(); + assert!(c.0 == [(0, 'a')]); + c.undo(); + assert!(c.0 == [(0, 'a'), (1, 'b'), (2, 'd'), (3, 'e')]); + c.redo(); + assert!(c.0 == [(0, 'a')]); + c.undo(); + assert!(c.0 == [(0, 'a'), (1, 'b'), (2, 'd'), (3, 'e')]); +} +#[cfg(feature = "serde")] +#[test] +fn serde() { + let mut commands = Commands::new(); + commands.push("a"); + let str = serde_json::to_string(&commands).unwrap(); + let commands: Commands = serde_json::from_str(&str).unwrap(); + assert_eq!(*commands, ["a".to_owned().into()]); +} diff --git a/typos.toml b/typos.toml index 16aab11772821..83014e7e1dc0c 100644 --- a/typos.toml +++ b/typos.toml @@ -1,9 +1,10 @@ [files] extend-exclude = [ - "*.pbxproj", # metadata file - "*.patch", # Automatically generated files that should not be manually modified. - "*.bin", # Binary files - ".git/", # Version control files + "*.pbxproj", # metadata file + "*.patch", # Automatically generated files that should not be manually modified. + "*.bin", # Binary files + ".git/", # Version control files + "crates/bevy_text/src/undo_2/", # undo_2 ] ignore-hidden = false