use super::*;
use crate::tui::theme::{SyntaxRole, Theme};
use ratatui::style::{Color, Style};
fn highlighter(token: &str) -> BlockHighlighter {
warm_syntax_set();
BlockHighlighter::for_language(token).expect("bundled syntax")
}
fn segment_texts(segments: &[HighlightSegment]) -> Vec<&str> {
segments
.iter()
.map(|segment| segment.text.as_str())
.collect()
}
fn role_of(segments: &[HighlightSegment], text: &str) -> Option<SyntaxRole> {
segments
.iter()
.find(|segment| segment.text.trim() == text)
.unwrap_or_else(|| panic!("no segment for {text:?} in {segments:?}"))
.role
}
#[test]
fn rust_tokens_map_to_distinct_roles() {
for (language, line, keyword) in [
("rust", "let answer = 42; // note", "let"),
("ts", "const answer: number = 42; // note", "const"),
] {
let mut highlighter = highlighter(language);
let segments = highlighter.highlight_line(line);
assert_eq!(segment_texts(&segments).concat(), line, "{language}");
assert_eq!(
role_of(&segments, keyword),
Some(SyntaxRole::Keyword),
"{language}"
);
assert_eq!(
role_of(&segments, "42"),
Some(SyntaxRole::Constant),
"{language}"
);
assert_eq!(
role_of(&segments, "// note"),
Some(SyntaxRole::Comment),
"{language}"
);
}
}
#[test]
fn string_state_carries_across_lines() {
let mut highlighter = highlighter("rust");
highlighter.highlight_line("let text = \"open");
let segments = highlighter.highlight_line("still inside");
assert!(segments
.iter()
.all(|segment| segment.role == Some(SyntaxRole::String)));
}
#[test]
fn fence_tokens_and_aliases_resolve() {
warm_syntax_set();
for token in [
"ts",
"tsx",
"typescript",
"jsx",
"shell",
"console",
"toml",
"ps1",
"powershell",
] {
assert!(
BlockHighlighter::for_language(token).is_some(),
"expected highlighter for fence token {token}"
);
}
assert!(BlockHighlighter::for_language("no-such-language").is_none());
}
#[test]
fn empty_line_yields_one_empty_plain_segment() {
let mut highlighter = highlighter("rust");
let segments = highlighter.highlight_line("");
assert_eq!(segment_texts(&segments), vec![""]);
assert_eq!(segments[0].role, None);
}
#[test]
fn path_extension_resolves_highlighter() {
warm_syntax_set();
for path in [
"src/lib.rs",
"foo.ts",
"pkg/main.py",
"Makefile",
"script.ps1",
] {
assert!(
BlockHighlighter::for_path(path).is_some(),
"expected highlighter for path {path}"
);
}
assert!(BlockHighlighter::for_path("/dev/null").is_none());
assert!(BlockHighlighter::for_path("unknown.nope").is_none());
}
#[test]
fn powershell_cmdlets_do_not_match_double_dash_flags() {
let mut highlighter = highlighter("powershell");
let segments = highlighter.highlight_line("git commit --no-verify; Write-Output 1");
assert_eq!(
role_of(&segments, "Write-Output"),
Some(SyntaxRole::Function)
);
assert!(
segments
.iter()
.filter(|segment| segment.text.contains("no-verify"))
.all(|segment| segment.role != Some(SyntaxRole::Function)),
"double-dash flags must not paint as cmdlets: {segments:?}"
);
}
#[test]
fn highlight_segment_style_uses_caller_plain() {
let plain = Theme::text();
let keyword = HighlightSegment {
text: "let".into(),
role: Some(SyntaxRole::Keyword),
};
let body = HighlightSegment {
text: " x".into(),
role: None,
};
assert_eq!(keyword.style(plain), Theme::syntax(SyntaxRole::Keyword));
assert_eq!(body.style(plain), plain);
let washed = Style::default().fg(Color::White).bg(Color::Rgb(12, 40, 24));
assert_eq!(keyword.style(washed).bg, washed.bg);
assert_eq!(
keyword.style(washed).fg,
Theme::syntax(SyntaxRole::Keyword).fg
);
assert_eq!(body.style(washed), washed);
}
#[test]
fn match_overlay_splits_segments() {
let segments = vec![
HighlightSegment {
text: "let ".into(),
role: Some(SyntaxRole::Keyword),
},
HighlightSegment {
text: "answer".into(),
role: None,
},
HighlightSegment {
text: " = 1".into(),
role: None,
},
];
let ranges = match_byte_ranges(
"let answer = 1",
&MatchQuery::new(
"answer", false, true,
),
);
assert_eq!(ranges, vec![(4, 10)]);
let spans = spans_from_segments_with_matches(&segments, Theme::text(), &ranges);
let answer = spans
.iter()
.find(|span| span.content.as_ref() == "answer")
.expect("answer span");
assert_eq!(answer.style, Theme::search_match(Theme::text()));
}
#[test]
fn literal_match_does_not_treat_dot_as_wildcard() {
for (case, haystack, pattern, literal, case_sensitive, expected) in [
("literal dot", "a.b", ".", true, true, vec![(1, 2)]),
("cased regex", "FOO foo", "foo", false, true, vec![(4, 7)]),
(
"unicode case pair",
"prefix ä suffix",
"Ä",
true,
false,
vec![(7, 9)],
),
(
"caseless literal metacharacters",
"A.B a.b",
"a.b",
true,
false,
vec![(0, 3), (4, 7)],
),
] {
let query = MatchQuery::new(pattern, literal, case_sensitive);
assert_eq!(match_byte_ranges(haystack, &query), expected, "{case}");
}
}