rexact-syntax 0.2.0

Regex parsers for ECMAScript, Python, Go, Rust, PCRE2 and Java dialects, with an error catalogue
Documentation
//! Positions and caret rendering (FR-A06, docs/07 rule 2).

use crate::error::RexactError;

/// 1-based (line, column) of a scalar offset. Columns count Unicode scalar
/// values; only `\n` starts a new line.
pub fn locate(chars: &[char], offset: usize) -> (usize, usize) {
    let end = offset.min(chars.len());
    let mut line = 1;
    let mut col = 1;
    for &c in &chars[..end] {
        if c == '\n' {
            line += 1;
            col = 1;
        } else {
            col += 1;
        }
    }
    (line, col)
}

impl RexactError {
    /// Message followed by the offending source line and a caret under the
    /// error column, when the error has a position.
    pub fn render(&self, source: &str) -> String {
        let mut out = self.to_string();
        if let Some((line, col)) = self.position {
            if let Some(text) = source.split('\n').nth(line - 1) {
                out.push_str(&format!("\n  {text}\n  {}^", " ".repeat(col - 1)));
            }
        }
        out
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::error::ErrorCode;

    #[test]
    fn locate_counts_scalars_and_lines() {
        let c: Vec<char> = "ab\nc😀d".chars().collect();
        assert_eq!(locate(&c, 0), (1, 1));
        assert_eq!(locate(&c, 2), (1, 3));
        assert_eq!(locate(&c, 3), (2, 1));
        assert_eq!(locate(&c, 5), (2, 3)); // after c and the emoji (2 scalars)
        assert_eq!(locate(&c, 99), (2, 4)); // clamped to end
    }

    #[test]
    fn caret_sits_under_the_column() {
        let mut e = RexactError::new(ErrorCode::P007);
        e.position = Some((1, 3));
        let r = e.render("ab[");
        assert!(r.ends_with("\n  ab[\n    ^"), "{r}");
    }
}