1use std::{fs, path::Path};
2
3use anstyle::AnsiColor;
4use anyhow::{anyhow, Result};
5use tree_sitter::Point;
6use tree_sitter_highlight::{Highlight, HighlightConfiguration, HighlightEvent, Highlighter};
7use tree_sitter_loader::{Config, Loader};
8
9use super::{
10 query_testing::{parse_position_comments, to_utf8_point, Assertion, Utf8Point},
11 test::paint,
12 util,
13};
14
15#[derive(Debug)]
16pub struct Failure {
17 row: usize,
18 column: usize,
19 expected_highlight: String,
20 actual_highlights: Vec<String>,
21}
22
23impl std::error::Error for Failure {}
24
25impl std::fmt::Display for Failure {
26 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
27 write!(
28 f,
29 "Failure - row: {}, column: {}, expected highlight '{}', actual highlights: ",
30 self.row, self.column, self.expected_highlight
31 )?;
32 if self.actual_highlights.is_empty() {
33 write!(f, "none.")?;
34 } else {
35 for (i, actual_highlight) in self.actual_highlights.iter().enumerate() {
36 if i > 0 {
37 write!(f, ", ")?;
38 }
39 write!(f, "'{actual_highlight}'")?;
40 }
41 }
42 Ok(())
43 }
44}
45
46pub fn test_highlights(
47 loader: &Loader,
48 loader_config: &Config,
49 highlighter: &mut Highlighter,
50 directory: &Path,
51 use_color: bool,
52) -> Result<()> {
53 println!("syntax highlighting:");
54 test_highlights_indented(loader, loader_config, highlighter, directory, use_color, 2)
55}
56
57fn test_highlights_indented(
58 loader: &Loader,
59 loader_config: &Config,
60 highlighter: &mut Highlighter,
61 directory: &Path,
62 use_color: bool,
63 indent_level: usize,
64) -> Result<()> {
65 let mut failed = false;
66
67 for highlight_test_file in fs::read_dir(directory)? {
68 let highlight_test_file = highlight_test_file?;
69 let test_file_path = highlight_test_file.path();
70 let test_file_name = highlight_test_file.file_name();
71 print!(
72 "{indent:indent_level$}",
73 indent = "",
74 indent_level = indent_level * 2
75 );
76 if test_file_path.is_dir() && test_file_path.read_dir()?.next().is_some() {
77 println!("{}:", test_file_name.to_string_lossy());
78 if test_highlights_indented(
79 loader,
80 loader_config,
81 highlighter,
82 &test_file_path,
83 use_color,
84 indent_level + 1,
85 )
86 .is_err()
87 {
88 failed = true;
89 }
90 } else {
91 let (language, language_config) = loader
92 .language_configuration_for_file_name(&test_file_path)?
93 .ok_or_else(|| {
94 anyhow!(
95 "{}",
96 util::lang_not_found_for_path(test_file_path.as_path(), loader_config)
97 )
98 })?;
99 let highlight_config = language_config
100 .highlight_config(language, None)?
101 .ok_or_else(|| anyhow!("No highlighting config found for {test_file_path:?}"))?;
102 match test_highlight(
103 loader,
104 highlighter,
105 highlight_config,
106 fs::read(&test_file_path)?.as_slice(),
107 ) {
108 Ok(assertion_count) => {
109 println!(
110 "✓ {} ({assertion_count} assertions)",
111 paint(
112 use_color.then_some(AnsiColor::Green),
113 test_file_name.to_string_lossy().as_ref()
114 ),
115 );
116 }
117 Err(e) => {
118 println!(
119 "✗ {}",
120 paint(
121 use_color.then_some(AnsiColor::Red),
122 test_file_name.to_string_lossy().as_ref()
123 )
124 );
125 println!(
126 "{indent:indent_level$} {e}",
127 indent = "",
128 indent_level = indent_level * 2
129 );
130 failed = true;
131 }
132 }
133 }
134 }
135
136 if failed {
137 Err(anyhow!(""))
138 } else {
139 Ok(())
140 }
141}
142pub fn iterate_assertions(
143 assertions: &[Assertion],
144 highlights: &[(Utf8Point, Utf8Point, Highlight)],
145 highlight_names: &[String],
146) -> Result<usize> {
147 let mut i = 0;
150 let mut actual_highlights = Vec::new();
151 for Assertion {
152 position,
153 length,
154 negative,
155 expected_capture_name: expected_highlight,
156 } in assertions
157 {
158 let mut passed = false;
159 let mut end_column = position.column + length - 1;
160 actual_highlights.clear();
161
162 'highlight_loop: while let Some(highlight) = highlights.get(i) {
165 if highlight.1 <= *position {
166 i += 1;
167 continue;
168 }
169
170 let mut j = i;
173 while let (false, Some(highlight)) = (passed, highlights.get(j)) {
174 end_column = position.column + length - 1;
175 if highlight.0.column > end_column {
176 break 'highlight_loop;
177 }
178
179 let highlight_name = &highlight_names[(highlight.2).0];
185 if (*highlight_name == *expected_highlight) == *negative {
186 actual_highlights.push(highlight_name);
187 } else {
188 passed = true;
189 break 'highlight_loop;
190 }
191
192 j += 1;
193 }
194 }
195
196 if !passed {
197 return Err(Failure {
198 row: position.row,
199 column: end_column,
200 expected_highlight: expected_highlight.clone(),
201 actual_highlights: actual_highlights.into_iter().cloned().collect(),
202 }
203 .into());
204 }
205 }
206
207 Ok(assertions.len())
208}
209
210pub fn test_highlight(
211 loader: &Loader,
212 highlighter: &mut Highlighter,
213 highlight_config: &HighlightConfiguration,
214 source: &[u8],
215) -> Result<usize> {
216 let highlight_names = loader.highlight_names();
218 let highlights = get_highlight_positions(loader, highlighter, highlight_config, source)?;
219 let assertions =
220 parse_position_comments(highlighter.parser(), &highlight_config.language, source)?;
221
222 iterate_assertions(&assertions, &highlights, &highlight_names)
223}
224
225pub fn get_highlight_positions(
226 loader: &Loader,
227 highlighter: &mut Highlighter,
228 highlight_config: &HighlightConfiguration,
229 source: &[u8],
230) -> Result<Vec<(Utf8Point, Utf8Point, Highlight)>> {
231 let mut row = 0;
232 let mut column = 0;
233 let mut byte_offset = 0;
234 let mut was_newline = false;
235 let mut result = Vec::new();
236 let mut highlight_stack = Vec::new();
237 let source = String::from_utf8_lossy(source);
238 let mut char_indices = source.char_indices();
239 for event in highlighter.highlight(highlight_config, source.as_bytes(), None, |string| {
240 loader.highlight_config_for_injection_string(string)
241 })? {
242 match event? {
243 HighlightEvent::HighlightStart(h) => highlight_stack.push(h),
244 HighlightEvent::HighlightEnd => {
245 highlight_stack.pop();
246 }
247 HighlightEvent::Source { start, end } => {
248 let mut start_position = Point::new(row, column);
249 while byte_offset < end {
250 if byte_offset <= start {
251 start_position = Point::new(row, column);
252 }
253 if let Some((i, c)) = char_indices.next() {
254 if was_newline {
255 row += 1;
256 column = 0;
257 } else {
258 column += i - byte_offset;
259 }
260 was_newline = c == '\n';
261 byte_offset = i;
262 } else {
263 break;
264 }
265 }
266 if let Some(highlight) = highlight_stack.last() {
267 let utf8_start_position = to_utf8_point(start_position, source.as_bytes());
268 let utf8_end_position =
269 to_utf8_point(Point::new(row, column), source.as_bytes());
270 result.push((utf8_start_position, utf8_end_position, *highlight));
271 }
272 }
273 }
274 }
275 Ok(result)
276}