use std::collections::VecDeque;
use std::ops::Range;
use crate::ropetext::change::Edit;
use crate::ropetext::text::Text;
pub(crate) const DEFAULT_BUDGET_BYTES: usize = 4 << 20;
const ENTRY_OVERHEAD_BYTES: usize = 128;
#[derive(Debug, Clone)]
pub(crate) struct Shape {
pub edits: Option<Vec<Edit>>,
pub rows: Range<usize>,
pub line_delta: isize,
}
#[derive(Debug)]
pub(crate) struct Entry {
pub before: Text,
pub after: Text,
pub forward: Shape,
pub inverse: Shape,
pub cursor_before: usize,
pub cursor_after: usize,
pub anchor_before: Option<usize>,
pub anchor_after: Option<usize>,
pub retained: usize,
}
impl Entry {
fn cost(&self) -> usize {
ENTRY_OVERHEAD_BYTES + self.retained
}
}
#[derive(Debug)]
pub(crate) struct History {
entries: VecDeque<Entry>,
index: usize,
budget: usize,
spent: usize,
}
impl Default for History {
fn default() -> Self {
Self::new(DEFAULT_BUDGET_BYTES)
}
}
impl History {
pub fn new(budget: usize) -> Self {
Self {
entries: VecDeque::new(),
index: 0,
budget,
spent: 0,
}
}
pub fn set_budget(&mut self, bytes: usize) {
self.budget = bytes;
self.evict_to_budget();
}
pub fn clear(&mut self) {
self.entries.clear();
self.index = 0;
self.spent = 0;
}
pub fn push(&mut self, entry: Entry) {
self.drop_redo_side();
self.spent += entry.cost();
self.entries.push_back(entry);
self.index = self.entries.len();
self.evict_to_budget();
}
pub fn extend_newest(&mut self, entry: Entry) -> Option<Entry> {
if self.index != self.entries.len() || self.entries.is_empty() {
return Some(entry);
}
let newest = self.entries.back_mut().expect("checked non-empty");
self.spent -= newest.cost();
newest.forward.rows = hull(
widen(&newest.forward.rows, entry.forward.line_delta),
&entry.forward.rows,
);
newest.inverse.rows = hull(
newest.inverse.rows.clone(),
&widen(&entry.inverse.rows, newest.forward.line_delta),
);
newest.forward.edits = None;
newest.inverse.edits = None;
newest.forward.line_delta += entry.forward.line_delta;
newest.inverse.line_delta += entry.inverse.line_delta;
newest.after = entry.after;
newest.cursor_after = entry.cursor_after;
newest.anchor_after = entry.anchor_after;
newest.retained += entry.retained;
self.spent += newest.cost();
self.evict_to_budget();
None
}
pub fn undo_candidate(&self) -> Option<&Entry> {
self.entries.get(self.index.checked_sub(1)?)
}
pub fn redo_candidate(&self) -> Option<&Entry> {
self.entries.get(self.index)
}
pub fn step_back(&mut self) {
self.index = self.index.saturating_sub(1);
}
pub fn step_forward(&mut self) {
self.index = (self.index + 1).min(self.entries.len());
}
#[cfg(test)]
pub fn len(&self) -> usize {
self.entries.len()
}
#[cfg(test)]
pub fn undo_depth(&self) -> usize {
self.index
}
fn drop_redo_side(&mut self) {
while self.entries.len() > self.index {
if let Some(dropped) = self.entries.pop_back() {
self.spent -= dropped.cost();
}
}
}
fn evict_to_budget(&mut self) {
while self.spent > self.budget && self.entries.len() > 1 {
let dropped = self.entries.pop_front().expect("checked non-empty");
self.spent -= dropped.cost();
self.index = self.index.saturating_sub(1);
}
}
}
fn hull(a: Range<usize>, b: &Range<usize>) -> Range<usize> {
a.start.min(b.start)..a.end.max(b.end)
}
fn widen(rows: &Range<usize>, delta: isize) -> Range<usize> {
let slack = delta.unsigned_abs();
rows.start.saturating_sub(slack)..rows.end.saturating_add(slack)
}
#[cfg(test)]
mod tests {
use super::*;
fn shape(rows: Range<usize>, line_delta: isize) -> Shape {
Shape {
edits: Some(vec![Edit::new(0..1, 0)]),
rows,
line_delta,
}
}
fn entry(before: &str, after: &str) -> Entry {
Entry {
before: Text::from(before),
after: Text::from(after),
forward: shape(0..1, 0),
inverse: shape(0..1, 0),
cursor_before: 0,
cursor_after: after.len(),
anchor_before: None,
anchor_after: None,
retained: after.len(),
}
}
#[test]
fn a_push_drops_the_redo_side() {
let mut h = History::default();
h.push(entry("", "a"));
h.push(entry("a", "ab"));
h.step_back();
assert!(h.redo_candidate().is_some());
h.push(entry("a", "ax"));
assert!(h.redo_candidate().is_none());
assert_eq!(h.len(), 2);
}
#[test]
fn extending_keeps_the_earlier_before_and_the_later_after() {
let mut h = History::default();
h.push(entry("", "a"));
assert!(h.extend_newest(entry("a", "ab")).is_none());
assert_eq!(h.len(), 1);
let merged = h.undo_candidate().expect("one entry");
assert_eq!(merged.before.to_string(), "");
assert_eq!(merged.after.to_string(), "ab");
}
#[test]
fn extending_forgets_the_per_edit_ranges() {
let mut h = History::default();
h.push(entry("", "a"));
h.extend_newest(entry("a", "ab"));
let merged = h.undo_candidate().expect("one entry");
assert!(
merged.forward.edits.is_none(),
"two coordinate spaces cannot be reported as one list"
);
assert!(merged.inverse.edits.is_none());
}
#[test]
fn extending_widens_rows_rather_than_narrowing_them() {
let mut h = History::default();
let mut first = entry("", "a\nb");
first.forward = shape(0..2, 1);
first.inverse = shape(0..1, -1);
h.push(first);
let mut second = entry("a\nb", "a\nbc");
second.forward = shape(1..2, 0);
second.inverse = shape(1..2, 0);
h.extend_newest(second);
let merged = h.undo_candidate().expect("one entry");
assert_eq!(merged.forward.rows, 0..2);
assert_eq!(merged.inverse.rows, 0..3);
assert_eq!(merged.forward.line_delta, 1);
}
#[test]
fn extending_is_refused_with_nothing_to_extend() {
let mut h = History::default();
assert!(h.extend_newest(entry("", "a")).is_some());
}
#[test]
fn extending_is_refused_across_an_undo() {
let mut h = History::default();
h.push(entry("", "a"));
h.push(entry("a", "ab"));
h.step_back();
assert!(h.extend_newest(entry("a", "ax")).is_some());
}
#[test]
fn eviction_drops_whole_entries_oldest_first() {
let mut h = History::new(ENTRY_OVERHEAD_BYTES * 3);
for _ in 0..10 {
h.push(entry("a", "ab"));
}
assert!(h.len() <= 3, "kept {} entries", h.len());
assert_eq!(h.undo_depth(), h.len(), "undo side stays consistent");
}
#[test]
fn the_newest_entry_is_never_evicted() {
let mut h = History::new(0);
h.push(entry("", "a"));
h.push(entry("a", "ab"));
assert_eq!(h.len(), 1);
assert_eq!(
h.undo_candidate()
.expect("the newest survives")
.after
.to_string(),
"ab"
);
}
#[test]
fn widening_by_nothing_changes_nothing() {
assert_eq!(widen(&(3..5), 0), 3..5);
assert_eq!(widen(&(3..5), 2), 1..7);
assert_eq!(widen(&(0..1), -2), 0..3);
}
}