use std::path::Path;
use std::str::FromStr;
use super::*;
fn test_grammar() -> Grammar {
Grammar::from_str(
r#"{
"name":"Test",
"scopeName":"source.test",
"fileTypes":["test"],
"firstLineMatch":"^#!.*test",
"patterns":[
{"match":"\\blet\\b","name":"keyword.control.test"},
{"begin":"/\\*","end":"\\*/","name":"comment.block.test"}
],
"repository":{}
}"#,
)
.unwrap()
}
fn test_theme() -> Theme {
Theme::from_str(
r##"{
"name":"Test Theme",
"settings":[
{"settings":{"foreground":"#010203"}},
{"scope":"keyword.control","settings":{"foreground":"#0a0b0c","fontStyle":"bold"}},
{"scope":["comment.block.test"],"settings":{"foreground":"#222222","fontStyle":"italic"}}
]
}"##,
)
.unwrap()
}
fn scope_names<'a>(grammar: &'a Grammar, spans: &'a [ScopeSpan]) -> Vec<&'a str> {
spans
.iter()
.filter_map(|span| grammar.scopes.get(span.scope.index()).map(String::as_str))
.collect()
}
#[test]
fn grammar_parses_textmate_keys_and_first_line_match() {
let grammar = test_grammar();
assert_eq!(grammar.name, "Test");
assert!(grammar.matches_name("test"));
assert!(grammar.matches_path(Path::new("main.test")));
assert!(grammar.matches_first_line("#!/usr/bin/env test"));
}
#[test]
fn tokenizer_preserves_multiline_state_incrementally() {
let grammar = test_grammar();
let mut state = LineState::default();
let first = grammar.tokenize_line(&mut state, "let x = /* comment");
let second = grammar.tokenize_line(&mut state, "still */ let y = 1");
assert_eq!(state.depth(), 0);
assert!(
scope_names(&grammar, &first)
.iter()
.any(|scope| scope.starts_with("keyword.control"))
);
assert!(
scope_names(&grammar, &first)
.iter()
.any(|scope| scope.starts_with("comment.block"))
);
assert!(
scope_names(&grammar, &second)
.iter()
.any(|scope| scope.starts_with("comment.block"))
);
assert!(
scope_names(&grammar, &second)
.iter()
.any(|scope| scope.starts_with("keyword.control"))
);
}
#[test]
fn scoped_nested_rules_keep_state_even_when_container_has_no_scope() {
let grammar = Grammar::from_str(
r#"{
"name":"Test",
"scopeName":"source.test",
"patterns":[
{
"begin":"BEGIN",
"end":"END",
"patterns":[{"match":"\\bx\\b","name":"variable.other.test"}]
}
],
"repository":{}
}"#,
)
.unwrap();
let mut state = LineState::default();
let first = grammar.tokenize_line(&mut state, "BEGIN");
assert!(first.is_empty());
assert_eq!(state.depth(), 1);
let second = grammar.tokenize_line(&mut state, "x");
assert_eq!(state.depth(), 1);
let third = grammar.tokenize_line(&mut state, "END");
assert_eq!(state.depth(), 0);
assert!(scope_names(&grammar, &second).contains(&"variable.other.test"));
assert!(third.is_empty());
}
#[test]
fn dynamic_end_backrefs_are_resolved_from_the_begin_match() {
let grammar = Grammar::from_str(
r#"{
"name":"Test",
"scopeName":"source.test",
"patterns":[
{
"begin":"<([A-Za-z]+)>",
"end":"</\\1>",
"name":"meta.tag.test"
}
],
"repository":{}
}"#,
)
.unwrap();
let mut state = LineState::default();
let _ = grammar.tokenize_line(&mut state, "<section>");
assert_eq!(state.depth(), 1);
let _ = grammar.tokenize_line(&mut state, "</section>");
assert!(state.is_empty());
}
#[test]
fn dynamic_end_backrefs_escape_captured_regex_metacharacters() {
let grammar = Grammar::from_str(
r#"{
"name":"Test",
"scopeName":"source.test",
"patterns":[
{
"begin":"<([A-Za-z.]+)>",
"end":"</\\1>",
"name":"meta.tag.test"
}
],
"repository":{}
}"#,
)
.unwrap();
let mut state = LineState::default();
let _ = grammar.tokenize_line(&mut state, "<foo.bar>");
assert_eq!(state.depth(), 1);
let _ = grammar.tokenize_line(&mut state, "</fooXbar>");
assert_eq!(state.depth(), 1);
let _ = grammar.tokenize_line(&mut state, "</foo.bar>");
assert!(state.is_empty());
}
#[test]
fn tokenizer_preserves_left_context_for_lookbehind_matches() {
let grammar = Grammar::from_str(
r#"{
"name":"Test",
"scopeName":"source.test",
"patterns":[
{"match":"foo","name":"keyword.control.test"},
{"match":"(?<=foo)bar","name":"entity.name.test"}
],
"repository":{}
}"#,
)
.unwrap();
let mut state = LineState::default();
let spans = grammar.tokenize_line(&mut state, "foobar");
let scopes = scope_names(&grammar, &spans);
assert!(scopes.contains(&"keyword.control.test"));
assert!(scopes.contains(&"entity.name.test"));
}
#[test]
fn nested_patterns_only_match_inside_their_parent_rule() {
let grammar = Grammar::from_str(
r#"{
"name":"Test",
"scopeName":"source.test",
"patterns":[
{
"begin":"\"",
"end":"\"",
"name":"string.quoted.double.test",
"patterns":[{"match":"\\\\.","name":"constant.character.escape.test"}]
}
],
"repository":{}
}"#,
)
.unwrap();
let mut state = LineState::default();
let outside = grammar.tokenize_line(&mut state, r#"outside \n"#);
assert!(!scope_names(&grammar, &outside).contains(&"constant.character.escape.test"));
let inside = grammar.tokenize_line(&mut state, r#""inside \n""#);
assert!(scope_names(&grammar, &inside).contains(&"constant.character.escape.test"));
}
#[test]
fn explicit_match_captures_still_emit_capture_scopes() {
let grammar = Grammar::from_str(
r#"{
"name":"Test",
"scopeName":"source.test",
"patterns":[
{
"match":"(foo)(bar)",
"name":"meta.word.test",
"captures":{"1":{"name":"entity.name.function.test"}}
}
],
"repository":{}
}"#,
)
.unwrap();
let mut state = LineState::default();
let spans = grammar.tokenize_line(&mut state, "foobar");
let scopes = scope_names(&grammar, &spans);
assert!(scopes.contains(&"meta.word.test"));
assert!(scopes.contains(&"entity.name.function.test"));
}
#[test]
fn line_buffer_reuses_storage_for_incremental_highlighting() {
let grammar = test_grammar();
let theme = test_theme();
let mut state = LineState::default();
let mut buffer = LineBuffer::default();
let (scopes, styles) = buffer.highlight(&grammar, &theme, &mut state, "let x = 1");
assert!(scopes.iter().any(
|span| grammar.scopes.get(span.scope.index()).map(String::as_str)
== Some("keyword.control.test")
));
assert!(styles.iter().any(|span| {
span.style.foreground
== Some(Rgb {
r: 10,
g: 11,
b: 12,
})
}));
}
#[test]
fn blob_highlighter_outputs_only_requested_range() {
let grammar = test_grammar();
let text = "let a\n/* comment\ninside\n*/ let b\nlet c";
let mut blob = BlobHighlighter::new(&grammar, text);
let mut visited = Vec::new();
let mut inside_comment = false;
blob.highlight_range(2..4, |line| {
visited.push(line.line_index);
if line.line_index == 2 {
inside_comment = line.scopes.iter().any(|span| {
grammar.scopes.get(span.scope.index()).map(String::as_str)
== Some("comment.block.test")
});
}
});
assert_eq!(visited, vec![2, 3]);
assert!(inside_comment);
assert!(blob.is_state_cached(2));
assert!(blob.is_state_cached(4));
}
#[test]
fn blob_highlighter_collects_owned_ranges() {
let grammar = test_grammar();
let mut blob = BlobHighlighter::new(&grammar, "let a\nlet b\n");
let lines = blob.highlighted_range(1..3);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].line_index, 1);
assert_eq!(lines[0].byte_range, 6..11);
}
#[test]
fn styled_range_applies_theme_without_returning_unrequested_lines() {
let grammar = test_grammar();
let theme = test_theme();
let mut blob = BlobHighlighter::new(&grammar, "let a\nlet b");
let mut styled = Vec::new();
blob.highlight_styled_range(&theme, 1..2, |line| {
styled.push((
line.line_index,
line.styles
.iter()
.any(|span| span.style.font_style.contains(FontStyle::BOLD)),
));
});
assert_eq!(styled, vec![(1, true)]);
}
#[test]
fn rgb_parses_six_digit_hex_colors() {
assert_eq!("#01020a".parse::<Rgb>().unwrap(), Rgb { r: 1, g: 2, b: 10 });
assert!("01020a".parse::<Rgb>().is_err());
assert!("#01020".parse::<Rgb>().is_err());
assert!("#01020x".parse::<Rgb>().is_err());
}
#[test]
fn font_style_parses_textmate_flags() {
let style = "bold italic underline strikethrough unknown"
.parse::<FontStyle>()
.unwrap();
assert!(style.contains(FontStyle::BOLD));
assert!(style.contains(FontStyle::ITALIC));
assert!(style.contains(FontStyle::UNDERLINE));
assert!(style.contains(FontStyle::STRIKETHROUGH));
}
#[test]
fn theme_clamps_invalid_utf8_span_boundaries() {
let grammar = test_grammar();
let theme = test_theme();
let keyword = grammar
.scopes
.iter()
.position(|scope| scope == "keyword.control.test")
.map(ScopeId::new)
.unwrap();
let spans = [ScopeSpan {
start: 1,
end: 2,
scope: keyword,
}];
let styles = theme.style_spans(&grammar, "éx", &spans);
assert_eq!(
styles,
vec![StyleSpan {
start: 0,
end: "éx".len(),
scope: None,
style: theme.default,
}]
);
}
#[test]
fn registry_infers_by_path_and_falls_back_to_plain_text() {
let mut registry = Registry::with_plain_text();
registry.add_grammar(test_grammar());
registry.add_theme(test_theme());
assert_eq!(
registry
.query(GrammarQuery {
path: Some(Path::new("main.test")),
..GrammarQuery::default()
})
.unwrap()
.name,
"Test"
);
assert_eq!(
registry
.query(GrammarQuery {
path: Some(Path::new("README")),
..GrammarQuery::default()
})
.unwrap()
.name,
crate::grammar::PLAIN_TEXT_NAME
);
assert_eq!(
registry.theme_by_name("test theme").unwrap().name,
"Test Theme"
);
}
#[test]
fn json_grammar_uses_dedicated_fast_path() {
let grammar = Grammar::json();
let mut state = LineState::default();
let line = r#" "a": "b\"c", "n": 12.5e-1, "ok": true"#;
let spans = grammar.tokenize_line(&mut state, line);
assert_eq!(grammar.kind, GrammarKind::Json);
assert!(state.is_empty());
assert!(spans.iter().any(|span| {
grammar.scopes.get(span.scope.index()).map(String::as_str)
== Some("support.type.property-name.json")
&& &line[span.start..span.end] == "a"
}));
assert!(spans.iter().any(|span| {
grammar.scopes.get(span.scope.index()).map(String::as_str)
== Some("string.quoted.double.json")
&& &line[span.start..span.end] == r#"b\"c"#
}));
assert!(spans.iter().any(|span| {
grammar.scopes.get(span.scope.index()).map(String::as_str) == Some("constant.numeric.json")
&& &line[span.start..span.end] == "12.5e-1"
}));
assert!(spans.iter().any(|span| {
grammar.scopes.get(span.scope.index()).map(String::as_str) == Some("constant.language.json")
&& &line[span.start..span.end] == "true"
}));
}
#[test]
fn json_fast_path_works_with_blob_ranges() {
let grammar = Grammar::json();
let text = "{\n \"a\": \"b\",\n \"n\": 1,\n \"ok\": false\n}";
let mut blob = BlobHighlighter::new(&grammar, text);
let mut visited = Vec::new();
let mut saw_number = false;
blob.highlight_range(2..4, |line| {
visited.push(line.line_index);
saw_number |= line.scopes.iter().any(|span| {
grammar.scopes.get(span.scope.index()).map(String::as_str)
== Some("constant.numeric.json")
&& &line.text[span.start..span.end] == "1"
});
});
assert_eq!(visited, vec![2, 3]);
assert!(saw_number);
}
#[test]
fn registry_can_infer_builtin_json_by_path() {
let registry = Registry::with_builtin_syntaxes();
let grammar = registry
.query(GrammarQuery {
path: Some(Path::new("data.json")),
..GrammarQuery::default()
})
.unwrap();
assert_eq!(grammar.name, "JSON");
assert_eq!(grammar.kind, GrammarKind::Json);
}