use crate::common::harness::{EditorTestHarness, HarnessOptions};
use crossterm::event::{KeyCode, KeyModifiers};
use ratatui::style::Color;
use std::path::PathBuf;
fn fixture_path(filename: &str) -> PathBuf {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
PathBuf::from(manifest_dir)
.join("tests/fixtures/syntax_highlighting")
.join(filename)
}
fn collect_highlight_colors(harness: &EditorTestHarness, row_start: u16, row_end: u16) -> usize {
let mut colors = std::collections::HashSet::new();
for y in row_start..row_end {
for x in 8..100 {
if let Some(style) = harness.get_cell_style(x, y) {
if let Some(fg) = style.fg {
match fg {
Color::Indexed(15) => {} Color::Indexed(244) => {} Color::Indexed(237) => {} Color::Indexed(0) => {} Color::Indexed(236) => {} Color::Reset => {}
_ => {
colors.insert(format!("{:?}", fg));
}
}
}
}
}
}
colors.len()
}
fn create_harness() -> EditorTestHarness {
EditorTestHarness::create(
120,
40,
HarnessOptions::new()
.with_project_root()
.with_full_grammar_registry(),
)
.unwrap()
}
fn goto_line(harness: &mut EditorTestHarness, line: usize) {
harness
.send_key(KeyCode::Char('g'), KeyModifiers::CONTROL)
.unwrap();
harness.type_text(&line.to_string()).unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
}
#[test]
fn test_embedded_css_highlighting_at_large_offset() {
let path = fixture_path("embedded_css_long.html");
assert!(path.exists(), "Fixture not found: {}", path.display());
let mut harness = create_harness();
harness.open_file(&path).unwrap();
harness.render().unwrap();
let top_colors = collect_highlight_colors(&harness, 2, 20);
assert!(
top_colors >= 2,
"Sanity check: expected highlighting at top of file, got {} colors",
top_colors
);
goto_line(&mut harness, 405);
harness.assert_screen_contains("display");
harness.assert_screen_contains("background");
let offset_colors = collect_highlight_colors(&harness, 2, 20);
assert!(
offset_colors >= 2,
"CSS inside <style> at large offset (line 405, >10KB from <style> tag) \
should have syntax highlighting, but got only {} distinct highlight colors. \
This indicates the TextMate parser lost embedded language context.",
offset_colors
);
}
#[test]
fn test_embedded_css_highlighting_via_scrolling() {
let path = fixture_path("embedded_css_long.html");
let mut harness = create_harness();
harness.open_file(&path).unwrap();
harness.render().unwrap();
for _ in 0..13 {
harness
.send_key(KeyCode::PageDown, KeyModifiers::NONE)
.unwrap();
}
harness.render().unwrap();
let colors = collect_highlight_colors(&harness, 2, 20);
assert!(
colors >= 2,
"CSS highlighting should work after gradual scrolling, got {} colors",
colors
);
}
#[test]
fn test_embedded_css_highlighting_after_edit() {
let path = fixture_path("embedded_css_long.html");
let mut harness = create_harness();
harness.open_file(&path).unwrap();
harness.render().unwrap();
goto_line(&mut harness, 405);
let colors_before = collect_highlight_colors(&harness, 2, 20);
assert!(
colors_before >= 2,
"Pre-edit: expected CSS highlighting, got {} colors",
colors_before
);
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.type_text(" color: green;").unwrap();
harness.render().unwrap();
let colors_after = collect_highlight_colors(&harness, 2, 20);
assert!(
colors_after >= 2,
"Post-edit: CSS highlighting should survive cache invalidation, got {} colors",
colors_after
);
}
#[test]
fn test_embedded_css_highlighting_after_edit_before_style() {
let path = fixture_path("embedded_css_long.html");
let mut harness = create_harness();
harness.open_file(&path).unwrap();
harness.render().unwrap();
goto_line(&mut harness, 405);
let colors_initial = collect_highlight_colors(&harness, 2, 20);
assert!(
colors_initial >= 2,
"Initial: expected CSS highlighting, got {} colors",
colors_initial
);
goto_line(&mut harness, 1);
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness
.send_key(KeyCode::Enter, KeyModifiers::NONE)
.unwrap();
harness.type_text("<!-- inserted -->").unwrap();
harness.render().unwrap();
goto_line(&mut harness, 406);
let colors_after = collect_highlight_colors(&harness, 2, 20);
assert!(
colors_after >= 2,
"After editing before <style> tag, CSS highlighting should still work \
(checkpoints rebuilt from byte 0), got {} colors",
colors_after
);
}
#[test]
fn test_embedded_css_highlighting_after_delete() {
let path = fixture_path("embedded_css_long.html");
let mut harness = create_harness();
harness.open_file(&path).unwrap();
harness.render().unwrap();
goto_line(&mut harness, 200);
let colors_before = collect_highlight_colors(&harness, 2, 20);
assert!(
colors_before >= 2,
"Pre-delete: expected CSS highlighting, got {} colors",
colors_before
);
harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
for _ in 0..5 {
harness
.send_key(KeyCode::Down, KeyModifiers::SHIFT)
.unwrap();
}
harness
.send_key(KeyCode::Backspace, KeyModifiers::NONE)
.unwrap();
harness.render().unwrap();
let colors_after = collect_highlight_colors(&harness, 2, 20);
assert!(
colors_after >= 2,
"Post-delete: CSS highlighting should survive, got {} colors",
colors_after
);
harness
.type_text(" .new-rule { color: red; }")
.unwrap();
harness.render().unwrap();
let colors_final = collect_highlight_colors(&harness, 2, 20);
assert!(
colors_final >= 2,
"Post-delete+insert: highlighting should work, got {} colors",
colors_final
);
}
#[test]
fn test_no_panic_on_rapid_typing_in_large_rust_file() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let path = std::path::PathBuf::from(manifest_dir).join("src/app/render.rs");
if !path.exists() {
return;
}
let mut harness = create_harness();
harness.open_file(&path).unwrap();
harness.render().unwrap();
goto_line(&mut harness, 4079);
for ch in "// test comment".chars() {
harness
.send_key(KeyCode::Char(ch), KeyModifiers::NONE)
.unwrap();
}
for _ in 0..5 {
harness
.send_key(KeyCode::Backspace, KeyModifiers::NONE)
.unwrap();
}
for ch in "edit".chars() {
harness
.send_key(KeyCode::Char(ch), KeyModifiers::NONE)
.unwrap();
}
harness.render().unwrap();
let colors = collect_highlight_colors(&harness, 2, 20);
assert!(
colors >= 1,
"After rapid typing in large Rust file, should not panic, got {} colors",
colors
);
}
#[test]
fn test_highlighting_near_top_still_works() {
let path = fixture_path("embedded_css_long.html");
let mut harness = create_harness();
harness.open_file(&path).unwrap();
harness.render().unwrap();
let colors = collect_highlight_colors(&harness, 2, 20);
assert!(
colors >= 2,
"Highlighting at top of file should work, got {} colors",
colors
);
}
#[test]
fn test_perf_cache_hit_no_reparse() {
let path = fixture_path("embedded_css_long.html");
let mut harness = create_harness();
harness.open_file(&path).unwrap();
harness.render().unwrap();
goto_line(&mut harness, 200);
harness.reset_highlight_stats();
harness.render().unwrap();
let stats = harness
.highlight_stats()
.expect("should have TextMate stats");
assert!(
stats.cache_hits >= 1,
"Second render without edits should be a cache hit, got {} hits",
stats.cache_hits
);
assert_eq!(
stats.bytes_parsed, 0,
"No bytes should be re-parsed on cache hit, got {}",
stats.bytes_parsed
);
}
#[test]
fn test_perf_single_char_edit_single_parse() {
let path = fixture_path("embedded_css_long.html");
let mut harness = create_harness();
harness.open_file(&path).unwrap();
harness.render().unwrap();
goto_line(&mut harness, 200);
harness.reset_highlight_stats();
harness
.send_key(KeyCode::Char('x'), KeyModifiers::NONE)
.unwrap();
let stats = harness
.highlight_stats()
.expect("should have TextMate stats");
assert_eq!(
stats.cache_misses, 1,
"Single char edit should cause exactly 1 cache miss, got {}",
stats.cache_misses
);
assert!(
stats.bytes_parsed < 50_000,
"Single char edit should parse < 50KB (single pass), got {} bytes",
stats.bytes_parsed
);
}
#[test]
fn test_perf_convergence_after_state_change() {
let path = fixture_path("embedded_css_long.html");
let mut harness = create_harness();
harness.open_file(&path).unwrap();
harness.render().unwrap();
goto_line(&mut harness, 200);
harness
.send_key(KeyCode::Char('"'), KeyModifiers::NONE)
.unwrap();
harness
.send_key(KeyCode::Char('a'), KeyModifiers::NONE)
.unwrap();
harness.reset_highlight_stats();
for ch in "hello".chars() {
harness
.send_key(KeyCode::Char(ch), KeyModifiers::NONE)
.unwrap();
}
let stats = harness
.highlight_stats()
.expect("should have TextMate stats");
assert!(
stats.bytes_parsed < 10_000,
"5 keystrokes after state stabilization should parse < 10KB total \
(convergence after ~256 bytes each), got {} bytes (avg {} per keystroke)",
stats.bytes_parsed,
stats.bytes_parsed / 5
);
assert!(
stats.convergences >= 5,
"5 keystrokes should each converge at least once, got {} convergences",
stats.convergences
);
assert!(
stats.checkpoints_updated <= stats.convergences,
"Should update fewer checkpoints than convergences, got {} updates vs {} convergences",
stats.checkpoints_updated,
stats.convergences
);
}
#[test]
fn test_perf_no_highlight_drift_after_typing() {
let path = fixture_path("embedded_css_long.html");
let mut harness = create_harness();
harness.open_file(&path).unwrap();
harness.render().unwrap();
goto_line(&mut harness, 200);
harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
harness
.send_key(KeyCode::Char('x'), KeyModifiers::NONE)
.unwrap();
let colors_before: Vec<_> = (8..60)
.filter_map(|x| {
harness
.get_cell_style(x, 15)
.and_then(|s| s.fg)
.map(|fg| (x, format!("{:?}", fg)))
})
.collect();
harness.reset_highlight_stats();
for ch in "0123456789".chars() {
harness
.send_key(KeyCode::Char(ch), KeyModifiers::NONE)
.unwrap();
}
harness.render().unwrap();
let stats = harness
.highlight_stats()
.expect("should have TextMate stats");
assert!(
stats.convergences >= 1,
"Expected convergence to kick in during typing, got {} convergences. \
Without convergence the span offset adjustment isn't exercised.",
stats.convergences
);
let colors_after: Vec<_> = (8..60)
.filter_map(|x| {
harness
.get_cell_style(x, 15)
.and_then(|s| s.fg)
.map(|fg| (x, format!("{:?}", fg)))
})
.collect();
assert_eq!(
colors_before, colors_after,
"Highlight colors on lines after the edit should not drift after typing. \
This indicates cached span byte offsets are not being adjusted for inserts."
);
}