aiken_lang/
line_numbers.rs1use 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 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}