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