eira/
lib.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/swamp/script
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5
6pub 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
53// Scopes and Labels for a section of a source code file
54pub 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    /// Writes the number of specified spaces
82    fn source_code_pad<W: Write>(count: usize, mut writer: W) -> io::Result<()> {
83        write!(writer, "{}", " ".repeat(count))
84    }
85
86    /// Writes the number of specified spaces
87    fn scope_margin_pad<W: Write>(count: usize, mut writer: W) -> io::Result<()> {
88        write!(writer, "{}", " ".repeat(count))
89    }
90
91    /// Writes the number of specified spaces
92    fn line_number_margin_pad<W: Write>(count: usize, mut writer: W) -> io::Result<()> {
93        write!(writer, "{}", " ".repeat(count))
94    }
95
96    /// Calculates which spans that are active for the specified source line.
97    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        // Add label spans
105        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        // Add scope start spans
114        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        // Add scope end spans
126        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    /// Writes a source line exactly as it is in the source file, but the colors are used from the `ColoredSpan`.
141    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        // Only filter out source code lines that will be referenced by scope or labels
184        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    /// Sorts the labels and scopes
200    pub fn layout(&mut self) {
201        // Sort scopes by x position first, then by y position
202        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        // Sort label positions by y first and then by x.
209        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    /// Write bars for active scopes
216    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    /// Writes the mandatory margin for a source code block with description items
236    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    /// Calculates the maximum number of overlapping scope items for the source block
258    /// Needed for the padding
259    #[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    /// # Errors
273    ///
274    /// # Panics
275    /// if the line number was not provided in `PrefixInfo`.
276    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    /// The mandatory prefix for each line. The line number and active scopes.
319    /// # Errors
320    ///
321    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    /// Underlines spans for upcoming labels to reference.
339    /// # Errors
340    ///
341    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    /// Writes the line labels
369    /// # Errors
370    ///
371    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            // Draw vertical bars for all labels that will come after this one
382            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            // TODO: Store the aligned position so it doesn't have to be calculated again.
390            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            // line length somewhat proportional to the span so it looks nicer
396            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    /// Writes the description for scopes that have ended
407    /// # Errors
408    ///
409    pub fn write_text_for_ending_scopes(
410        prefix_info: &PrefixInfo,
411        active_scopes: &[&Scope],
412        line_number: usize, // Line number is provided, since the prefix_info line_number is `None`.
413        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; // stop writing since we are on the scope text we should print
427                        }
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    /// Draws the source file section
442    /// # Errors
443    ///
444    /// # Panics
445    /// If a source line can not be provided for the line number
446    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            // Get active scopes for source line (includes end line)
462            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            // Get labels for the current line and sort labels by x position in reverse order (right to left)
469            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; // only use line number when writing source lines
491
492            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, // Give extra information about an error. Maybe should be included in each warning and error?
511    Note, // Extra context. Maybe should be included in each warning and error?
512    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    /// # Errors
545    ///
546    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    /// # Errors
563    ///
564    /// # Panics
565    ///
566    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}