1pub mod prelude;
7
8use std::fmt::{Display, Formatter};
9use std::io;
10use std::io::Write;
11use yansi::Paint;
12
13pub use yansi::Color;
14
15#[derive(Clone, PartialEq, Eq)]
16pub struct Pos {
17 pub x: usize,
18 pub y: usize,
19}
20
21#[derive(Clone, PartialEq, Eq)]
22pub struct PosSpan {
23 pub pos: Pos,
24 pub length: usize,
25}
26
27#[derive(PartialEq, Eq)]
28pub struct ColoredSpan {
29 pub pos: PosSpan,
30 pub color: Color,
31}
32
33#[derive(PartialEq, Eq)]
34pub struct Scope {
35 pub start: PosSpan,
36 pub end: PosSpan,
37 pub text: String,
38 pub color: Color,
39}
40
41#[derive(Clone, PartialEq, Eq)]
42pub struct Label {
43 pub start: Pos,
44 pub character_count: usize,
45 pub text: String,
46 pub color: Color,
47}
48
49pub trait SourceLines {
50 fn get_line(&self, line_number: usize) -> Option<&str>;
51}
52
53pub struct SourceFileSection {
55 pub scopes: Vec<Scope>,
56 pub labels: Vec<Label>,
57}
58
59impl Default for SourceFileSection {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65pub struct PrefixInfo<'a> {
66 pub maximum_overlapping_scope_count: usize,
67 pub active_scopes: &'a [&'a Scope],
68 pub max_number_string_size: usize,
69 line_number: Option<usize>,
70}
71
72impl SourceFileSection {
73 #[must_use]
74 pub fn new() -> Self {
75 Self {
76 scopes: vec![],
77 labels: vec![],
78 }
79 }
80
81 fn source_code_pad<W: Write>(count: usize, mut writer: W) -> io::Result<()> {
83 write!(writer, "{}", " ".repeat(count))
84 }
85
86 fn scope_margin_pad<W: Write>(count: usize, mut writer: W) -> io::Result<()> {
88 write!(writer, "{}", " ".repeat(count))
89 }
90
91 fn line_number_margin_pad<W: Write>(count: usize, mut writer: W) -> io::Result<()> {
93 write!(writer, "{}", " ".repeat(count))
94 }
95
96 fn get_colored_spans_for_line(
98 line_labels: &[&Label],
99 scopes: &[&Scope],
100 line_number: usize,
101 ) -> Vec<ColoredSpan> {
102 let mut spans = Vec::new();
103
104 spans.extend(line_labels.iter().map(|label| ColoredSpan {
106 pos: PosSpan {
107 pos: label.start.clone(),
108 length: label.character_count,
109 },
110 color: label.color,
111 }));
112
113 spans.extend(scopes.iter().filter_map(|scope| {
115 if scope.start.pos.y == line_number {
116 Some(ColoredSpan {
117 pos: scope.start.clone(),
118 color: scope.color,
119 })
120 } else {
121 None
122 }
123 }));
124
125 spans.extend(scopes.iter().filter_map(|scope| {
127 if scope.end.pos.y == line_number {
128 Some(ColoredSpan {
129 pos: scope.end.clone(),
130 color: scope.color,
131 })
132 } else {
133 None
134 }
135 }));
136
137 spans
138 }
139
140 fn write_source_line<W: Write>(
142 source_line: &str,
143 colored_spans: &[ColoredSpan],
144 mut writer: W,
145 ) -> io::Result<()> {
146 let chars: Vec<char> = source_line.chars().collect();
147 let mut current_pos = 0;
148
149 while current_pos < chars.len() {
150 let matching_span = colored_spans.iter().find(|span| {
151 (current_pos + 1) >= span.pos.pos.x
152 && (current_pos + 1) < span.pos.pos.x + span.pos.length
153 });
154
155 let mut region_end = current_pos;
156 while region_end < chars.len() {
157 let next_span = colored_spans.iter().find(|span| {
158 (region_end + 1) >= span.pos.pos.x
159 && (region_end + 1) < span.pos.pos.x + span.pos.length
160 });
161 if next_span != matching_span {
162 break;
163 }
164 region_end += 1;
165 }
166
167 let text: String = chars[current_pos..region_end].iter().collect();
168 if let Some(span) = matching_span {
169 write!(writer, "{}", text.fg(span.color))?;
170 } else {
171 write!(writer, "{text}")?;
172 }
173
174 current_pos = region_end;
175 }
176
177 writeln!(writer)?;
178 Ok(())
179 }
180
181 #[must_use]
182 pub fn calculate_source_lines_that_must_be_shown(&self) -> Vec<usize> {
183 let mut lines_to_show: Vec<usize> = self
185 .labels
186 .iter()
187 .map(|label| label.start.y)
188 .chain(
189 self.scopes
190 .iter()
191 .flat_map(|scope| vec![scope.start.pos.y, scope.end.pos.y]),
192 )
193 .collect();
194 lines_to_show.sort_unstable();
195 lines_to_show.dedup();
196 lines_to_show
197 }
198
199 pub fn layout(&mut self) {
201 self.scopes
203 .sort_by(|a, b| match a.start.pos.x.cmp(&b.start.pos.x) {
204 std::cmp::Ordering::Equal => a.start.pos.y.cmp(&b.start.pos.y),
205 other => other,
206 });
207
208 self.labels.sort_by(|a, b| match a.start.y.cmp(&b.start.y) {
210 std::cmp::Ordering::Equal => a.start.x.cmp(&b.start.x),
211 other => other,
212 });
213 }
214
215 fn write_scope_continuation<W: Write>(
217 active_scopes: &[&Scope],
218 max_scopes: usize,
219 mut writer: W,
220 ) -> io::Result<()> {
221 let mut sorted_scopes = active_scopes.to_vec();
222 sorted_scopes.sort_by_key(|scope| scope.start.pos.x);
223
224 for i in 0..max_scopes {
225 if let Some(scope) = sorted_scopes.get(i) {
226 write!(writer, "{}", "│".fg(scope.color))?;
227 Self::scope_margin_pad(3, &mut writer)?;
228 } else {
229 Self::scope_margin_pad(4, &mut writer)?;
230 }
231 }
232 Ok(())
233 }
234
235 fn write_line_prefix<W: Write>(
237 max_line_num_width: usize,
238 line_number: Option<usize>,
239 mut writer: W,
240 ) -> io::Result<()> {
241 let number_string =
242 line_number.map_or_else(String::new, |found_number| found_number.to_string());
243
244 let padding = max_line_num_width - number_string.len();
245
246 Self::line_number_margin_pad(padding, &mut writer)?;
247 write!(writer, "{}", number_string.fg(Color::BrightBlack))?;
248 Self::line_number_margin_pad(1, &mut writer)?;
249 let separator = if line_number.is_some() { "|" } else { "·" };
250
251 write!(writer, "{}", separator.fg(Color::BrightBlack))?;
252 Self::line_number_margin_pad(1, &mut writer)?;
253
254 Ok(())
255 }
256
257 #[must_use]
260 pub fn calculate_max_overlapping_scopes(scopes: &[Scope]) -> usize {
261 scopes.iter().fold(0, |max_count, scope| {
262 let overlapping = scopes
263 .iter()
264 .filter(|other| {
265 scope.start.pos.y <= other.end.pos.y && other.start.pos.y <= scope.end.pos.y
266 })
267 .count();
268 max_count.max(overlapping)
269 })
270 }
271
272 pub fn write_source_line_with_prefixes(
277 prefix_info: &PrefixInfo,
278 labels: &[&Label],
279 source_line: &str,
280 mut writer: impl Write,
281 ) -> io::Result<()> {
282 let current_line_number = prefix_info
283 .line_number
284 .expect("a source line was missing a line number");
285 Self::write_line_prefix(
286 prefix_info.max_number_string_size,
287 Some(current_line_number),
288 &mut writer,
289 )?;
290 for i in 0..prefix_info.maximum_overlapping_scope_count {
291 if let Some(scope) = prefix_info.active_scopes.get(i) {
292 let is_start = current_line_number == scope.start.pos.y;
293 let is_end = current_line_number == scope.end.pos.y;
294 let scope_line_prefix = if is_start {
295 "╭─▶"
296 } else if is_end {
297 "├─▶"
298 } else {
299 "│"
300 };
301 let prefix = scope_line_prefix.fg(scope.color).to_string();
302 write!(writer, "{prefix}")?;
303 let padding = if is_start || is_end { 1 } else { 3 };
304
305 Self::scope_margin_pad(padding, &mut writer)?;
306 } else {
307 Self::scope_margin_pad(4, &mut writer)?;
308 }
309 }
310 let colored_spans = Self::get_colored_spans_for_line(
311 labels,
312 prefix_info.active_scopes,
313 current_line_number,
314 );
315 Self::write_source_line(source_line, &colored_spans, &mut writer)
316 }
317
318 pub fn write_start_of_line_prefix(
322 prefix: &PrefixInfo,
323 mut writer: impl Write,
324 ) -> io::Result<()> {
325 Self::write_line_prefix(
326 prefix.max_number_string_size,
327 prefix.line_number,
328 &mut writer,
329 )?;
330
331 Self::write_scope_continuation(
332 prefix.active_scopes,
333 prefix.maximum_overlapping_scope_count,
334 &mut writer,
335 )
336 }
337
338 pub fn write_underlines_for_upcoming_labels(
342 prefix_info: &PrefixInfo,
343 line_labels: &[&Label],
344 mut writer: impl Write,
345 ) -> io::Result<()> {
346 Self::write_start_of_line_prefix(prefix_info, &mut writer)?;
347
348 let mut current_pos = 0;
349
350 for label in line_labels.iter().rev() {
351 if label.start.x > current_pos {
352 Self::source_code_pad(label.start.x - 1 - current_pos, &mut writer)?;
353 }
354
355 let middle = (label.character_count - 1) / 2;
356 let underline: String = (0..label.character_count)
357 .map(|i| if i == middle { '┬' } else { '─' })
358 .collect();
359
360 write!(writer, "{}", underline.fg(label.color))?;
361
362 current_pos = label.start.x - 1 + label.character_count;
363 }
364
365 writeln!(writer)
366 }
367
368 pub fn write_labels(
372 prefix_info: &PrefixInfo,
373 line_labels: &[&Label],
374 mut writer: impl Write,
375 ) -> io::Result<()> {
376 for (idx, label) in line_labels.iter().enumerate() {
377 Self::write_start_of_line_prefix(prefix_info, &mut writer)?;
378
379 let mut current_pos = 0;
380
381 for future_label in line_labels.iter().skip(idx + 1) {
383 let middle = (future_label.start.x - 1) + (future_label.character_count - 1) / 2;
384 Self::source_code_pad(middle - current_pos, &mut writer)?;
385 write!(writer, "{}", "│".fg(future_label.color))?;
386 current_pos = middle + 1;
387 }
388
389 let middle = (label.start.x - 1) + (label.character_count - 1) / 2;
391 if middle > current_pos {
392 Self::source_code_pad(middle - current_pos, &mut writer)?;
393 }
394
395 let dash_count = (label.character_count / 4).clamp(2, 8);
397 let connector = format!("╰{}", "─".repeat(dash_count));
398 let label_line = format!("{} {}", connector.fg(label.color), label.text);
399 write!(writer, "{label_line}")?;
400
401 writeln!(writer)?;
402 }
403 Ok(())
404 }
405
406 pub fn write_text_for_ending_scopes(
410 prefix_info: &PrefixInfo,
411 active_scopes: &[&Scope],
412 line_number: usize, mut writer: impl Write,
414 ) -> io::Result<()> {
415 for scope in active_scopes {
416 if scope.end.pos.y == line_number {
417 Self::write_start_of_line_prefix(prefix_info, &mut writer)?;
418 writeln!(writer)?;
419
420 Self::write_line_prefix(prefix_info.max_number_string_size, None, &mut writer)?;
421
422 for i in 0..prefix_info.maximum_overlapping_scope_count {
423 if let Some(s) = active_scopes.get(i) {
424 if s == scope {
425 write!(writer, "{}", "╰─── ".fg(s.color))?;
426 break; }
428 write!(writer, "{}", "│".fg(s.color))?;
429 Self::scope_margin_pad(3, &mut writer)?;
430 } else {
431 write!(writer, " ")?;
432 }
433 }
434 writeln!(writer, "{}", scope.text.fg(scope.color))?;
435 }
436 }
437
438 Ok(())
439 }
440
441 pub fn draw<W: Write, S: SourceLines>(&self, source: &S, mut writer: W) -> io::Result<()> {
447 let line_numbers_to_show = self.calculate_source_lines_that_must_be_shown();
448
449 let max_overlapping_scopes_count = Self::calculate_max_overlapping_scopes(&self.scopes);
450
451 let max_line_number_width = line_numbers_to_show
452 .iter()
453 .max()
454 .map_or(0, |&max_line| max_line.to_string().len());
455
456 for &line_number in &line_numbers_to_show {
457 let source_line = source
458 .get_line(line_number)
459 .expect("Source code lines with the requested line number should exist");
460
461 let active_scopes: Vec<_> = self
463 .scopes
464 .iter()
465 .filter(|scope| scope.start.pos.y <= line_number && line_number <= scope.end.pos.y)
466 .collect();
467
468 let mut line_labels: Vec<_> = self
470 .labels
471 .iter()
472 .filter(|label| label.start.y == line_number)
473 .collect();
474 line_labels.sort_by_key(|label| std::cmp::Reverse(label.start.x));
475
476 let mut prefix_info = PrefixInfo {
477 maximum_overlapping_scope_count: max_overlapping_scopes_count,
478 active_scopes: &active_scopes,
479 max_number_string_size: max_line_number_width,
480 line_number: Some(line_number),
481 };
482
483 Self::write_source_line_with_prefixes(
484 &prefix_info,
485 &line_labels,
486 source_line,
487 &mut writer,
488 )?;
489
490 prefix_info.line_number = None; Self::write_underlines_for_upcoming_labels(&prefix_info, &line_labels, &mut writer)?;
493
494 Self::write_labels(&prefix_info, &line_labels, &mut writer)?;
495
496 Self::write_text_for_ending_scopes(
497 &prefix_info,
498 &active_scopes,
499 line_number,
500 &mut writer,
501 )?;
502 }
503
504 Ok(())
505 }
506}
507
508#[derive(Copy, Clone, Debug)]
509pub enum Kind {
510 Help, Note, Warning,
513 Error,
514}
515
516impl Display for Kind {
517 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
518 let prefix = match self {
519 Self::Help => "help",
520 Self::Note => "note",
521 Self::Warning => "warning",
522 Self::Error => "error",
523 };
524 write!(f, "{prefix}")
525 }
526}
527
528pub struct Header<C: Display> {
529 pub header_kind: Kind,
530 pub code: C,
531 pub message: String,
532}
533
534impl<C: Display> Header<C> {
535 const fn color_for_kind(kind: Kind) -> Color {
536 match kind {
537 Kind::Help => Color::BrightBlue,
538 Kind::Note => Color::BrightMagenta,
539 Kind::Warning => Color::BrightYellow,
540 Kind::Error => Color::BrightRed,
541 }
542 }
543
544 pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
547 write!(
548 writer,
549 "{}",
550 self.header_kind.fg(Self::color_for_kind(self.header_kind))
551 )?;
552 write!(writer, "[{}]", self.code.fg(Color::Blue))?;
553 write!(writer, ": ")?;
554 write!(writer, "{}", self.message.bold())?;
555 writeln!(writer)
556 }
557}
558
559pub struct FileSpanMessage;
560
561impl FileSpanMessage {
562 pub fn write<W: Write>(
567 relative_file_name: &str,
568 pos_span: &PosSpan,
569 mut writer: W,
570 ) -> io::Result<()> {
571 write!(writer, " --> ")?;
572 write!(writer, "{}", relative_file_name.bright_cyan(),)?;
573 write!(
574 writer,
575 ":{}:{}",
576 pos_span.pos.y.fg(Color::BrightMagenta),
577 pos_span.pos.x.fg(Color::BrightMagenta),
578 )?;
579 writeln!(writer)?;
580 writeln!(writer)
581 }
582}