1use std::{
36 io::{self, Write},
37 iter,
38 ops::Range,
39};
40use termcolor::{BufferWriter, ColorChoice, WriteColor};
41use thiserror::Error;
42
43mod annotation;
44pub use annotation::{Annotation, AnnotationText, Severity};
45
46mod stylesheet;
47pub use stylesheet::Stylesheet;
48
49#[derive(Debug, Error, PartialEq, Eq)]
50#[non_exhaustive]
51pub enum Error {
54 #[error("range {0} .. {1} crosses line boundary")]
56 MultilineRange(usize, usize),
57 #[error("range {0} .. {1} is invalid: {1} < {0}")]
59 InvalidRange(usize, usize),
60 #[error("range {0} .. {1} starts after last line end")]
62 AfterStringEnd(usize, usize),
63}
64
65pub type Result<T, E = Error> = std::result::Result<T, E>;
66
67#[derive(Debug, PartialEq, Eq)]
68#[doc(hidden)]
69pub struct AnnotatedLine<'a> {
70 start: usize,
71 content: &'a str,
72 annotations: Vec<Annotation>,
73}
74
75impl AnnotatedLine<'_> {
76 pub fn start(&self) -> usize {
77 self.start
78 }
79
80 pub fn annotations(&self) -> &[Annotation] {
81 &self.annotations
82 }
83
84 pub fn content(&self) -> &str {
85 self.content
86 }
87
88 pub fn add(&mut self, annotation: Annotation) -> Result<&mut Self> {
89 let range = annotation.range();
90 if range.end - range.start > self.content.len() {
91 Err(Error::MultilineRange(range.start, range.end))
92 } else {
93 self.annotations.push(annotation);
94 Ok(self)
95 }
96 }
97}
98
99#[derive(Debug, PartialEq, Eq)]
102pub struct AnnotationList<'a> {
103 lines: Vec<AnnotatedLine<'a>>,
104 filename: String,
105}
106
107impl<'a> AnnotationList<'a> {
108 pub fn new(filename: impl AsRef<str>, string: &'a str) -> Self {
111 let linebreaks: Vec<_> = iter::once(0)
112 .chain(
113 string
114 .chars()
115 .enumerate()
116 .filter(|(_idx, c)| *c == '\n')
117 .map(|(idx, _c)| idx + 1),
118 )
119 .chain(iter::once(string.len()))
120 .collect();
121 let lines = linebreaks
122 .windows(2)
123 .filter(|bounds| bounds[0] != bounds[1])
125 .map(|bounds| AnnotatedLine {
126 start: bounds[0],
127 content: &string[bounds[0]..bounds[1]],
128 annotations: vec![],
129 })
130 .collect();
131 Self {
132 filename: filename.as_ref().into(),
133 lines,
134 }
135 }
136
137 #[doc(hidden)]
138 pub fn annotated_lines(&self) -> &[AnnotatedLine] {
139 &self.lines
140 }
141
142 pub fn add(&mut self, annotation: Annotation) -> Result<&mut Self> {
145 let range = annotation.range();
146 let line_idx = match self
147 .lines
148 .binary_search_by(|line| line.start.cmp(&range.start))
149 {
150 Ok(idx) => idx,
151 Err(idx) if idx > 0 => idx - 1,
152 _ => unreachable!("lines in AnnotationList not starting at 0"),
153 };
154 let line = &mut self.lines[line_idx];
155 if range.start >= line.start() + line.content.len() {
156 Err(Error::AfterStringEnd(range.start, range.end))
157 } else {
158 self.lines[line_idx].add(annotation)?;
159 Ok(self)
160 }
161 }
162
163 pub fn info(
165 &mut self,
166 range: Range<usize>,
167 header: impl AnnotationText,
168 text: impl AnnotationText,
169 ) -> Result<&mut Self> {
170 self.add(Annotation::info(range, header, text)?)
171 }
172
173 pub fn warning(
175 &mut self,
176 range: Range<usize>,
177 header: impl AnnotationText,
178 text: impl AnnotationText,
179 ) -> Result<&mut Self> {
180 self.add(Annotation::warning(range, header, text)?)
181 }
182
183 pub fn error(
185 &mut self,
186 range: Range<usize>,
187 header: impl AnnotationText,
188 text: impl AnnotationText,
189 ) -> Result<&mut Self> {
190 self.add(Annotation::error(range, header, text)?)
191 }
192
193 pub fn show<W: Write + WriteColor>(
204 &self,
205 mut stream: W,
206 stylesheet: &Stylesheet,
207 ) -> io::Result<()> {
208 let mut first_output = true;
209 for (idx, line) in self.lines.iter().enumerate() {
210 for annotation in line.annotations() {
211 let range = annotation.range();
212
213 if first_output {
215 first_output = false;
216 } else {
217 stream.write(b"\n")?;
218 }
219
220 let severity_color = stylesheet.by_severity(&annotation.severity);
222 stream.set_color(severity_color)?;
223 write!(stream, "{}:", annotation.severity)?;
224 if let Some(header) = &annotation.header {
225 write!(stream, " {}\n", header)?;
226 } else {
227 stream.write(b"\n")?;
228 }
229
230 stream.set_color(&stylesheet.linenr)?;
232 let linenr = (idx + 1).to_string();
233 let nrcol_width = linenr.len() + 2;
234 print_n(&mut stream, b" ", linenr.len() + 1)?;
235 write!(stream, "--> ")?;
236 stream.set_color(&stylesheet.filename)?;
237 write!(
238 stream,
239 "{}:{}:{}\n",
240 self.filename,
241 idx + 1,
242 range.start - line.start() + 1
243 )?;
244 stream.set_color(&stylesheet.linenr)?;
245 print_n(&mut stream, b" ", nrcol_width)?;
246 write!(stream, "|\n {} | ", idx + 1)?;
247
248 stream.set_color(&stylesheet.content)?;
250 write!(stream, "{}", line.content)?;
251 if !line.content.ends_with('\n') {
252 stream.write(b"\n")?;
253 }
254
255 stream.set_color(&stylesheet.linenr)?;
257 print_n(&mut stream, b" ", nrcol_width)?;
258 stream.write(b"|")?;
259
260 if range.end - range.start != 0 {
262 stream.set_color(severity_color)?;
263 print_n(&mut stream, b" ", range.start - line.start + 1)?;
264 print_n(&mut stream, b"^", range.end - range.start)?;
265 if let Some(text) = &annotation.text {
266 write!(stream, " {}", text)?;
267 }
268 }
269 stream.write(b"\n")?;
270 stream.reset()?;
271 }
272 }
273 Ok(())
274 }
275
276 fn show_bufwriter(&self, stream: BufferWriter, stylesheet: &Stylesheet) -> io::Result<()> {
277 let mut buf = stream.buffer();
278 self.show(&mut buf, stylesheet)?;
279 stream.print(&buf)
280 }
281
282 pub fn show_stdout(&self, stylesheet: &Stylesheet) -> io::Result<()> {
284 let color_choice = if atty::is(atty::Stream::Stdout) {
285 ColorChoice::Auto
286 } else {
287 ColorChoice::Never
288 };
289 self.show_bufwriter(termcolor::BufferWriter::stdout(color_choice), stylesheet)
290 }
291
292 pub fn show_stderr(&self, stylesheet: &Stylesheet) -> io::Result<()> {
294 let color_choice = if atty::is(atty::Stream::Stderr) {
295 ColorChoice::Auto
296 } else {
297 ColorChoice::Never
298 };
299 self.show_bufwriter(termcolor::BufferWriter::stderr(color_choice), stylesheet)
300 }
301
302 pub fn to_bytes(&self) -> io::Result<Vec<u8>> {
304 let mut buf = termcolor::Buffer::no_color();
305 self.show(&mut buf, &Stylesheet::monochrome())?;
306 Ok(buf.into_inner())
307 }
308
309 pub fn to_ansi_bytes(&self, stylesheet: &Stylesheet) -> io::Result<Vec<u8>> {
311 let mut buf = termcolor::Buffer::ansi();
312 self.show(&mut buf, stylesheet)?;
313 Ok(buf.into_inner())
314 }
315
316 pub fn to_string(&self) -> io::Result<String> {
320 Ok(String::from_utf8(self.to_bytes()?).expect("invalid utf-8 in AnnotationList"))
321 }
322
323 pub fn to_ansi_string(&self, stylesheet: &Stylesheet) -> io::Result<String> {
327 Ok(String::from_utf8(self.to_ansi_bytes(stylesheet)?)
328 .expect("invalid utf-8 in AnnotationList"))
329 }
330}
331
332fn print_n(mut stream: impl io::Write, buf: &[u8], count: usize) -> io::Result<()> {
333 for _ in 0..count {
334 stream.write(buf)?;
335 }
336 Ok(())
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 fn assert_start_content<'a>(line: &AnnotatedLine<'a>, start: usize, content: &'a str) {
344 assert_eq!(line.start(), start);
345 assert_eq!(line.content(), content);
346 }
347
348 fn create_list() -> AnnotationList<'static> {
349 AnnotationList::new("test.txt", "\nstring\nwith\nmany\n\nnewlines\n\n")
350 }
351
352 #[test]
353 fn test_new_many_newlines() {
354 let annotation_list = create_list();
355 let mut lines = annotation_list.annotated_lines().iter();
356 assert_start_content(lines.next().unwrap(), 0, "\n");
357 assert_start_content(lines.next().unwrap(), 1, "string\n");
358 assert_start_content(lines.next().unwrap(), 8, "with\n");
359 assert_start_content(lines.next().unwrap(), 13, "many\n");
360 assert_start_content(lines.next().unwrap(), 18, "\n");
361 assert_start_content(lines.next().unwrap(), 19, "newlines\n");
362 assert_start_content(lines.next().unwrap(), 28, "\n");
363 assert!(lines.next().is_none());
364 }
365
366 #[test]
367 fn test_new_without_newlines() {
368 let annotation_list = AnnotationList::new("filename", "string without newlines");
369 let mut lines = annotation_list.annotated_lines().iter();
370 assert_start_content(lines.next().unwrap(), 0, "string without newlines");
371 assert!(lines.next().is_none());
372 }
373
374 #[test]
375 fn test_new_trailing_newline() {
376 let annotation_list = AnnotationList::new("filename", "string with trailing newline\n");
377 let mut lines = annotation_list.annotated_lines().iter();
378 assert_start_content(lines.next().unwrap(), 0, "string with trailing newline\n");
379 }
380
381 #[test]
382 fn test_new_leading_newline() {
383 let annotation_list = AnnotationList::new("filename", "\nstring with leading newline");
384 let mut lines = annotation_list.annotated_lines().iter();
385 assert_start_content(lines.next().unwrap(), 0, "\n");
386 assert_start_content(lines.next().unwrap(), 1, "string with leading newline");
387 }
388
389 #[test]
390 fn test_add_normal() -> Result<()> {
391 let ann1 = Annotation::info(1..3, "test1", "ann1")?;
392 let ann2 = Annotation::warning(13..17, "test2", "ann2")?;
393 let ann3 = Annotation::error(19..20, "test3", None)?;
394 let ann4 = Annotation::error(14..16, "test4", "ann4")?;
395
396 let mut list = create_list();
397 list.add(ann1.clone())?
398 .add(ann2.clone())?
399 .add(ann3.clone())?
400 .add(ann4.clone())?;
401
402 let mut other_option = create_list();
403 other_option
404 .info(1..3, "test1", "ann1")?
405 .warning(13..17, "test2", "ann2")?
406 .error(19..20, "test3", None)?
407 .error(14..16, "test4", "ann4")?;
408 assert_eq!(list, other_option);
409
410 for (idx, line) in list.annotated_lines().iter().enumerate() {
411 match idx {
412 1 => assert_eq!(line.annotations(), &[ann1.clone()]),
413 3 => assert_eq!(line.annotations(), &[ann2.clone(), ann4.clone()]),
414 5 => assert_eq!(line.annotations(), &[ann3.clone()]),
415 _ => assert_eq!(line.annotations(), &[]),
416 }
417 }
418 Ok(())
419 }
420
421 #[test]
422 fn test_add_at_the_end() -> Result<()> {
423 let mut list = AnnotationList::new("fname", "hello world");
424 list.error(10..10, None, None)?;
425 let mut list = AnnotationList::new("fname", "hello world\n");
426 list.error(11..11, None, None)?;
427 Ok(())
428 }
429
430 #[test]
431 fn test_invalid_adds() -> Result<()> {
432 let mut list = create_list();
433 assert_eq!(
434 list.add(Annotation::info(1..10, "test", "ann")?)
435 .unwrap_err(),
436 Error::MultilineRange(1, 10)
437 );
438 assert_eq!(
439 list.add(Annotation::info(1000..1001, "test", "ann")?)
440 .unwrap_err(),
441 Error::AfterStringEnd(1000, 1001)
442 );
443 assert_eq!(
444 Annotation::info(10..9, "test", "ann").unwrap_err(),
445 Error::InvalidRange(10, 9)
446 );
447 Ok(())
448 }
449
450 #[test]
451 fn test_to_string() -> Result<()> {
452 let mut list = create_list();
453 list.info(1..3, "test1", "ann1")?
454 .warning(13..17, "test2", "ann2")?
455 .error(19..20, "test3", None)?
456 .error(14..16, "test4", "ann4")?
457 .error(14..16, None, "ann5")?;
458 let result = r#"info: test1
459 --> test.txt:2:1
460 |
461 2 | string
462 | ^^ ann1
463
464warning: test2
465 --> test.txt:4:1
466 |
467 4 | many
468 | ^^^^ ann2
469
470error: test4
471 --> test.txt:4:2
472 |
473 4 | many
474 | ^^ ann4
475
476error:
477 --> test.txt:4:2
478 |
479 4 | many
480 | ^^ ann5
481
482error: test3
483 --> test.txt:6:1
484 |
485 6 | newlines
486 | ^
487"#;
488 assert_eq!(list.to_string().unwrap(), result);
489 Ok(())
490 }
491}