molframe_core/diagnostic/
render.rs1use super::{Diagnostic, Severity};
11use std::fmt;
12
13const RESET: &str = "\u{1b}[0m";
14const BOLD: &str = "\u{1b}[1m";
15const RED: &str = "\u{1b}[31m";
16const YELLOW: &str = "\u{1b}[33m";
17const BLUE: &str = "\u{1b}[34m";
18
19#[derive(Clone, Copy, Debug)]
35pub struct Rendered<'a> {
36 finding: &'a Diagnostic,
37 source: Option<&'a [u8]>,
38 origin: Option<&'a str>,
39 color: bool,
40}
41
42impl<'a> Rendered<'a> {
43 #[must_use]
45 pub const fn new(finding: &'a Diagnostic) -> Self {
46 Self {
47 finding,
48 source: None,
49 origin: None,
50 color: false,
51 }
52 }
53
54 #[must_use]
57 pub const fn with_source(mut self, source: &'a [u8]) -> Self {
58 self.source = Some(source);
59 self
60 }
61
62 #[must_use]
64 pub const fn with_origin(mut self, origin: &'a str) -> Self {
65 self.origin = Some(origin);
66 self
67 }
68
69 #[must_use]
74 pub const fn with_color(mut self, color: bool) -> Self {
75 self.color = color;
76 self
77 }
78
79 fn accent(self) -> &'static str {
80 if !self.color {
81 return "";
82 }
83 match self.finding.severity() {
84 Severity::Invalidating | Severity::Breaking => RED,
85 Severity::Strict | Severity::Loose => YELLOW,
86 Severity::Info => BLUE,
87 }
88 }
89
90 fn paint(self, code: &'static str) -> &'static str {
91 if self.color { code } else { "" }
92 }
93
94 fn write_heading(self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 let finding = self.finding;
97 write!(
98 f,
99 "{}{}{}[{}]{}: {}{}{}",
100 self.accent(),
101 self.paint(BOLD),
102 finding.severity().label(),
103 finding.code(),
104 self.paint(RESET),
105 self.paint(BOLD),
106 finding.message(),
107 self.paint(RESET),
108 )
109 }
110
111 fn write_location(self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 let (Some(span), gutter) = (self.finding.span(), self.paint(BLUE)) else {
114 if let Some(origin) = self.origin {
115 write!(
116 f,
117 "\n {}┌─{} {origin}",
118 self.paint(BLUE),
119 self.paint(RESET)
120 )?;
121 }
122 return Ok(());
123 };
124 write!(f, "\n {gutter}┌─{} ", self.paint(RESET))?;
125 match self.origin {
126 Some(origin) => write!(f, "{origin}:{}", span.start),
127 None => write!(f, "{}", span.start),
128 }
129 }
130
131 fn write_snippet(self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 let (Some(source), Some(span)) = (self.source, self.finding.span()) else {
134 return Ok(());
135 };
136 let Ok(offset) = usize::try_from(span.start.byte_offset) else {
137 return Ok(());
138 };
139 let Some(line) = line_containing(source, offset) else {
140 return Ok(());
141 };
142 let Ok(text) = str::from_utf8(line.text) else {
143 return Ok(());
144 };
145
146 let gutter = self.paint(BLUE);
147 let reset = self.paint(RESET);
148 let number = span.start.line;
149 let width = decimal_width(number);
150
151 write!(f, "\n {:width$} {gutter}│{reset}", "")?;
152 write!(f, "\n {number} {gutter}│{reset} {}", text.trim_end())?;
153
154 let column = offset.checked_sub(line.start).ok_or(fmt::Error)?;
155 let available = text.len().checked_sub(column).ok_or(fmt::Error)?.max(1);
156 let caret_count = span
157 .len()
158 .and_then(|length| usize::try_from(length).ok())
159 .ok_or(fmt::Error)?
160 .clamp(1, available);
161 write!(f, "\n {:width$} {gutter}│{reset} {:column$}", "", "")?;
162 write!(f, "{}", self.accent())?;
163 for _ in 0..caret_count {
164 f.write_str("^")?;
165 }
166 f.write_str(reset)
167 }
168
169 fn write_details(self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 let finding = self.finding;
172 let gutter = self.paint(BLUE);
173 let reset = self.paint(RESET);
174
175 if let Some(category) = finding.category() {
176 write!(f, "\n {gutter}={reset} category: {category}")?;
177 }
178 if let Some(field) = finding.field() {
179 write!(f, "\n {gutter}={reset} item: {field}")?;
180 }
181 if let Some(row) = finding.row() {
182 write!(f, "\n {gutter}={reset} row: {row}")?;
183 }
184 for item in finding.context() {
185 write!(f, "\n {gutter}={reset} {}: {}", item.label(), item.value())?;
186 }
187 write!(f, "\n {gutter}={reset} help: {}", finding.remedy())
188 }
189}
190
191impl fmt::Display for Rendered<'_> {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 self.write_heading(f)?;
194 self.write_location(f)?;
195 self.write_snippet(f)?;
196 self.write_details(f)
197 }
198}
199
200struct Line<'a> {
202 text: &'a [u8],
203 start: usize,
204}
205
206fn line_containing(source: &[u8], offset: usize) -> Option<Line<'_>> {
207 if offset > source.len() {
208 return None;
209 }
210 let start = source[..offset]
211 .iter()
212 .rposition(|&b| b == b'\n')
213 .map_or(0, |at| at + 1);
214 let end = source[start..]
215 .iter()
216 .position(|&b| b == b'\n')
217 .map_or(source.len(), |at| start + at);
218 Some(Line {
219 text: source.get(start..end)?,
220 start,
221 })
222}
223
224const fn decimal_width(mut value: u64) -> usize {
225 let mut width = 1;
226 while value >= 10 {
227 value /= 10;
228 width += 1;
229 }
230 width
231}
232
233#[cfg(test)]
234#[path = "render_tests.rs"]
235mod tests;