use crate::emacs_core::value::{Value, ValueKind};
pub fn undo_list_is_disabled(undo_list: &Value) -> bool {
undo_list.is_t()
}
pub fn undo_list_record_insert(undo_list: &mut Value, beg: usize, len: usize, pt: usize) {
if undo_list_is_disabled(undo_list) || len == 0 {
return;
}
let at_boundary = undo_list.is_nil() || (undo_list.is_cons() && undo_list.cons_car().is_nil());
if at_boundary && pt != beg {
undo_list_record_point(undo_list, pt);
}
let beg1 = (beg + 1) as i64;
let end1 = (beg + len + 1) as i64;
if undo_list.is_cons() {
let head = undo_list.cons_car();
if head.is_cons() {
let car = head.cons_car();
let cdr = head.cons_cdr();
if let (Some(prev_beg), Some(prev_end)) = (car.as_fixnum(), cdr.as_fixnum()) {
if prev_end == beg1 {
head.set_cdr(Value::fixnum(prev_end + len as i64));
return;
}
if prev_beg == end1 {
head.set_car(Value::fixnum(beg1));
return;
}
let _ = (prev_beg, prev_end); }
}
}
let entry = Value::cons(Value::fixnum(beg1), Value::fixnum(end1));
*undo_list = Value::cons(entry, *undo_list);
}
pub fn undo_list_record_delete(undo_list: &mut Value, beg: usize, text: &str, pt: usize) {
if undo_list_is_disabled(undo_list) || text.is_empty() {
return;
}
let at_boundary = undo_list.is_nil() || (undo_list.is_cons() && undo_list.cons_car().is_nil());
if at_boundary && pt != beg {
undo_list_record_point(undo_list, pt);
}
let pos1 = (beg + 1) as i64;
let stored_pos = if pt == beg + text.len() { -pos1 } else { pos1 };
let entry = Value::cons(Value::string(text), Value::fixnum(stored_pos));
*undo_list = Value::cons(entry, *undo_list);
}
pub fn undo_list_record_point(undo_list: &mut Value, pt: usize) {
if undo_list_is_disabled(undo_list) {
return;
}
let pt1 = Value::fixnum((pt + 1) as i64);
if undo_list.is_cons() {
let head = undo_list.cons_car();
if head == pt1 {
return;
}
}
*undo_list = Value::cons(pt1, *undo_list);
}
pub fn undo_list_record_property_change(
undo_list: &mut Value,
prop: Value,
val: Value,
beg: usize,
end: usize,
) {
if undo_list_is_disabled(undo_list) || beg >= end {
return;
}
let beg1 = Value::fixnum((beg + 1) as i64);
let end1 = Value::fixnum((end + 1) as i64);
let inner = Value::cons(beg1, end1);
let inner = Value::cons(val, inner);
let inner = Value::cons(prop, inner);
let entry = Value::cons(Value::NIL, inner);
*undo_list = Value::cons(entry, *undo_list);
}
pub fn undo_list_record_first_change(undo_list: &mut Value) {
if undo_list_is_disabled(undo_list) {
return;
}
let entry = Value::cons(Value::T, Value::fixnum(0));
*undo_list = Value::cons(entry, *undo_list);
}
pub fn undo_list_boundary(undo_list: &mut Value) {
if undo_list_is_disabled(undo_list) {
return;
}
if undo_list.is_nil() {
return;
}
if undo_list.is_cons() && undo_list.cons_car().is_nil() {
return;
}
*undo_list = Value::cons(Value::NIL, *undo_list);
}
pub fn undo_list_pop_group(undo_list: &mut Value) -> Vec<Value> {
while undo_list.is_cons() && undo_list.cons_car().is_nil() {
*undo_list = undo_list.cons_cdr();
}
let mut group = Vec::new();
while undo_list.is_cons() {
let head = undo_list.cons_car();
if head.is_nil() {
break;
}
group.push(head);
*undo_list = undo_list.cons_cdr();
}
group
}
pub fn undo_list_is_empty(undo_list: &Value) -> bool {
undo_list.is_nil()
}
pub fn undo_list_contains_boundary(undo_list: &Value) -> bool {
let mut cursor = *undo_list;
while cursor.is_cons() {
if cursor.cons_car().is_nil() {
return true;
}
cursor = cursor.cons_cdr();
}
false
}
pub fn undo_list_has_trailing_boundary(undo_list: &Value) -> bool {
undo_list.is_cons() && undo_list.cons_car().is_nil()
}
fn undo_entry_size(entry: &Value) -> usize {
match entry.kind() {
ValueKind::Nil => 0,
ValueKind::Fixnum(_) => 8,
ValueKind::String => entry.as_str().map(|s| s.len()).unwrap_or(8),
_ if entry.is_cons() => {
let car = entry.cons_car();
let cdr = entry.cons_cdr();
let car_size = match car.kind() {
ValueKind::String => car.as_str().map(|s| s.len()).unwrap_or(8),
_ => 8,
};
let cdr_size = match cdr.kind() {
ValueKind::String => cdr.as_str().map(|s| s.len()).unwrap_or(8),
_ => 8,
};
16 + car_size + cdr_size
}
_ => 8,
}
}
pub fn truncate_undo_list(undo_list: Value, undo_limit: usize, undo_strong_limit: usize) -> Value {
if undo_list_is_disabled(&undo_list) || undo_list.is_nil() {
return undo_list;
}
let mut total_size: usize = 0;
let mut past_limit = false;
let mut scan = undo_list;
while scan.is_cons() {
let entry = scan.cons_car();
total_size += undo_entry_size(&entry) + 16;
if total_size > undo_strong_limit {
scan.set_cdr(Value::NIL);
return undo_list;
}
if total_size > undo_limit {
past_limit = true;
}
if past_limit && entry.is_nil() {
scan.set_cdr(Value::NIL);
return undo_list;
}
scan = scan.cons_cdr();
}
undo_list
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_insert_undo() {
crate::test_utils::init_test_tracing();
let mut list = Value::NIL;
undo_list_record_insert(&mut list, 0, 5, 0);
undo_list_record_insert(&mut list, 5, 3, 5);
undo_list_boundary(&mut list);
assert!(undo_list_has_trailing_boundary(&list));
let group = undo_list_pop_group(&mut list);
assert_eq!(group.len(), 1); let entry = group[0];
assert!(entry.is_cons());
assert_eq!(entry.cons_car(), Value::fixnum(1));
assert_eq!(entry.cons_cdr(), Value::fixnum(9));
}
#[test]
fn delete_records_text() {
crate::test_utils::init_test_tracing();
let mut list = Value::NIL;
undo_list_record_delete(&mut list, 3, "hello", 3);
undo_list_boundary(&mut list);
let group = undo_list_pop_group(&mut list);
assert_eq!(group.len(), 1);
let entry = group[0];
assert!(entry.is_cons());
let car = entry.cons_car();
assert!(car.is_string());
assert_eq!(entry.cons_cdr(), Value::fixnum(4));
}
#[test]
fn boundary_separates_groups() {
crate::test_utils::init_test_tracing();
let mut list = Value::NIL;
undo_list_record_insert(&mut list, 0, 1, 0);
undo_list_boundary(&mut list);
undo_list_record_insert(&mut list, 1, 1, 1);
undo_list_boundary(&mut list);
let g2 = undo_list_pop_group(&mut list);
assert_eq!(g2.len(), 1);
let entry = g2[0];
assert!(entry.is_cons());
assert_eq!(entry.cons_car(), Value::fixnum(2)); assert_eq!(entry.cons_cdr(), Value::fixnum(3));
let g1 = undo_list_pop_group(&mut list);
assert_eq!(g1.len(), 1);
let entry = g1[0];
assert!(entry.is_cons());
assert_eq!(entry.cons_car(), Value::fixnum(1)); assert_eq!(entry.cons_cdr(), Value::fixnum(2)); }
#[test]
fn disabled_records_nothing() {
crate::test_utils::init_test_tracing();
let mut list = Value::T;
undo_list_record_insert(&mut list, 0, 5, 0);
assert!(undo_list_is_disabled(&list));
}
#[test]
fn cursor_move_dedup() {
crate::test_utils::init_test_tracing();
let mut list = Value::NIL;
undo_list_record_point(&mut list, 5);
undo_list_record_point(&mut list, 5);
undo_list_record_point(&mut list, 5);
assert!(list.is_cons());
assert_eq!(list.cons_car(), Value::fixnum(6));
assert!(list.cons_cdr().is_nil());
undo_list_record_point(&mut list, 10);
assert!(list.is_cons());
assert_eq!(list.cons_car(), Value::fixnum(11));
}
#[test]
fn no_double_boundary() {
crate::test_utils::init_test_tracing();
let mut list = Value::NIL;
undo_list_record_insert(&mut list, 0, 1, 0);
undo_list_boundary(&mut list);
undo_list_boundary(&mut list);
undo_list_boundary(&mut list);
assert!(undo_list_has_trailing_boundary(&list));
let group = undo_list_pop_group(&mut list);
assert_eq!(group.len(), 1);
}
#[test]
fn to_value_produces_list() {
crate::test_utils::init_test_tracing();
let mut list = Value::NIL;
undo_list_record_insert(&mut list, 0, 5, 0);
undo_list_boundary(&mut list);
assert!(list.is_list());
}
#[test]
fn undoing_flag_not_needed() {
crate::test_utils::init_test_tracing();
let mut list = Value::T; undo_list_record_insert(&mut list, 0, 5, 0);
assert!(undo_list_is_disabled(&list));
let mut list2 = Value::NIL; undo_list_record_insert(&mut list2, 0, 5, 0);
assert!(!undo_list_is_empty(&list2));
}
}