use std::collections::{BTreeSet, HashMap}; use std::time::Duration; use bounded_vec_deque::BoundedVecDeque; use instant::Instant; use super::time::{Global, Lamport, LamportValue}; use crate::editor::view::model::LocalSelections; use crate::editor::view::PlainTextEditorViewAction; /// The maximum time we will batch consecutive edits for the same [`Action`]. /// The "batch" here is not to be confused with the [`Buffer`]'s notion /// of a "batch". A single undo / redo batched item might consist /// of multiple buffer batched items. const UNDO_REDO_BATCH_TIMER: Duration = Duration::from_millis(500); /// The maximum size of the undo stack. /// TODO: this could be substantially larger now that we don't store snapshots. const DEFAULT_UNDO_STACK_CAPACITY: usize = 20; /// An entry in the [`LocalUndoStack`]. #[derive(Clone)] struct UndoStackEntry { /// The set of operations that would be undone. /// These numbers are lamport timestamps corresponding to the edit operations. /// We don't need a full [`Lamport`] timestamp because the replica ID would be /// redundant (this is only for local edits). ops: Vec, /// A snapshot of the selection state after the ops. selections: LocalSelections, /// The action associated with this entry. /// If this is the current item on the stack and the last /// change in the current batch was a pure selection change, this will be /// [`Action::CursorChanged`]. action: PlainTextEditorViewAction, } /// A stack of batched, local operations that can be undone / redone. /// The undo stack batches consecutive records within a [`UNDO_REDO_BATCH_TIMER`] /// period if: /// - the records are associated to the same action /// - the associated action is not atomic /// - there aren't any undos (we're at the top of the stack) #[derive(Clone)] pub struct LocalUndoStack { /// The stack itself. stack: BoundedVecDeque, /// Where we are in the stack currently. /// As we undo / redo, the position into the stack /// changes but the stack itself stays in tact. current_index: usize, /// The absolute time when the stack was last modified. last_changed_at: Option, } impl LocalUndoStack { pub fn new(init_selections: LocalSelections) -> LocalUndoStack { Self::new_with_capacity(init_selections, DEFAULT_UNDO_STACK_CAPACITY) } fn new_with_capacity(init_selections: LocalSelections, capacity: usize) -> LocalUndoStack { let init_entry = UndoStackEntry { ops: vec![], selections: init_selections, action: PlainTextEditorViewAction::ReplaceBuffer, }; Self { stack: BoundedVecDeque::from_iter([init_entry], capacity), current_index: 0, last_changed_at: None, } } /// Records a selection change by updating the current item's selection snapshot. pub fn record_selection_change(&mut self, selections: LocalSelections) { self.stack[self.current_index].selections = selections; self.stack[self.current_index].action = PlainTextEditorViewAction::CursorChanged; } /// Records an edit and potentially batches it with other edits if possible. pub fn record_edit( &mut self, action: PlainTextEditorViewAction, operations: impl Iterator, selections: LocalSelections, ) { let should_insert_new_entry = self.current_index == 0 || self.current_index < self.stack.len() - 1 || action.is_atomic() || Some(action) != self.last_action() || self .last_changed_at .is_none_or(|t| t.elapsed() >= UNDO_REDO_BATCH_TIMER); // If the index is positioned somewhere other than the last item, // this means that the user is making an edit after some undo'ing. // Any of the items past the current index are not needed anymore. self.stack.truncate(self.current_index + 1); if should_insert_new_entry { // Since we're using a [`BoundedVecDeque`], pushing to the // back might remove from the front if the stack is at capacity. self.stack.push_back(UndoStackEntry { action, ops: operations.collect(), selections, }); self.current_index = self.stack.len() - 1; } else { let back = self.stack.back_mut().unwrap(); back.ops.extend(operations); back.selections = selections; } self.last_changed_at = Some(Instant::now()); } /// Processes an undo and returns /// 1. the set of operations that need to be undone /// 2. the set of selections that need to be restored pub fn undo(&mut self) -> Option<(Vec, LocalSelections)> { if self.current_index == 0 { return None; } self.last_changed_at = None; let ops = self.stack[self.current_index].ops.clone(); self.current_index -= 1; let selections = self.stack[self.current_index].selections.clone(); Some((ops, selections)) } /// Processes a redo and returns /// 1. the set of operations that need to be redone /// 2. the set of selections that need to be restored pub fn redo(&mut self) -> Option<(Vec, LocalSelections)> { if self.current_index == self.stack.len() - 1 { return None; } self.last_changed_at = None; self.current_index += 1; let UndoStackEntry { ops, selections, .. } = self.stack[self.current_index].clone(); Some((ops, selections)) } /// Resets the undo stack to a single item with the given selections. pub fn reset(&mut self, selections: LocalSelections) { let init_entry = UndoStackEntry { ops: vec![], selections, action: PlainTextEditorViewAction::ReplaceBuffer, }; self.stack = BoundedVecDeque::from_iter([init_entry], DEFAULT_UNDO_STACK_CAPACITY); self.last_changed_at = None; self.current_index = 0; } /// Returns the action associated to the last item on the undo stack. pub fn last_action(&self) -> Option { if self.stack.len() == 1 { return None; }; self.stack.back().map(|e| e.action) } } /// A history of local and remote undos. #[derive(Clone)] pub struct UndoHistory { /// A mapping from an edit ID to the lamport timestamps of undo operations for that edit. /// The values are bare lamport times because the replica ID of an undo operation will /// always be the replica ID for the edit itself (one can only undo their own edits). /// /// Suppose an undo operation of an edit `e` is defined as `undo(e)`. /// Then, a redo of an edit operation `e` can be thought of as `undo(undo(e))`. /// Using that line of logic, an edit is considered undone iff the number of undos are odd. /// /// We use a [`BTreeSet`] to quickly query the max undo ts for a given edit /// and to efficiently compute intersections with other sets (e.g. deletions). map: HashMap>, } impl UndoHistory { pub fn new() -> Self { UndoHistory { map: HashMap::new(), } } /// Returns the largest known undo timestamp for `edit`. fn max_undo_ts(&self, edit_id: &Lamport) -> LamportValue { self.map .get(edit_id) .and_then(|v| v.last().copied()) .unwrap_or_default() } /// Registers an undo for `edit` pub fn undo(&mut self, edit_id: Lamport, undo_ts: LamportValue) { // The undo timestamp should be larger than all undo timestamps // so far for this edit and must be after the edit itself. debug_assert!(undo_ts > self.max_undo_ts(&edit_id) && undo_ts > edit_id.value); self.map.entry(edit_id).or_default().insert(undo_ts); } /// Returns true iff `edit` is considered undone. /// An edit is considered undone iff it has an odd number of undos. /// In other words, there exists some undo that wasn't redone. pub fn is_edit_undone(&self, edit_id: &Lamport) -> bool { self.map.get(edit_id).map_or(0, |v| v.len()) % 2 == 1 } /// Returns true iff `edit` was undone when the version vector was `version`. pub fn was_edit_undone(&self, edit_id: &Lamport, version: &Global) -> bool { let num_undos = self .map .get(edit_id) .iter() .flat_map(|v| v.iter()) .take_while(|undo| { version.observed(&Lamport { replica_id: edit_id.replica_id(), value: **undo, }) }) .count(); num_undos % 2 == 1 } } impl PlainTextEditorViewAction { /// Whether the action is atomic. An atomic action is one where a single event /// is independently undoable. This differs from non-atomic actions (such as `INSERT_TEXT`) /// where multiple events are coalesced and undone together if they occur within [`UNDO_REDO_BATCH_TIMER`] of each other. pub fn is_atomic(&self) -> bool { matches!( self, PlainTextEditorViewAction::Yank | PlainTextEditorViewAction::AcceptCompletionSuggestion | PlainTextEditorViewAction::NewLine | PlainTextEditorViewAction::DeleteWordLeft | PlainTextEditorViewAction::DeleteWordRight | PlainTextEditorViewAction::AutoSuggestion | PlainTextEditorViewAction::ReplaceBuffer | PlainTextEditorViewAction::Paste | PlainTextEditorViewAction::ClearLines | PlainTextEditorViewAction::ClearAndCopyLines | PlainTextEditorViewAction::DeleteAll | PlainTextEditorViewAction::CutAll | PlainTextEditorViewAction::InsertSelectedText | PlainTextEditorViewAction::CutWordRight | PlainTextEditorViewAction::SystemInsert | PlainTextEditorViewAction::ExpandAlias | PlainTextEditorViewAction::CycleCompletionSuggestion ) } } #[cfg(test)] #[path = "undo_tests.rs"] mod tests;