use std::ops::Range;
use std::time::{Duration, Instant};
use rdom_core::{NodeId, Position};
pub const COALESCE_WINDOW: Duration = Duration::from_millis(500);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EditKind {
Insert,
Delete,
Replace,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EditEntry {
pub node: NodeId,
pub range: Range<usize>,
pub old: String,
pub new: String,
pub caret_before: Position,
pub caret_after: Position,
pub kind: EditKind,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct EditorState {
undo: Vec<EditEntry>,
redo: Vec<EditEntry>,
last_commit: Option<Instant>,
sticky_x: Option<u16>,
}
impl EditorState {
pub fn new() -> Self {
Self::default()
}
pub fn sticky_x(&self) -> Option<u16> {
self.sticky_x
}
pub fn set_sticky_x(&mut self, x: u16) {
self.sticky_x = Some(x);
}
pub fn clear_sticky_x(&mut self) {
self.sticky_x = None;
}
pub fn record(&mut self, entry: EditEntry, now: Instant) {
self.redo.clear();
if Self::can_coalesce_with_previous(&self.undo, &entry, self.last_commit, now) {
let top = self.undo.last_mut().unwrap();
Self::extend_in_place(top, &entry);
} else {
self.undo.push(entry);
}
self.last_commit = Some(now);
}
pub fn pop_undo(&mut self) -> Option<EditEntry> {
let entry = self.undo.pop()?;
self.last_commit = None;
Some(entry)
}
pub fn pop_redo(&mut self) -> Option<EditEntry> {
let entry = self.redo.pop()?;
self.last_commit = None;
Some(entry)
}
pub fn push_redo(&mut self, entry: EditEntry) {
self.redo.push(entry);
}
pub fn push_undo(&mut self, entry: EditEntry) {
self.undo.push(entry);
}
pub fn undo_depth(&self) -> usize {
self.undo.len()
}
pub fn redo_depth(&self) -> usize {
self.redo.len()
}
fn can_coalesce_with_previous(
undo: &[EditEntry],
new_entry: &EditEntry,
last_commit: Option<Instant>,
now: Instant,
) -> bool {
if new_entry.kind != EditKind::Insert {
return false;
}
let Some(top) = undo.last() else { return false };
if top.kind != EditKind::Insert {
return false;
}
if top.node != new_entry.node {
return false;
}
if new_entry.range.start != top.caret_after.offset {
return false;
}
let Some(last) = last_commit else {
return false;
};
if now.duration_since(last) > COALESCE_WINDOW {
return false;
}
true
}
fn extend_in_place(top: &mut EditEntry, incoming: &EditEntry) {
top.new.push_str(&incoming.new);
top.caret_after = incoming.caret_after;
}
}
#[cfg(test)]
mod tests;