use tess::line_index::LineIndex;
use tess::render::Cell;
use tess::source::{MockSource, Source};
use tess::viewport::Viewport;
fn wrapping_source() -> MockSource {
let s = MockSource::new();
for ch in ['a', 'b', 'c', 'd', 'e'] {
let mut line: String = std::iter::repeat(ch).take(15).collect();
line.push('\n');
s.append(line.as_bytes());
}
s.finish();
s
}
fn body_text(viewport: &mut Viewport, src: &dyn Source) -> Vec<String> {
let mut idx = LineIndex::new();
let frame = viewport.frame(src, &mut idx);
frame
.body
.iter()
.map(|row| {
let mut s = String::new();
for c in row {
match c {
Cell::Char { ch, .. } => s.push(*ch),
Cell::Continuation => {}
Cell::Empty => s.push(' '),
}
}
s.trim_end().to_string()
})
.collect()
}
#[test]
fn goto_bottom_shows_last_line_when_lines_wrap() {
let src = wrapping_source();
let mut idx = LineIndex::new();
let mut v = Viewport::new(10, 4, "mock".to_string());
v.goto_bottom(&src, &mut idx);
let body = body_text(&mut v, &src);
assert_eq!(body, vec!["ddddd", "eeeeeeeeee", "eeeee"]);
assert!(
body.last().unwrap().starts_with('e'),
"last body row must be the final line's tail, got {body:?}"
);
}
#[test]
fn is_at_bottom_false_when_wrapped_tail_is_offscreen() {
let src = wrapping_source();
let mut idx = LineIndex::new();
idx.notice_new_bytes(&src);
let mut v = Viewport::new(10, 4, "mock".to_string());
v.goto_line(2, &src, &mut idx);
assert!(
!v.is_at_bottom(&src, &idx),
"viewport whose tail is still off-screen must not report at-bottom"
);
}
#[test]
fn cannot_scroll_below_the_bottom_anchor() {
let src = wrapping_source();
let mut idx = LineIndex::new();
let mut v = Viewport::new(10, 4, "mock".to_string());
v.scroll_lines(100, &src, &mut idx);
let body = body_text(&mut v, &src);
assert_eq!(
body,
vec!["ddddd", "eeeeeeeeee", "eeeee"],
"scrolling past the end must clamp with the last line at the bottom row"
);
assert!(v.is_at_bottom(&src, &idx));
}
#[test]
fn startup_follow_anchors_end_when_last_line_wraps_many_rows() {
let s = MockSource::new();
s.append(b"one\ntwo\nthree\n");
s.append(&[b'z'; 95]); s.append(b"\n");
s.finish();
let mut idx = LineIndex::new();
let mut v = Viewport::new(10, 6, "mock".to_string()); v.set_follow_mode(true);
idx.notice_new_bytes(&s);
v.extend_visible_lines(&idx, &s);
v.goto_bottom(&s, &mut idx);
let body = body_text(&mut v, &s);
eprintln!("body = {body:?}");
assert_eq!(body.last().unwrap(), "zzzzz", "should show the END of the wrapped last line, got {body:?}");
}
#[test]
fn startup_follow_anchors_end_when_only_last_line_wraps() {
let s = MockSource::new();
s.append(b"alpha\n");
s.append(&[b'q'; 35]); s.append(b"\n");
s.finish();
let mut idx = LineIndex::new();
let mut v = Viewport::new(10, 4, "mock".to_string()); v.set_follow_mode(true);
idx.notice_new_bytes(&s);
v.extend_visible_lines(&idx, &s);
v.goto_bottom(&s, &mut idx);
let body = body_text(&mut v, &s);
eprintln!("body = {body:?}");
assert_eq!(body.last().unwrap(), "qqqqq", "should show the END of the wrapped last line, got {body:?}");
}
#[test]
fn real_file_sources_anchor_wrapping_last_line() {
use tess::source::{FileSource, LiveFileSource};
let dir = std::env::temp_dir().join("tess_wrap_repro");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("f.txt");
let mut content = b"one\ntwo\nthree\n".to_vec();
content.extend_from_slice(&[b'z'; 95]);
content.push(b'\n');
std::fs::write(&path, &content).unwrap();
for which in ["file", "live"] {
let src: Box<dyn Source> = match which {
"file" => Box::new(FileSource::open(&path).unwrap()),
_ => Box::new(LiveFileSource::open(&path).unwrap()),
};
let mut idx = LineIndex::new();
let mut v = Viewport::new(10, 6, "f".to_string()); v.set_follow_mode(true);
src.pump();
idx.notice_new_bytes(src.as_ref());
v.extend_visible_lines(&idx, src.as_ref());
v.goto_bottom(src.as_ref(), &mut idx);
let body = body_text(&mut v, src.as_ref());
eprintln!("[{which}] body = {body:?}");
assert_eq!(
body.last().unwrap(),
"zzzzz",
"[{which}] should show the END of the wrapped last line, got {body:?}"
);
}
}
#[test]
fn last_line_no_trailing_newline_anchors_end() {
let s = MockSource::new();
s.append(b"one\ntwo\nthree\n");
s.append(&[b'z'; 95]); s.finish();
let mut idx = LineIndex::new();
let mut v = Viewport::new(10, 6, "mock".to_string()); v.set_follow_mode(true);
idx.notice_new_bytes(&s);
v.extend_visible_lines(&idx, &s);
v.goto_bottom(&s, &mut idx);
let body = body_text(&mut v, &s);
eprintln!("no-newline body = {body:?}");
assert_eq!(body.last().unwrap(), "zzzzz", "got {body:?}");
}
#[test]
fn hide_mode_wrapping_last_line_shows_end() {
let s = MockSource::new();
s.append(b"DROP me\n");
s.append(b"KEEP short\n");
s.append(b"DROP again\n");
let mut keep = b"KEEP".to_vec();
keep.extend_from_slice(&[b'z'; 36]); keep.push(b'\n');
s.append(&keep);
s.finish();
let mut idx = LineIndex::new();
let mut v = Viewport::new(10, 6, "mock".to_string()); v.set_follow_mode(true);
v.set_grep(Some(
tess::grep::GrepPredicate::compile(
&["^KEEP".to_string()],
tess::viewport::CaseMode::Sensitive,
)
.unwrap(),
));
idx.notice_new_bytes(&s);
v.extend_visible_lines(&idx, &s);
v.goto_bottom(&s, &mut idx);
let body = body_text(&mut v, &s);
eprintln!("hide-mode body = {body:?}");
assert_eq!(body.last().unwrap(), "zzzzzzzzzz", "filtered wrapping last line: END must be at the bottom, got {body:?}");
assert!(v.is_at_bottom(&s, &idx));
}
#[test]
fn count_rows_matches_render_and_no_phantom_line() {
use tess::render::{count_rows, render_line, RenderOpts};
let opts = RenderOpts { cols: 10, ..RenderOpts::default() };
for len in [9usize, 10, 11, 19, 20, 21, 30] {
let line: Vec<u8> = std::iter::repeat(b'x').take(len).collect();
let cr = count_rows(&line, &opts, None);
let rl = render_line(&line, &opts, None).len();
eprintln!("len={len}: count_rows={cr} render_line={rl} {}", if cr==rl {"OK"} else {"MISMATCH"});
assert_eq!(cr, rl, "count_rows != render_line rows for len {len}");
}
let s = MockSource::new();
s.append(b"a\nb\nc\n");
s.finish();
let mut idx = LineIndex::new();
idx.notice_new_bytes(&s);
eprintln!("line_count for \"a\\nb\\nc\\n\" = {}", idx.line_count());
assert_eq!(idx.line_count(), 3, "trailing newline must not add a phantom empty line");
}
#[test]
fn resize_smaller_then_repin_keeps_end_in_view() {
let s = MockSource::new();
s.append(b"one\ntwo\nthree\n");
s.append(&[b'z'; 35]); s.append(b"\n");
s.finish();
let mut idx = LineIndex::new();
let mut v = Viewport::new(10, 12, "mock".to_string());
v.set_follow_mode(true);
idx.notice_new_bytes(&s);
v.extend_visible_lines(&idx, &s);
v.goto_bottom(&s, &mut idx);
assert!(v.is_at_bottom(&s, &idx));
v.resize(10, 5); assert!(!v.is_at_bottom(&s, &idx), "after shrink the tail is off-screen");
v.goto_bottom(&s, &mut idx);
let body = body_text(&mut v, &s);
assert_eq!(body.last().unwrap(), "zzzzz", "END must be at the bottom after re-pin, got {body:?}");
assert!(v.is_at_bottom(&s, &idx));
}