use std::ops::Range;
use rdom_core::{InputType, NodeId, Position, SelectionSerial};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EditKind {
Insert,
DeleteBackward,
DeleteForward,
Standalone,
}
impl EditKind {
pub fn of(input_type: &InputType) -> Self {
match input_type {
InputType::InsertText | InputType::InsertLineBreak | InputType::InsertParagraph => {
Self::Insert
}
InputType::DeleteContentBackward => Self::DeleteBackward,
InputType::DeleteContentForward => Self::DeleteForward,
_ => Self::Standalone,
}
}
}
#[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,
}
pub type HistoryItem = Vec<EditEntry>;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct EditorState {
undo: Vec<HistoryItem>,
redo: Vec<HistoryItem>,
open_group: Option<SelectionSerial>,
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,
selection_before: SelectionSerial,
selection_after: SelectionSerial,
) {
self.redo.clear();
let open = self.open_group == Some(selection_before);
let joins = open
&& match self.undo.last() {
Some(top) if top.len() == 1 => Self::continues(&top[0], &entry),
_ => false,
};
let kind = entry.kind;
if joins {
let top = &mut self.undo.last_mut().expect("joins implies a top step")[0];
Self::extend_in_place(top, &entry);
} else {
self.undo.push(vec![entry]);
}
self.open_group = match kind {
EditKind::Standalone => None,
_ => Some(selection_after),
};
}
pub fn record_compound(&mut self, parts: HistoryItem) {
if parts.is_empty() {
return;
}
self.redo.clear();
self.undo.push(parts);
self.open_group = None;
}
pub fn pop_undo(&mut self) -> Option<HistoryItem> {
let entry = self.undo.pop()?;
self.open_group = None;
Some(entry)
}
pub fn pop_redo(&mut self) -> Option<HistoryItem> {
let entry = self.redo.pop()?;
self.open_group = None;
Some(entry)
}
pub fn push_redo(&mut self, entry: HistoryItem) {
self.redo.push(entry);
}
pub fn push_undo(&mut self, entry: HistoryItem) {
self.undo.push(entry);
}
pub fn undo_depth(&self) -> usize {
self.undo.len()
}
pub fn redo_depth(&self) -> usize {
self.redo.len()
}
fn continues(top: &EditEntry, next: &EditEntry) -> bool {
if top.kind != next.kind || top.node != next.node {
return false;
}
match next.kind {
EditKind::Insert => next.range.is_empty() && next.range.start == top.caret_after.offset,
EditKind::DeleteBackward => {
top.new.is_empty() && next.new.is_empty() && next.range.end == top.range.start
}
EditKind::DeleteForward => {
top.new.is_empty() && next.new.is_empty() && next.range.start == top.range.start
}
EditKind::Standalone => false,
}
}
fn extend_in_place(top: &mut EditEntry, next: &EditEntry) {
match next.kind {
EditKind::Insert => top.new.push_str(&next.new),
EditKind::DeleteBackward => {
top.old.insert_str(0, &next.old);
top.range.start = next.range.start;
}
EditKind::DeleteForward => {
top.old.push_str(&next.old);
top.range.end += next.old.len();
}
EditKind::Standalone => unreachable!("standalone edits never join a group"),
}
top.caret_after = next.caret_after;
}
}
#[cfg(test)]
mod tests;