use crate::cell::Cell;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirtyRegion {
pub first_changed: Option<u16>,
pub last_changed: Option<u16>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScrollOp {
pub start: usize,
pub size: usize,
pub shift: isize,
}
impl DirtyRegion {
pub fn clean() -> Self {
Self {
first_changed: None,
last_changed: None,
}
}
pub fn full(width: u16) -> Self {
Self {
first_changed: Some(0),
last_changed: Some(width.saturating_sub(1)),
}
}
pub fn mark(&mut self, start: u16, end: u16) {
match (self.first_changed, self.last_changed) {
(None, None) => {
self.first_changed = Some(start);
self.last_changed = Some(end);
}
(Some(first), Some(last)) => {
self.first_changed = Some(first.min(start));
self.last_changed = Some(last.max(end));
}
_ => unreachable!("Invalid dirty region state"),
}
}
pub fn is_dirty(&self) -> bool {
self.first_changed.is_some()
}
pub fn range(&self) -> Option<(u16, u16)> {
match (self.first_changed, self.last_changed) {
(Some(first), Some(last)) => Some((first, last)),
_ => None,
}
}
}
pub fn find_line_diff(old_line: &[Cell], new_line: &[Cell]) -> Option<(usize, usize)> {
let len = old_line.len();
if len != new_line.len() {
return Some((0, new_line.len().saturating_sub(1)));
}
if len == 0 {
return None;
}
if old_line == new_line {
return None;
}
let mut first_diff = 0;
while first_diff < len && old_line[first_diff] == new_line[first_diff] {
first_diff += 1;
}
if first_diff == len {
return None;
}
let mut last_diff = len - 1;
while last_diff > first_diff && old_line[last_diff] == new_line[last_diff] {
last_diff -= 1;
}
Some((first_diff, last_diff))
}
pub fn hash_line(cells: &[Cell]) -> u64 {
if cells.is_empty() {
return 0;
}
const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
let mut hash = FNV_OFFSET_BASIS;
for cell in cells {
let raw = cell.raw();
for &byte in &raw.to_ne_bytes() {
hash ^= byte as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
}
hash
}
pub fn detect_scrolls(old_hashes: &[u64], new_hashes: &[u64]) -> Vec<ScrollOp> {
let old_len = old_hashes.len();
let new_len = new_hashes.len();
if old_len == 0 || new_len == 0 {
return vec![];
}
let mut old_num: Vec<Option<usize>> = vec![None; new_len];
for new_i in 0..new_len {
let hash = new_hashes[new_i];
if hash == 0 {
continue; }
let new_count = new_hashes.iter().filter(|&&h| h == hash).count();
if new_count != 1 {
continue; }
let old_matches: Vec<usize> = old_hashes
.iter()
.enumerate()
.filter(|(_, h)| **h == hash)
.map(|(i, _)| i)
.collect();
if old_matches.len() == 1 {
old_num[new_i] = Some(old_matches[0]);
}
}
for new_i in 0..new_len {
if let Some(old_i) = old_num[new_i] {
let mut offset = 1;
while new_i + offset < new_len
&& old_i + offset < old_len
&& old_num[new_i + offset].is_none()
&& new_hashes[new_i + offset] == old_hashes[old_i + offset]
&& new_hashes[new_i + offset] != 0
{
old_num[new_i + offset] = Some(old_i + offset);
offset += 1;
}
offset = 1;
while new_i >= offset
&& old_i >= offset
&& old_num[new_i - offset].is_none()
&& new_hashes[new_i - offset] == old_hashes[old_i - offset]
&& new_hashes[new_i - offset] != 0
{
old_num[new_i - offset] = Some(old_i - offset);
offset += 1;
}
}
}
let mut scrolls = Vec::new();
let mut i = 0;
while i < new_len {
if let Some(old_i) = old_num[i] {
let shift = old_i as isize - i as isize;
let start = i;
let mut end = i;
while end + 1 < new_len {
if let Some(next_old) = old_num[end + 1] {
let next_shift = next_old as isize - (end + 1) as isize;
if next_shift == shift {
end += 1;
} else {
break;
}
} else {
break;
}
}
let size = end - start + 1;
let min_efficiency = size + (size / 8).min(2);
let shift_abs = shift.unsigned_abs();
if size >= 3 && min_efficiency >= shift_abs {
scrolls.push(ScrollOp { start, size, shift });
}
i = end + 1;
} else {
i += 1;
}
}
scrolls
}
#[cfg(test)]
mod tests {
use super::*;
use crate::attr::Attr;
use crate::color::Color;
#[test]
fn test_dirty_region_clean() {
let region = DirtyRegion::clean();
assert!(!region.is_dirty());
assert_eq!(region.range(), None);
}
#[test]
fn test_dirty_region_full() {
let region = DirtyRegion::full(80);
assert!(region.is_dirty());
assert_eq!(region.range(), Some((0, 79)));
}
#[test]
fn test_dirty_region_mark_first_time() {
let mut region = DirtyRegion::clean();
region.mark(10, 20);
assert!(region.is_dirty());
assert_eq!(region.range(), Some((10, 20)));
}
#[test]
fn test_dirty_region_mark_expand() {
let mut region = DirtyRegion::clean();
region.mark(10, 20);
region.mark(5, 15);
assert_eq!(region.range(), Some((5, 20)));
}
#[test]
fn test_dirty_region_mark_expand_both_ends() {
let mut region = DirtyRegion::clean();
region.mark(10, 20);
region.mark(5, 25);
assert_eq!(region.range(), Some((5, 25)));
}
#[test]
fn test_find_line_diff_identical() {
let line1 = vec![
Cell::pack('A', Attr::NORMAL, 0, 0),
Cell::pack('B', Attr::NORMAL, 0, 0),
Cell::pack('C', Attr::NORMAL, 0, 0),
];
let line2 = vec![
Cell::pack('A', Attr::NORMAL, 0, 0),
Cell::pack('B', Attr::NORMAL, 0, 0),
Cell::pack('C', Attr::NORMAL, 0, 0),
];
assert_eq!(find_line_diff(&line1, &line2), None);
}
#[test]
fn test_find_line_diff_single_char() {
let line1 = vec![
Cell::pack('A', Attr::NORMAL, 0, 0),
Cell::pack('B', Attr::NORMAL, 0, 0),
Cell::pack('C', Attr::NORMAL, 0, 0),
];
let line2 = vec![
Cell::pack('A', Attr::NORMAL, 0, 0),
Cell::pack('X', Attr::NORMAL, 0, 0),
Cell::pack('C', Attr::NORMAL, 0, 0),
];
assert_eq!(find_line_diff(&line1, &line2), Some((1, 1)));
}
#[test]
fn test_find_line_diff_multiple_chars() {
let line1 = vec![
Cell::pack('A', Attr::NORMAL, 0, 0),
Cell::pack('B', Attr::NORMAL, 0, 0),
Cell::pack('C', Attr::NORMAL, 0, 0),
];
let line2 = vec![
Cell::pack('X', Attr::NORMAL, 0, 0),
Cell::pack('Y', Attr::NORMAL, 0, 0),
Cell::pack('C', Attr::NORMAL, 0, 0),
];
assert_eq!(find_line_diff(&line1, &line2), Some((0, 1)));
}
#[test]
fn test_find_line_diff_all_different() {
let line1 = vec![
Cell::pack('A', Attr::NORMAL, 0, 0),
Cell::pack('B', Attr::NORMAL, 0, 0),
Cell::pack('C', Attr::NORMAL, 0, 0),
];
let line2 = vec![
Cell::pack('X', Attr::NORMAL, 0, 0),
Cell::pack('Y', Attr::NORMAL, 0, 0),
Cell::pack('Z', Attr::NORMAL, 0, 0),
];
assert_eq!(find_line_diff(&line1, &line2), Some((0, 2)));
}
#[test]
fn test_find_line_diff_different_lengths() {
let line1 = vec![
Cell::pack('A', Attr::NORMAL, 0, 0),
Cell::pack('B', Attr::NORMAL, 0, 0),
];
let line2 = vec![
Cell::pack('A', Attr::NORMAL, 0, 0),
Cell::pack('B', Attr::NORMAL, 0, 0),
Cell::pack('C', Attr::NORMAL, 0, 0),
];
assert_eq!(find_line_diff(&line1, &line2), Some((0, 2)));
}
#[test]
fn test_find_line_diff_empty() {
let line1: Vec<Cell> = vec![];
let line2: Vec<Cell> = vec![];
assert_eq!(find_line_diff(&line1, &line2), None);
}
#[test]
fn test_find_line_diff_style_change() {
let line1 = vec![Cell::pack('A', Attr::NORMAL, 0, 0)];
let line2 = vec![Cell::pack('A', Attr::BOLD, 0, 0)];
assert_eq!(find_line_diff(&line1, &line2), Some((0, 0)));
}
#[test]
fn test_hash_line_identical() {
let line1 = vec![
Cell::pack('A', Attr::NORMAL, 0, 0),
Cell::pack('B', Attr::NORMAL, 0, 0),
Cell::pack('C', Attr::NORMAL, 0, 0),
];
let line2 = vec![
Cell::pack('A', Attr::NORMAL, 0, 0),
Cell::pack('B', Attr::NORMAL, 0, 0),
Cell::pack('C', Attr::NORMAL, 0, 0),
];
assert_eq!(hash_line(&line1), hash_line(&line2));
}
#[test]
fn test_hash_line_different_chars() {
let line1 = vec![
Cell::pack('A', Attr::NORMAL, 0, 0),
Cell::pack('B', Attr::NORMAL, 0, 0),
Cell::pack('C', Attr::NORMAL, 0, 0),
];
let line2 = vec![
Cell::pack('X', Attr::NORMAL, 0, 0),
Cell::pack('Y', Attr::NORMAL, 0, 0),
Cell::pack('Z', Attr::NORMAL, 0, 0),
];
assert_ne!(hash_line(&line1), hash_line(&line2));
}
#[test]
fn test_hash_line_different_attrs() {
let line1 = vec![Cell::pack('A', Attr::NORMAL, 0, 0)];
let line2 = vec![Cell::pack('A', Attr::BOLD, 0, 0)];
assert_ne!(hash_line(&line1), hash_line(&line2));
}
#[test]
fn test_hash_line_different_colors() {
let line1 = vec![Cell::pack('A', Attr::NORMAL, 2, 0)];
let line2 = vec![Cell::pack('A', Attr::NORMAL, 5, 0)];
assert_ne!(hash_line(&line1), hash_line(&line2));
}
#[test]
fn test_hash_line_empty() {
let line1: Vec<Cell> = vec![];
let line2: Vec<Cell> = vec![];
assert_eq!(hash_line(&line1), hash_line(&line2));
assert_eq!(hash_line(&line1), 0);
}
#[test]
fn test_hash_line_with_spaces() {
let line1 = vec![
Cell::pack(' ', Attr::NORMAL, 0, 0),
Cell::pack(' ', Attr::NORMAL, 0, 0),
Cell::pack(' ', Attr::NORMAL, 0, 0),
];
let line2 = vec![
Cell::pack(' ', Attr::NORMAL, 0, 0),
Cell::pack(' ', Attr::NORMAL, 0, 0),
Cell::pack(' ', Attr::NORMAL, 0, 0),
];
assert_eq!(hash_line(&line1), hash_line(&line2));
}
#[test]
fn test_detect_scrolls_empty() {
let old: Vec<u64> = vec![];
let new: Vec<u64> = vec![];
let scrolls = detect_scrolls(&old, &new);
assert_eq!(scrolls.len(), 0);
}
#[test]
fn test_detect_scrolls_no_match() {
let old = vec![1, 2, 3, 4, 5];
let new = vec![6, 7, 8, 9, 10];
let scrolls = detect_scrolls(&old, &new);
assert_eq!(scrolls.len(), 0);
}
#[test]
fn test_detect_scrolls_scroll_up() {
let old = vec![1, 2, 3, 100, 101, 102, 103, 104];
let new = vec![100, 101, 102, 103, 104, 4, 5, 6];
let scrolls = detect_scrolls(&old, &new);
assert_eq!(scrolls.len(), 1);
assert_eq!(scrolls[0].start, 0); assert_eq!(scrolls[0].size, 5); assert_eq!(scrolls[0].shift, 3); }
#[test]
fn test_detect_scrolls_scroll_down() {
let old = vec![100, 101, 102, 103, 104];
let new = vec![1, 2, 3, 100, 101, 102, 103, 104];
let scrolls = detect_scrolls(&old, &new);
assert_eq!(scrolls.len(), 1);
assert_eq!(scrolls[0].start, 3); assert_eq!(scrolls[0].size, 5); assert_eq!(scrolls[0].shift, -3); }
#[test]
fn test_detect_scrolls_too_small_hunk() {
let old = vec![1, 100, 101, 2];
let new = vec![100, 101, 3, 4];
let scrolls = detect_scrolls(&old, &new);
assert_eq!(scrolls.len(), 0);
}
#[test]
fn test_detect_scrolls_minimum_hunk_size() {
let old = vec![1, 100, 101, 102, 2];
let new = vec![100, 101, 102, 3, 4];
let scrolls = detect_scrolls(&old, &new);
assert_eq!(scrolls.len(), 1);
assert_eq!(scrolls[0].size, 3);
}
#[test]
fn test_detect_scrolls_ignore_blank_lines() {
let old = vec![0, 0, 100, 101, 102];
let new = vec![100, 101, 102, 0, 0];
let scrolls = detect_scrolls(&old, &new);
assert_eq!(scrolls.len(), 1);
assert_eq!(scrolls[0].start, 0);
assert_eq!(scrolls[0].size, 3);
assert_eq!(scrolls[0].shift, 2); }
#[test]
fn test_detect_scrolls_duplicate_hashes() {
let old = vec![100, 100, 101, 102];
let new = vec![101, 102, 100, 100];
let scrolls = detect_scrolls(&old, &new);
assert_eq!(scrolls.len(), 0);
}
#[test]
fn test_detect_scrolls_grow_matches() {
let old = vec![1, 100, 101, 102, 103, 2];
let new = vec![100, 101, 102, 103, 3, 4];
let scrolls = detect_scrolls(&old, &new);
assert_eq!(scrolls.len(), 1);
assert_eq!(scrolls[0].size, 4); }
#[test]
fn test_detect_scrolls_efficiency_heuristic() {
let old = vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100, 101, 102];
let new = vec![100, 101, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let scrolls = detect_scrolls(&old, &new);
assert_eq!(scrolls.len(), 0);
}
#[test]
fn test_detect_scrolls_multiple_hunks() {
let old = vec![
100, 101, 102, 103, 104, 105, 106, 107, 0, 200, 201, 202, 203, 204, 205, 206,
207,
];
let new = vec![
200, 201, 202, 203, 204, 205, 206, 207, 0, 100, 101, 102, 103, 104, 105, 106,
107,
];
let scrolls = detect_scrolls(&old, &new);
assert_eq!(scrolls.len(), 2);
assert_eq!(scrolls[0].start, 0);
assert_eq!(scrolls[0].size, 8);
assert_eq!(scrolls[0].shift, 9);
assert_eq!(scrolls[1].start, 9);
assert_eq!(scrolls[1].size, 8);
assert_eq!(scrolls[1].shift, -9);
}
}