use super::*;
#[test]
fn viewport_can_start_inside_wrapped_logical_line() {
let lines = vec!["abcdefghijkl".to_owned(), "next".to_owned()];
let request = RenderRequest {
context: RenderContext {
gutter: GutterLayout::new(1, false),
x: 0,
width: 4,
wrap: true,
mode: FormatKind::Json,
},
row_limit: 8,
};
let mut cache = RenderedLineCache::default();
let first = render_viewport(
&lines,
1,
0,
2,
request,
&mut cache,
ViewportRenderOptions::default(),
);
assert_eq!(first.last_line_number, Some(1));
assert_eq!(span_text(&first.lines[0].spans), "1 │ abcd");
assert_eq!(span_text(&first.lines[1].spans), " ┆ efgh");
let second = render_viewport(
&lines,
1,
1,
2,
request,
&mut cache,
ViewportRenderOptions::default(),
);
assert_eq!(second.last_line_number, Some(1));
assert_eq!(span_text(&second.lines[0].spans), " ┆ efgh");
assert_eq!(span_text(&second.lines[1].spans), " ┆ ijkl");
}
#[test]
fn viewport_reports_actual_last_logical_line() {
let lines = vec!["abcdefghijkl".to_owned(), "next".to_owned()];
let request = RenderRequest {
context: RenderContext {
gutter: GutterLayout::new(1, false),
x: 0,
width: 4,
wrap: true,
mode: FormatKind::Json,
},
row_limit: 8,
};
let mut cache = RenderedLineCache::default();
let viewport = render_viewport(
&lines,
1,
2,
3,
request,
&mut cache,
ViewportRenderOptions::default(),
);
assert_eq!(viewport.last_line_number, Some(2));
assert_eq!(span_text(&viewport.lines[0].spans), " ┆ ijkl");
assert_eq!(span_text(&viewport.lines[1].spans), "2 │ next");
}
#[test]
fn markdown_viewport_reuses_inner_code_highlighter() {
let lines = vec![
"```json".to_owned(),
r#"{"ok": true}"#.to_owned(),
"```".to_owned(),
];
let line_modes = vec![FormatKind::Markdown, FormatKind::Json, FormatKind::Markdown];
let request = RenderRequest {
context: RenderContext {
gutter: GutterLayout::new(1, false),
x: 0,
width: 80,
wrap: false,
mode: FormatKind::Markdown,
},
row_limit: 8,
};
let mut cache = RenderedLineCache::default();
let viewport = render_viewport(
&lines,
1,
0,
3,
request,
&mut cache,
ViewportRenderOptions {
line_modes: Some(&line_modes),
search_query: None,
},
);
assert_eq!(span_text(&viewport.lines[1].spans), r#"2 │ {"ok": true}"#);
assert_eq!(
styles_for_text(&viewport.lines[1].spans, r#""ok""#),
vec![key_style()]
);
assert_eq!(
styles_for_text(&viewport.lines[1].spans, "true"),
vec![bool_style()]
);
}
#[test]
fn json_viewport_shows_chat_role_gutter_on_message_start() {
let lines = vec![
"[".to_owned(),
" {".to_owned(),
r#" "role": "user","#.to_owned(),
r#" "content": "hello""#.to_owned(),
" }".to_owned(),
"]".to_owned(),
];
let request = RenderRequest {
context: RenderContext {
gutter: GutterLayout::new(1, true),
x: 0,
width: 80,
wrap: false,
mode: FormatKind::Json,
},
row_limit: 8,
};
let mut cache = RenderedLineCache::default();
let viewport = render_viewport(
&lines,
1,
0,
6,
request,
&mut cache,
ViewportRenderOptions::default(),
);
assert_eq!(span_text(&viewport.lines[0].spans), "1 │ │ [");
assert_eq!(span_text(&viewport.lines[1].spans), "2 │ U │ {");
assert_eq!(
span_text(&viewport.lines[2].spans),
r#"3 │ │ "role": "user","#
);
}
#[test]
fn json_chat_role_gutter_adapts_to_available_width() {
let file = indexed_lines(&[r#"{"role":"assistant"}"#]);
let state = ViewState::default();
let wide = draw_layout(
ratatui::layout::Size::new(80, 20),
&file,
&state,
FormatKind::Json,
);
assert!(wide.context.gutter.chat_role_enabled());
assert_eq!(wide.gutter_width, 8);
let narrow = draw_layout(
ratatui::layout::Size::new(61, 20),
&file,
&state,
FormatKind::Json,
);
assert!(!narrow.context.gutter.chat_role_enabled());
assert_eq!(narrow.gutter_width, 4);
}
#[test]
fn markdown_json_code_does_not_enable_chat_role_gutter() {
let lines = vec![
"```json".to_owned(),
r#"{"role":"assistant"}"#.to_owned(),
"```".to_owned(),
];
let line_modes = vec![FormatKind::Markdown, FormatKind::Json, FormatKind::Markdown];
let request = RenderRequest {
context: RenderContext {
gutter: GutterLayout::new(1, false),
x: 0,
width: 80,
wrap: false,
mode: FormatKind::Markdown,
},
row_limit: 8,
};
let mut cache = RenderedLineCache::default();
let viewport = render_viewport(
&lines,
1,
0,
3,
request,
&mut cache,
ViewportRenderOptions {
line_modes: Some(&line_modes),
search_query: None,
},
);
assert_eq!(
span_text(&viewport.lines[1].spans),
r#"2 │ {"role":"assistant"}"#
);
}
#[test]
fn markdown_mode_cache_resolves_fence_state_before_window() {
let mut temp = NamedTempFile::new().unwrap();
writeln!(temp, "# notes").unwrap();
writeln!(temp, "```toml").unwrap();
writeln!(temp, "[viewer]").unwrap();
writeln!(temp, "mode = \"markdown\"").unwrap();
writeln!(temp, "```").unwrap();
let file = IndexedTempFile::new("notes".to_owned(), temp).unwrap();
let lines = file.read_window(2, 1).unwrap();
let mut cache = MarkdownModeCache::default();
let modes = cache
.line_modes(&file, 2, &lines, FormatKind::Markdown)
.unwrap()
.unwrap();
assert_eq!(modes, vec![FormatKind::Toml]);
}
#[test]
fn markdown_mode_cache_keeps_interval_checkpoints_only() {
let mut temp = NamedTempFile::new().unwrap();
for index in 0..1_600 {
if index == 10 {
writeln!(temp, "```toml").unwrap();
} else if index == 1_200 {
writeln!(temp, "```").unwrap();
} else {
writeln!(temp, "line {index}").unwrap();
}
}
let file = IndexedTempFile::new("notes".to_owned(), temp).unwrap();
let mut cache = MarkdownModeCache::default();
for start in 0..1_500 {
let lines = file.read_window(start, 1).unwrap();
cache
.line_modes(&file, start, &lines, FormatKind::Markdown)
.unwrap();
}
assert_eq!(cache.checkpoint_count(), 3);
}
#[test]
fn wrapped_progress_advances_by_visible_bytes() {
let mut temp = NamedTempFile::new().unwrap();
writeln!(temp, "abcdefghijkl").unwrap();
writeln!(temp, "next").unwrap();
let file = IndexedTempFile::new("test".to_owned(), temp).unwrap();
let context = RenderContext {
gutter: GutterLayout::new(1, false),
x: 0,
width: 4,
wrap: true,
mode: FormatKind::Json,
};
assert_eq!(
viewer_progress_percent(
&file,
context,
1,
Some(ViewportBottom {
line_index: 0,
byte_end: 8,
line_end: false,
}),
),
44
);
assert_eq!(
viewer_progress_percent(
&file,
context,
1,
Some(ViewportBottom {
line_index: 0,
byte_end: 12,
line_end: true,
}),
),
72
);
assert_eq!(
viewer_progress_percent(
&file,
context,
2,
Some(ViewportBottom {
line_index: 1,
byte_end: 4,
line_end: true,
}),
),
100
);
}
#[test]
fn tail_position_keeps_nowrap_last_page_full() {
assert_eq!(last_full_logical_page_top(10, 3), 7);
assert_eq!(last_full_logical_page_top(2, 5), 0);
}
#[test]
fn wrapped_tail_position_can_start_inside_last_line() {
let mut temp = NamedTempFile::new().unwrap();
writeln!(temp, "prev").unwrap();
writeln!(temp, "abcdefghijkl").unwrap();
let file = IndexedTempFile::new("test".to_owned(), temp).unwrap();
let context = RenderContext {
gutter: GutterLayout::new(1, false),
x: 0,
width: 4,
wrap: true,
mode: FormatKind::Json,
};
let tail = compute_tail_position(&file, 2, context).unwrap();
assert_eq!(
tail,
ViewPosition {
top: 1,
row_offset: 1
}
);
}
#[test]
fn wrapped_tail_view_renders_last_full_page() {
let mut temp = NamedTempFile::new().unwrap();
writeln!(temp, "{{").unwrap();
writeln!(temp, "abcdefghijkl").unwrap();
writeln!(temp, "}}").unwrap();
let file = IndexedTempFile::new("test".to_owned(), temp).unwrap();
let context = RenderContext {
gutter: GutterLayout::new(1, false),
x: 0,
width: 4,
wrap: true,
mode: FormatKind::Json,
};
let request = RenderRequest {
context,
row_limit: 8,
};
let tail = compute_tail_position(&file, 3, context).unwrap();
let lines = file.read_window(tail.top, 3).unwrap();
let mut cache = RenderedLineCache::default();
let viewport = render_viewport(
&lines,
tail.top + 1,
tail.row_offset,
3,
request,
&mut cache,
ViewportRenderOptions::default(),
);
assert_eq!(
tail,
ViewPosition {
top: 1,
row_offset: 1
}
);
assert_eq!(viewport.lines.len(), 3);
assert_eq!(viewport.last_line_number, Some(3));
assert!(viewport_reaches_file_end(&viewport, file.line_count()));
assert!(
tail.row_offset > top_line_tail_offset(tail.top + 1, 3, context, &cache),
"global file tail may need a deeper offset than the top line's own full-page tail"
);
assert_eq!(
effective_top_row_offset(tail.top + 1, 3, context, &cache, Some(tail)),
tail.row_offset
);
assert_eq!(span_text(&viewport.lines[0].spans), " ┆ efgh");
assert_eq!(span_text(&viewport.lines[1].spans), " ┆ ijkl");
assert_eq!(span_text(&viewport.lines[2].spans), "3 │ }");
}
#[test]
fn eof_wrap_offset_clamps_to_last_full_page() {
let mut temp = NamedTempFile::new().unwrap();
writeln!(temp, "abcdefghijkl").unwrap();
let file = IndexedTempFile::new("test".to_owned(), temp).unwrap();
let context = RenderContext {
gutter: GutterLayout::new(1, false),
x: 0,
width: 4,
wrap: true,
mode: FormatKind::Json,
};
let request = RenderRequest {
context,
row_limit: 8,
};
let tail = compute_tail_position(&file, 2, context).unwrap();
let lines = file.read_window(0, 2).unwrap();
let mut cache = RenderedLineCache::default();
let partial = render_viewport(
&lines,
1,
2,
2,
request,
&mut cache,
ViewportRenderOptions::default(),
);
let max_offset = effective_top_row_offset(1, 2, context, &cache, Some(tail));
let clamped = render_viewport(
&lines,
1,
max_offset,
2,
request,
&mut cache,
ViewportRenderOptions::default(),
);
let progress = viewer_progress_percent(&file, context, 1, clamped.bottom);
assert_eq!(
tail,
ViewPosition {
top: 0,
row_offset: 1
}
);
assert!(viewport_reaches_file_end(&partial, file.line_count()));
assert_eq!(partial.lines.len(), 1);
assert_eq!(max_offset, 1);
assert_eq!(clamped.lines.len(), 2);
assert_eq!(progress, 100);
assert_eq!(span_text(&clamped.lines[0].spans), " ┆ efgh");
assert_eq!(span_text(&clamped.lines[1].spans), " ┆ ijkl");
}