Files
oak-gpui/crates/gpui_elements/src/editable_text/history.rs
T

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,
}
}
}