use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::changeset::ChangeSet;
use crate::selection::SelectionSet;
use crate::text::Document;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Transaction {
pub changes: ChangeSet,
pub inverse: ChangeSet,
pub before: SelectionSet,
pub after: SelectionSet,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum EditKind {
Insert,
DeleteBackward,
DeleteForward,
Other,
}
pub const COALESCE_TIMEOUT: Duration = Duration::from_secs(1);
pub type Revision = u64;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UndoStep {
pub txns: Vec<Transaction>,
pub kind: EditKind,
pub rev_before: Revision,
pub rev_after: Revision,
#[serde(skip)]
last_edit_at: Duration,
}
impl UndoStep {
pub fn approx_size(&self) -> usize {
self.txns
.iter()
.map(|t| {
let edits = t.changes.edits().iter().chain(t.inverse.edits());
let sels = t.before.len() + t.after.len();
edits.map(|e| e.text.len() + 48).sum::<usize>() + sels * 40 + 64
})
.sum()
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SavedHistory {
pub undo: Vec<UndoStep>,
pub redo: Vec<UndoStep>,
pub revision: Revision,
pub next_revision: Revision,
}
impl SavedHistory {
pub fn is_empty(&self) -> bool {
self.undo.is_empty() && self.redo.is_empty()
}
pub fn check(&self, doc: &Document) -> Result<(), String> {
if self.next_revision > Revision::MAX / 2 || self.revision >= self.next_revision {
return Err("a revision is out of range".into());
}
let all = self.undo.iter().chain(&self.redo);
if all
.clone()
.any(|s| s.rev_before >= self.next_revision || s.rev_after >= self.next_revision)
{
return Err("a revision is out of range".into());
}
if self
.undo
.last()
.is_some_and(|s| s.rev_after != self.revision)
|| self
.redo
.last()
.is_some_and(|s| s.rev_before != self.revision)
|| self
.undo
.windows(2)
.any(|w| w[0].rev_after != w[1].rev_before)
|| self
.redo
.windows(2)
.any(|w| w[1].rev_after != w[0].rev_before)
{
return Err("the steps do not connect".into());
}
if all.clone().any(|s| s.txns.is_empty()) {
return Err("a step is empty".into());
}
let bad = |what: &str| Err(format!("{what} does not fit the text"));
let mut d = doc.clone();
for t in self.undo.iter().rev().flat_map(|s| s.txns.iter().rev()) {
if !t.after.fits(&d) || !t.inverse.fits(&d) {
return bad("an undo step");
}
t.inverse.apply(&mut d);
if !t.changes.fits(&d) || !t.before.fits(&d) {
return bad("an undo step");
}
}
let mut d = doc.clone();
for t in self.redo.iter().rev().flat_map(|s| s.txns.iter()) {
if !t.before.fits(&d) || !t.changes.fits(&d) {
return bad("a redo step");
}
t.changes.apply(&mut d);
if !t.inverse.fits(&d) || !t.after.fits(&d) {
return bad("a redo step");
}
}
Ok(())
}
}
pub fn sha256_hex(doc: &Document) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
for chunk in doc.chunks() {
hasher.update(chunk.as_bytes());
}
hasher
.finalize()
.iter()
.map(|b| format!("{b:02x}"))
.collect()
}
#[derive(Clone, Debug, Default)]
pub struct History {
undo: Vec<UndoStep>,
redo: Vec<UndoStep>,
coalesce_open: bool,
group_depth: usize,
group_started: bool,
}
impl History {
pub fn push(
&mut self,
txn: Transaction,
kind: EditKind,
rev_before: Revision,
rev_after: Revision,
now: Duration,
) {
self.redo.clear();
let extend_top = if self.group_depth > 0 {
self.group_started
} else {
self.coalesce_open
&& kind != EditKind::Other
&& self.undo.last().is_some_and(|top| {
top.kind == kind
&& top.rev_after == rev_before
&& now.saturating_sub(top.last_edit_at) <= COALESCE_TIMEOUT
})
};
match self.undo.last_mut() {
Some(top) if extend_top => {
top.txns.push(txn);
top.rev_after = rev_after;
top.last_edit_at = now;
}
_ => self.undo.push(UndoStep {
txns: vec![txn],
kind,
rev_before,
rev_after,
last_edit_at: now,
}),
}
self.group_started = self.group_depth > 0;
self.coalesce_open = true;
}
pub fn break_coalescing(&mut self) {
self.coalesce_open = false;
}
pub fn begin_group(&mut self) {
if self.group_depth == 0 {
self.group_started = false;
}
self.group_depth += 1;
}
pub fn end_group(&mut self) {
self.group_depth = self.group_depth.saturating_sub(1);
if self.group_depth == 0 {
self.group_started = false;
self.coalesce_open = false;
}
}
pub fn can_undo(&self) -> bool {
!self.undo.is_empty()
}
pub fn can_redo(&self) -> bool {
!self.redo.is_empty()
}
pub(crate) fn pop_undo(&mut self) -> Option<UndoStep> {
self.coalesce_open = false;
self.group_started = false;
self.undo.pop()
}
pub(crate) fn push_redo(&mut self, step: UndoStep) {
self.redo.push(step);
}
pub(crate) fn pop_redo(&mut self) -> Option<UndoStep> {
self.coalesce_open = false;
self.group_started = false;
self.redo.pop()
}
pub(crate) fn push_undo(&mut self, step: UndoStep) {
self.undo.push(step);
}
pub(crate) fn newest(&self, max_bytes: usize) -> (Vec<UndoStep>, Vec<UndoStep>) {
let mut budget = max_bytes;
let mut take = |steps: &[UndoStep]| {
let mut kept: Vec<UndoStep> = Vec::new();
for s in steps.iter().rev() {
let size = s.approx_size();
if size > budget {
break;
}
budget -= size;
kept.push(s.clone());
}
kept.reverse();
kept
};
let redo = take(&self.redo);
let undo = take(&self.undo);
(undo, redo)
}
pub(crate) fn replace(&mut self, undo: Vec<UndoStep>, redo: Vec<UndoStep>) {
*self = History {
undo,
redo,
..History::default()
};
}
pub fn clear(&mut self) {
*self = History::default();
}
pub(crate) fn extend_base(&mut self, by: usize, old: Revision, new: Revision) {
for step in self.undo.iter_mut().chain(self.redo.iter_mut()) {
for t in &mut step.txns {
t.changes.extend_base(by);
t.inverse.extend_base(by);
}
}
if let Some(top) = self.undo.last_mut().filter(|s| s.rev_after == old) {
top.rev_after = new;
}
if let Some(next) = self.redo.last_mut().filter(|s| s.rev_before == old) {
next.rev_before = new;
}
self.coalesce_open = false;
}
}