aiken_lang/
line_numbers.rs

1use std::fmt::{self, Display};
2
3#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
4pub struct LineNumbers {
5    line_starts: Vec<usize>,
6    length: usize,
7    last: Option<usize>,
8}
9
10#[derive(Debug, PartialEq, Clone, Copy)]
11pub struct LineColumn {
12    pub line: usize,
13    pub column: usize,
14}
15
16impl Display for LineColumn {
17    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
18        f.write_str(&format!("L{};{}", self.line, self.column))
19    }
20}
21
22impl LineNumbers {
23    pub fn new(src: &str) -> Self {
24        let line_starts: Vec<usize> = std::iter::once(0)
25            .chain(src.match_indices('\n').map(|(i, _)| i + 1))
26            .collect();
27
28        let length = src.len();
29
30        Self {
31            length,
32            last: line_starts.last().cloned(),
33            line_starts: if length > 0 { line_starts } else { Vec::new() },
34        }
35    }
36
37    /// Get the line number for a byte index
38    pub fn line_number(&self, byte_index: usize) -> Option<usize> {
39        self.line_starts
40            .binary_search(&byte_index)
41            .map(|l| Some(l + 1))
42            .unwrap_or_else(|next_index| {
43                if Some(next_index) >= self.last {
44                    None
45                } else {
46                    Some(next_index)
47                }
48            })
49    }
50
51    pub fn line_and_column_number(&self, byte_index: usize) -> Option<LineColumn> {
52        let line = self.line_number(byte_index)?;
53        let column = byte_index - self.line_starts.get(line - 1).copied().unwrap_or_default() + 1;
54        Some(LineColumn { line, column })
55    }
56
57    #[allow(dead_code)]
58    pub fn byte_index(&self, line: usize, character: usize) -> usize {
59        match self.line_starts.get(line) {
60            Some(line_index) => *line_index + character,
61            None => self.length,
62        }
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use chumsky::text::Character;
70    use indoc::indoc;
71
72    fn assert_line_column(src: &str, ix: usize, lcol: Option<LineColumn>) {
73        let lines = LineNumbers::new(src);
74
75        println!("{lines:?}");
76
77        let byte = src
78            .as_bytes()
79            .get(ix)
80            .map(|b| {
81                if b.is_ascii() {
82                    format!("{}", b.to_char())
83                } else {
84                    format!("{b}")
85                }
86            })
87            .unwrap_or_else(|| "OUT-OF-BOUNDS".to_string());
88
89        assert_eq!(
90            lines.line_and_column_number(ix),
91            lcol,
92            "\n{src}\n--> at index {ix} ({byte})\n",
93        );
94    }
95
96    #[test]
97    fn out_of_range_byte_index() {
98        let src = indoc! { r#""# };
99        assert_line_column(src, 42, None);
100        assert_line_column(src, 0, None);
101    }
102
103    #[test]
104    fn basic() {
105        let src = indoc! { r#"
106            foo
107            bar
108        "# };
109
110        assert_line_column(src, 0, Some(LineColumn { line: 1, column: 1 }));
111        assert_line_column(src, 2, Some(LineColumn { line: 1, column: 3 }));
112        assert_line_column(src, 4, Some(LineColumn { line: 2, column: 1 }));
113    }
114
115    #[test]
116    fn unicode() {
117        let src = indoc! { r#"
118            💩
119            foo
120        "# };
121
122        assert_line_column(src, 0, Some(LineColumn { line: 1, column: 1 }));
123        assert_line_column(src, 2, Some(LineColumn { line: 1, column: 3 }));
124        assert_line_column(src, 5, Some(LineColumn { line: 2, column: 1 }));
125    }
126}