Skip to main content

rexact_syntax/
locate.rs

1//! Positions and caret rendering (FR-A06, docs/07 rule 2).
2
3use crate::error::RexactError;
4
5/// 1-based (line, column) of a scalar offset. Columns count Unicode scalar
6/// values; only `\n` starts a new line.
7pub fn locate(chars: &[char], offset: usize) -> (usize, usize) {
8    let end = offset.min(chars.len());
9    let mut line = 1;
10    let mut col = 1;
11    for &c in &chars[..end] {
12        if c == '\n' {
13            line += 1;
14            col = 1;
15        } else {
16            col += 1;
17        }
18    }
19    (line, col)
20}
21
22impl RexactError {
23    /// Message followed by the offending source line and a caret under the
24    /// error column, when the error has a position.
25    pub fn render(&self, source: &str) -> String {
26        let mut out = self.to_string();
27        if let Some((line, col)) = self.position {
28            if let Some(text) = source.split('\n').nth(line - 1) {
29                out.push_str(&format!("\n  {text}\n  {}^", " ".repeat(col - 1)));
30            }
31        }
32        out
33    }
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39    use crate::error::ErrorCode;
40
41    #[test]
42    fn locate_counts_scalars_and_lines() {
43        let c: Vec<char> = "ab\nc😀d".chars().collect();
44        assert_eq!(locate(&c, 0), (1, 1));
45        assert_eq!(locate(&c, 2), (1, 3));
46        assert_eq!(locate(&c, 3), (2, 1));
47        assert_eq!(locate(&c, 5), (2, 3)); // after c and the emoji (2 scalars)
48        assert_eq!(locate(&c, 99), (2, 4)); // clamped to end
49    }
50
51    #[test]
52    fn caret_sits_under_the_column() {
53        let mut e = RexactError::new(ErrorCode::P007);
54        e.position = Some((1, 3));
55        let r = e.render("ab[");
56        assert!(r.ends_with("\n  ab[\n    ^"), "{r}");
57    }
58}