1use std::fmt::Display;
4
5use somni_parser::{Error as ParserError, Location};
6
7use crate::EvalError;
8
9pub trait ErrorWithLocation: Display {
11 fn location(&self) -> Location;
13}
14
15impl ErrorWithLocation for ParserError {
16 fn location(&self) -> Location {
17 self.location
18 }
19}
20
21impl ErrorWithLocation for EvalError {
22 fn location(&self) -> Location {
23 self.location
24 }
25}
26
27pub struct MarkInSource<'s>(pub &'s str, pub Location, pub &'s str, pub &'s str);
29impl std::fmt::Display for MarkInSource<'_> {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 let start_loc = location(self.0, self.1.start);
32 let end_loc = location(self.0, self.1.end);
33 mark_in_source(f, self.0, start_loc, end_loc, self.2, self.3)
34 }
35}
36
37fn mark_in_source(
38 f: &mut std::fmt::Formatter<'_>,
39 source: &str,
40 start_loc: (usize, usize),
41 end_loc: (usize, usize),
42 message: &str,
43 hint: &str,
44) -> Result<(), std::fmt::Error> {
45 let (start_line, start_col) = start_loc;
46 let (end_line, end_col) = end_loc;
47
48 let line = source.lines().nth(start_line - 1).unwrap();
49 let line_no_chars = start_line.ilog10() as usize + 1;
50 f.write_fmt(format_args!(
51 "{bold}{message}{reset}\n{:>width$}{blue}--->{reset} at line {start_line} column {start_col}\n",
52 "",
53 width = line_no_chars,
54 blue = "\x1b[1;36m",
55 bold = "\x1b[1m",
56 reset = "\x1b[0m",
57 ))?;
58 f.write_fmt(format_args!(
59 "{:>width$} {blue}|{reset}\n",
60 "",
61 width = line_no_chars,
62 blue = "\x1b[1;36m",
63 reset = "\x1b[0m",
64 ))?;
65 f.write_fmt(format_args!(
66 "{blue}{start_line} |{reset} {line}\n",
67 blue = "\x1b[1;36m",
68 reset = "\x1b[0m",
69 ))?;
70
71 let arrow_width = if start_line == end_line {
72 end_col - start_col
73 } else {
74 1
76 };
77
78 f.write_fmt(format_args!(
79 "{placeholder:>line_no_chars$} {blue}|{reset} {placeholder:>space_width$}{red}{arrow:^<arrow_width$} {hint}{reset}",
80 placeholder = "",
81 line_no_chars = line_no_chars,
82 space_width = start_col - 1,
83 arrow = "^",
84 arrow_width = arrow_width,
85 blue = "\x1b[1;36m",
86 red = "\x1b[1;31m",
87 reset = "\x1b[0m",
88 ))
89}
90
91fn location(source: &str, offset: usize) -> (usize, usize) {
92 let before_offset = &source[..offset];
93 let mut line = 1;
94 let mut col = 1;
95
96 for char in before_offset.chars() {
97 if char == '\n' {
98 line += 1;
99 col = 1;
100 } else if char == '\r' {
101 col = 1;
102 } else {
103 col += char.len_utf8();
104 }
105 }
106
107 (line, col)
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn test_source_location() {
116 let test_cases = [
117 ((1, 1), 0, "asdf"),
118 ((1, 4), 3, "asdf"),
119 ((2, 1), 5, "asdf\n"),
120 ((3, 4), 17, "asdf\njlk hsdf\nfoo"),
121 ((1, 5), 4, "1 + 2 * foo(3)"),
122 ];
123
124 for (expected, offset, source) in test_cases {
125 assert_eq!(location(source, offset), expected);
126 }
127 }
128}