fea-rs 0.22.0

Tools for working with Adobe OpenType Feature files.
Documentation
//! syntax highlighting functions

use std::{fmt::Write, path::Path};

use crate::{Diagnostic, Kind, Level, parse::Source};
use ansi_term::{Colour, Style};

/// Return the appropriate visual style for this token kind.
pub fn style_for_kind(kind: Kind) -> Style {
    match kind {
        Kind::Comment => Style::new().fg(Colour::Yellow).dimmed(),
        Kind::Number | Kind::Octal | Kind::Hex | Kind::Float | Kind::String => {
            Style::new().fg(Colour::Green)
        }
        Kind::Ident | Kind::Tag | Kind::Label => Style::new().fg(Colour::Purple),
        Kind::TableKw
        | Kind::IncludeKw
        | Kind::LookupKw
        | Kind::LanguagesystemKw
        | Kind::AnchorDefKw
        | Kind::FeatureKw
        | Kind::MarkClassKw
        | Kind::AnonKw
        | Kind::GlyphClassDefKw => Style::new().fg(Colour::Cyan),
        Kind::NamedGlyphClass => Style::new().fg(Colour::Blue).italic(),
        Kind::LookupflagKw | Kind::ScriptKw | Kind::LanguageKw => Colour::Blue.into(),
        Kind::Backslash => Style::new().fg(Colour::Yellow).dimmed(),
        Kind::SubKw
        | Kind::PosKw
        | Kind::IgnoreKw
        | Kind::EnumKw
        | Kind::RsubKw
        | Kind::ByKw
        | Kind::FromKw => Style::new().fg(Colour::Cyan).italic(),
        _ => Style::new().fg(Colour::White),
    }
}

//FIXME: get from terminal?
const MAX_PRINT_WIDTH: usize = 100;

macro_rules! style_or_dont {
    ($tty:expr_2021, $kind:expr_2021) => {
        if $tty { $kind.into() } else { Style::default() }
    };
}

/// Given an error and a line's text, write a fancy error message.
pub(crate) fn write_diagnostic(
    writer: &mut impl Write,
    err: &Diagnostic,
    source: &Source,
    line_width: Option<usize>,
    colorized: bool,
) {
    write_header(writer, err, source, colorized);

    let line_width = line_width.unwrap_or(MAX_PRINT_WIDTH);
    let span = err.message.span.range();
    let (line_n, text) = source.line_containing_offset(span.start);
    let line_start = source.offset_for_line_number(line_n);
    let err_start = span.start - line_start;

    // if a line is really long, we clip it
    let trim_start = if text.len() > line_width {
        const SLOP: usize = 10; // buffer before start of error when clipping
        let max_trim = (text.len()) - line_width;
        err_start.saturating_sub(SLOP).min(max_trim)
    } else {
        0
    };

    let trim_end = (text.len() - trim_start).saturating_sub(line_width);
    let text = &text[trim_start..text.len() - trim_end];
    let ellipsis = if trim_start == 0 { "" } else { "..." };

    let line_ws = text.bytes().take_while(u8::is_ascii_whitespace).count();
    let n_digits = decimal_digits(line_n);
    let blue = style_or_dont!(colorized, Colour::Blue);

    // one blank line:
    writeln!(
        writer,
        "{}{} |{} ",
        blue.prefix(),
        &super::SPACES[..n_digits],
        blue.suffix()
    )
    .unwrap();
    write!(writer, "{}{} |{} ", blue.prefix(), line_n, blue.suffix()).unwrap();
    writeln!(writer, "{ellipsis}{text}").unwrap();
    let n_spaces = (span.start - line_start) - trim_start;
    // use the whitespace at the front of the line first, so that
    // we don't replace tabs with spaces
    let reuse_ws = n_spaces.min(line_ws);
    let extra_ws = n_spaces - reuse_ws;

    let n_carets = span.end - span.start;
    let n_carets = n_carets.min(CARETS.len());
    let color = style_or_dont!(colorized, err.level.color());

    write!(
        writer,
        "{}{} |{} ",
        blue.prefix(),
        &super::SPACES[..n_digits],
        blue.suffix()
    )
    .unwrap();
    writeln!(
        writer,
        "{}{}{}{}{}{}",
        &text[..reuse_ws],
        &super::SPACES[..extra_ws],
        &super::SPACES[..ellipsis.len()],
        color.prefix(),
        &CARETS[..n_carets],
        color.suffix(),
    )
    .unwrap();
}

fn write_header(writer: &mut impl Write, err: &Diagnostic, source: &Source, colorized: bool) {
    let color = style_or_dont!(colorized, err.level.color());
    let text = err.level.label();

    write!(writer, "{}{}: {}", color.prefix(), text, color.suffix(),).unwrap();

    writeln!(writer, "{}", &err.message.text).unwrap();
    let (line, column) = source.line_col_for_offset(err.message.span.range().start);
    let pre = style_or_dont!(colorized, Colour::Blue.italic()).prefix();
    let suf = style_or_dont!(colorized, Colour::Blue.italic()).suffix();
    writeln!(
        writer,
        "{pre}in{suf} {} {pre}at{suf} {line}:{column}",
        Path::new(source.path()).display(),
    )
    .unwrap();
}

impl Level {
    fn color(&self) -> Colour {
        match self {
            Level::Info => Colour::Cyan,
            Level::Warning => Colour::Yellow,
            Level::Error => Colour::Red,
        }
    }

    fn label(&self) -> &str {
        match self {
            Level::Info => "info",
            Level::Warning => "warning",
            Level::Error => "error",
        }
    }
}

static CARETS: &str = "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^";

pub(crate) fn decimal_digits(n: usize) -> usize {
    (n as f64).log10().floor() as usize + 1
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use super::*;

    #[test]
    fn highlight_long_line() {
        static A_BAD_LINE: &str = "@COMBINING_MARKS = [ candrabindu-kannada nukta-kannada ssa-kannada.below.kssa ra-kannada.below rVocalicMatra-kannada rrVocalicMatra-kannada ailength-kannada ka-kannada.below kha-kannada.below ga-kannada.below gha-kannada.below nga-kannada.below ca-kannada.below cha-kannada.below ja-kannada.below jha-kannada.below nya-kannada.below tta-kannada.below ttha-kannada.below dda-kannada.below ddha-kannada.below nna-kannada.below ta-kannada.below tha-kannada.below da-kannada.below dha-kannada.below na-kannada.below pa-kannada.below pha-kannada.below ba-kannada.below bha-kannada.below ma-kannada.below ya-kannada.below la-kannada.below va-kannada.below sha-kannada.below ssa-kannada.below sa-kannada.below ha-kannada.below rra-kannada.below lla-kannada.below fa-kannada.below ka_ssa-kannada.below ta_ra-kannada.below ra-kannada.below.following rVocalicMatra-kannada.following rrVocalicMatra-kannada.following ailength-kannada.following ka-kannada.below.following kha-kannada.below.following ga-kannada.below.following gha-kannada.below.following nga-kannada.below.following ca-kannada.below.following cha-kannada.below.following ja-kannada.below.following jha-kannada.below.following nya-kannada.below.following tta-kannada.below.following ttha-kannada.below.following dda-kannada.below.following ddha-kannada.below.following nna-kannada.below.following ta-kannada.below.following tha-kannada.below.following da-kannada.below.following dha-kannada.below.following na-kannada.below.following pa-kannada.below.following pha-kannada.below.following ba-kannada.below.following bha-kannada.below.following ma-kannada.below.following ya-kannada.below.following la-kannada.below.following va-kannada.below.following sha-kannada.below.following ssa-kannada.below.following sa-kannada.below.following ha-kannada.below.following rra-kannada.below.following lla-kannada.below.following fa-kannada.below.following ka_ssa-kannada.below.following ta_ra-kannada.below.following ];";

        let source = Source::new(PathBuf::from("test"), A_BAD_LINE.into());
        let err = Diagnostic::warning(source.id(), 200..220, "bad!");
        let mut write_to = String::new();
        write_diagnostic(&mut write_to, &err, &source, None, true);
    }
}