use crate::buffer::{Buffer, Cur};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(super) enum Kind {
Insert,
Delete,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Txt {
Char(char),
String(String),
}
impl Txt {
fn append(&mut self, t: Txt) {
let push = |buf: &mut String| match t {
Txt::Char(c) => buf.push(c),
Txt::String(s) => buf.push_str(&s),
};
match self {
Txt::Char(c) => {
let mut buf = c.to_string();
push(&mut buf);
*self = Txt::String(buf);
}
Txt::String(s) => push(s),
};
}
fn prepend(&mut self, t: Txt) {
let insert = |buf: &mut String| match t {
Txt::Char(c) => buf.insert(0, c),
Txt::String(s) => buf.insert_str(0, &s),
};
match self {
Txt::Char(c) => {
let mut buf = c.to_string();
insert(&mut buf);
*self = Txt::String(buf);
}
Txt::String(s) => insert(s),
};
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Edit {
pub(super) kind: Kind,
pub(super) cur: Cur,
pub(super) txt: Txt,
}
impl Edit {
fn into_undo(mut self) -> Self {
self.kind = match self.kind {
Kind::Insert => Kind::Delete,
Kind::Delete => Kind::Insert,
};
self
}
pub fn string_repr(&self, b: &Buffer) -> String {
let indicator = if self.kind == Kind::Insert { "I" } else { "D" };
let addr = self.cur.as_string_addr(b);
match &self.txt {
Txt::Char(c) => format!("{indicator} {addr} '{}'", c),
Txt::String(s) => format!("{indicator} {addr} '{}'", s.replace('\n', "\\n")),
}
}
fn try_combine(&mut self, e: Edit) -> Option<Edit> {
match (self.kind, e.kind) {
(Kind::Insert, Kind::Insert) => self.try_extend_insert(e),
(Kind::Delete, Kind::Delete) => self.try_extend_delete(e),
_ => Some(e),
}
}
fn try_extend_insert(&mut self, e: Edit) -> Option<Edit> {
if e.cur == self.cur {
self.txt.prepend(e.txt);
None
} else {
match &self.txt {
Txt::Char(_) if e.cur.idx == self.cur.idx + 1 => {
self.txt.append(e.txt);
None
}
Txt::String(s) if e.cur.idx == self.cur.idx + s.len() => {
self.txt.append(e.txt);
None
}
_ => Some(e),
}
}
}
fn try_extend_delete(&mut self, e: Edit) -> Option<Edit> {
if e.cur == self.cur {
self.txt.append(e.txt);
None
} else {
match &e.txt {
Txt::Char(_) if e.cur.idx + 1 == self.cur.idx => {
self.txt.prepend(e.txt);
self.cur = e.cur;
None
}
Txt::String(s) if e.cur.idx + s.len() == self.cur.idx => {
self.txt.prepend(e.txt);
self.cur = e.cur;
None
}
_ => Some(e),
}
}
}
}
pub type Transaction = Vec<Edit>;
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub(crate) struct EditLog {
pub(super) edits: Vec<Transaction>,
pub(super) undone_edits: Vec<Transaction>,
pub(super) paused: bool,
}
impl EditLog {
pub(crate) fn clear(&mut self) {
self.edits.clear();
self.undone_edits.clear();
self.paused = false;
}
pub(crate) fn debug_edits(&self, b: &Buffer) -> Vec<String> {
self.edits
.iter()
.flat_map(|t| t.iter().map(|e| e.string_repr(b)))
.collect()
}
pub(crate) fn undo(&mut self) -> Option<Transaction> {
let mut t = self.edits.pop()?;
while t.is_empty() {
t = self.edits.pop()?;
}
self.undone_edits.push(t.clone());
t.reverse();
Some(t.into_iter().map(|e| e.into_undo()).collect())
}
pub(crate) fn redo(&mut self) -> Option<Transaction> {
let mut t = self.undone_edits.pop()?;
while t.is_empty() {
t = self.edits.pop()?;
}
self.edits.push(t.clone());
Some(t)
}
pub(crate) fn is_empty(&self) -> bool {
self.edits.is_empty()
}
pub(crate) fn insert_char(&mut self, cur: Cur, c: char) {
if !self.paused {
self.push(Edit {
kind: Kind::Insert,
cur,
txt: Txt::Char(c),
});
}
}
pub(crate) fn insert_string(&mut self, cur: Cur, s: String) {
if !self.paused {
self.push(Edit {
kind: Kind::Insert,
cur,
txt: Txt::String(s),
});
}
}
pub(crate) fn delete_char(&mut self, cur: Cur, c: char) {
if !self.paused {
self.push(Edit {
kind: Kind::Delete,
cur,
txt: Txt::Char(c),
});
}
}
pub(crate) fn delete_string(&mut self, cur: Cur, s: String) {
if !self.paused {
self.push(Edit {
kind: Kind::Delete,
cur,
txt: Txt::String(s),
});
}
}
pub(crate) fn new_transaction(&mut self) {
match self.edits.last() {
Some(t) if t.is_empty() => return,
_ => (),
}
self.edits.push(Vec::new());
}
fn push(&mut self, e: Edit) {
self.undone_edits.clear();
if self.edits.is_empty() {
self.edits.push(vec![e]);
return;
}
let transaction = self.edits.last_mut().unwrap();
if transaction.is_empty() {
transaction.push(e);
return;
}
if let Some(e) = transaction.last_mut().unwrap().try_combine(e) {
self.edits.last_mut().unwrap().push(e);
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use simple_test_case::test_case;
pub(crate) fn in_c(idx: usize, c: char) -> Edit {
Edit {
kind: Kind::Insert,
cur: Cur { idx },
txt: Txt::Char(c),
}
}
pub(crate) fn in_s(idx: usize, s: &str) -> Edit {
Edit {
kind: Kind::Insert,
cur: Cur { idx },
txt: Txt::String(s.to_string()),
}
}
pub(crate) fn del_c(idx: usize, c: char) -> Edit {
Edit {
kind: Kind::Delete,
cur: Cur { idx },
txt: Txt::Char(c),
}
}
pub(crate) fn del_s(idx: usize, s: &str) -> Edit {
Edit {
kind: Kind::Delete,
cur: Cur { idx },
txt: Txt::String(s.to_string()),
}
}
#[test_case(
vec![in_c(0, 'a'), in_c(1, 'b')],
&[in_s(0, "ab")];
"run of characters"
)]
#[test_case(
vec![in_c(0, 'a'), in_c(0, 'b')],
&[in_s(0, "ba")];
"run of characters at same cursor"
)]
#[test_case(
vec![in_c(0, 'a'), in_s(1, "bcd")],
&[in_s(0, "abcd")];
"char then string"
)]
#[test_case(
vec![in_c(0, 'a'), in_s(0, "bcd")],
&[in_s(0, "bcda")];
"char then string at same cursor"
)]
#[test_case(
vec![in_s(0, "ab"), in_s(2, "cd")],
&[in_s(0, "abcd")];
"run of strings"
)]
#[test_case(
vec![in_s(0, "ab"), in_s(0, "cd")],
&[in_s(0, "cdab")];
"run of strings at same cursor"
)]
#[test_case(
vec![in_s(0, "abc"), in_c(3, 'd')],
&[in_s(0, "abcd")];
"string then char"
)]
#[test_case(
vec![in_s(0, "abc"), in_c(0, 'd')],
&[in_s(0, "dabc")];
"string then char at same cursor"
)]
#[test]
fn inserts_work(edits: Vec<Edit>, expected: &[Edit]) {
let mut log = EditLog::default();
for e in edits {
log.push(e);
}
assert_eq!(&log.edits, &[expected.to_vec()]);
}
#[test_case(
vec![del_c(1, 'b'), del_c(0, 'a')],
&[del_s(0, "ab")];
"run of chars"
)]
#[test_case(
vec![del_c(0, 'a'), del_c(0, 'b')],
&[del_s(0, "ab")];
"run of characters at same cursor"
)]
#[test_case(
vec![del_c(3, 'd'), del_s(0, "abc")],
&[del_s(0, "abcd")];
"char then string"
)]
#[test_case(
vec![del_c(0, 'a'), del_s(0, "bcd")],
&[del_s(0, "abcd")];
"char then string at same cursor"
)]
#[test_case(
vec![del_s(2, "cde"), del_s(0, "ab")],
&[del_s(0, "abcde")];
"run of strings"
)]
#[test_case(
vec![del_s(0, "abc"), del_s(0, "de")],
&[del_s(0, "abcde")];
"run of strings at same cursor"
)]
#[test_case(
vec![del_s(0, "abc"), del_c(0, 'd')],
&[del_s(0, "abcd")];
"string then char"
)]
#[test_case(
vec![del_s(0, "abc"), del_c(0, 'd')],
&[del_s(0, "abcd")];
"string then char at same cursor"
)]
#[test]
fn delete_work(edits: Vec<Edit>, expected: &[Edit]) {
let mut log = EditLog::default();
for e in edits {
log.push(e);
}
assert_eq!(&log.edits, &[expected.to_vec()]);
}
}