159 lines
5.1 KiB
Rust
159 lines
5.1 KiB
Rust
use smallvec::SmallVec;
|
|
use std::{
|
|
ops::Range,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
/// Maximum number of history entries to keep.
|
|
pub const MAX_HISTORY_LEN: usize = 1000;
|
|
|
|
/// Default interval for grouping consecutive edits into a single undo entry.
|
|
pub const DEFAULT_GROUP_INTERVAL: Duration = Duration::from_millis(300);
|
|
|
|
// TODO: Should history get attached directly to storage? currently its per text field and operate both on storage and selection
|
|
pub struct EditableTextHistory {
|
|
/// The maximum duration between changes to `content` that can be grouped together as a single entry in the history log.
|
|
grouping_interval: Duration,
|
|
/// Stack of previous states for undo.
|
|
undo_stack: SmallVec<[HistoryEntry; MAX_HISTORY_LEN]>,
|
|
/// Stack of undone states for redo.
|
|
redo_stack: SmallVec<[HistoryEntry; MAX_HISTORY_LEN]>,
|
|
}
|
|
impl Default for EditableTextHistory {
|
|
fn default() -> Self {
|
|
Self {
|
|
grouping_interval: DEFAULT_GROUP_INTERVAL,
|
|
undo_stack: Default::default(),
|
|
redo_stack: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A patch-based history entry for memory-efficient undo/redo operations.
|
|
/// Instead of storing the full content, we store only the change needed to reverse the edit.
|
|
#[derive(Clone, Debug)]
|
|
pub struct HistoryEntry {
|
|
/// The byte range that was modified (after the edit, for undo; before the edit, for redo).
|
|
pub range: Range<usize>,
|
|
/// The text that was replaced (to restore on undo).
|
|
pub old_text: String,
|
|
/// The length of the new text that replaced old_text (to know how much to remove on undo).
|
|
pub new_text_len: usize,
|
|
/// The selection range before the edit.
|
|
pub selected_range: Range<usize>,
|
|
/// Timestamp for grouping consecutive edits.
|
|
pub timestamp: Instant,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum HistoryKind {
|
|
Undo,
|
|
Redo,
|
|
}
|
|
|
|
impl EditableTextHistory {
|
|
pub fn set_grouping_interval(&mut self, interval: Duration) {
|
|
self.grouping_interval = interval;
|
|
}
|
|
|
|
pub fn record(
|
|
&mut self,
|
|
range: Range<usize>,
|
|
old_text: &str,
|
|
new_text_len: usize,
|
|
selected_range: Range<usize>,
|
|
) {
|
|
let now = Instant::now();
|
|
|
|
// Check if we should group with the last entry
|
|
if let Some(last) = self.undo_stack.last_mut()
|
|
&& now.duration_since(last.timestamp) < self.grouping_interval
|
|
{
|
|
// The change was triggered within group interval timing.
|
|
// Try to extend the existing patch (which is a mutation).
|
|
// If extending successeds, then we can early-out. Otherwise the mutation is non-contiguous.
|
|
if last.extend(&range, new_text_len) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Limit history size
|
|
if self.undo_stack.len() >= MAX_HISTORY_LEN {
|
|
self.undo_stack.remove(0);
|
|
}
|
|
|
|
self.push(
|
|
HistoryKind::Undo,
|
|
HistoryEntry {
|
|
range: range.start..range.start + new_text_len,
|
|
old_text: old_text.to_string(),
|
|
new_text_len,
|
|
selected_range,
|
|
timestamp: now,
|
|
},
|
|
);
|
|
|
|
// New edit invalidates redo stack
|
|
self.redo_stack.clear();
|
|
}
|
|
|
|
fn stack(&self, kind: HistoryKind) -> &SmallVec<[HistoryEntry; MAX_HISTORY_LEN]> {
|
|
// NOTE: Could be an internal map
|
|
match kind {
|
|
HistoryKind::Undo => &self.undo_stack,
|
|
HistoryKind::Redo => &self.redo_stack,
|
|
}
|
|
}
|
|
|
|
fn stack_mut(&mut self, kind: HistoryKind) -> &mut SmallVec<[HistoryEntry; MAX_HISTORY_LEN]> {
|
|
match kind {
|
|
HistoryKind::Undo => &mut self.undo_stack,
|
|
HistoryKind::Redo => &mut self.redo_stack,
|
|
}
|
|
}
|
|
|
|
pub fn has_next(&self, kind: HistoryKind) -> bool {
|
|
!self.stack(kind).is_empty()
|
|
}
|
|
|
|
pub fn push(&mut self, kind: HistoryKind, entry: HistoryEntry) {
|
|
self.stack_mut(kind).push(entry);
|
|
}
|
|
|
|
pub fn take(&mut self, kind: HistoryKind) -> Option<HistoryEntry> {
|
|
self.stack_mut(kind).pop()
|
|
}
|
|
}
|
|
|
|
impl HistoryEntry {
|
|
fn extend(&mut self, range: &Range<usize>, new_text_len: usize) -> bool {
|
|
// NOTE: Could be more robust. Currently only supports human-written extensions from start towards end.
|
|
|
|
// ranges must be contiguous in order to integrate/extend
|
|
if self.range.end != range.start {
|
|
return false;
|
|
}
|
|
|
|
self.range.end = range.start + new_text_len;
|
|
self.new_text_len += new_text_len;
|
|
|
|
true
|
|
}
|
|
|
|
pub fn char_range(&self, max_len: usize) -> Range<usize> {
|
|
let undo_start = self.range.start;
|
|
let undo_end = (self.range.start + self.new_text_len).min(max_len);
|
|
undo_start..undo_end
|
|
}
|
|
|
|
pub fn as_inverted(self, prev_text_at_range: String) -> Self {
|
|
HistoryEntry {
|
|
range: self.range.start..self.range.start + self.old_text.len(),
|
|
old_text: prev_text_at_range,
|
|
new_text_len: self.old_text.len(),
|
|
selected_range: self.selected_range.clone(),
|
|
timestamp: self.timestamp,
|
|
}
|
|
}
|
|
}
|