source_map/
lines_columns_indexes.rs

1/// Contains the byte indexes of when line starts
2#[derive(Clone, Debug)]
3pub struct LineStarts(pub(crate) Vec<usize>);
4
5impl LineStarts {
6    /// Implementation copied from [codespan-reporting](https://docs.rs/codespan-reporting/0.11.1/codespan_reporting/)
7    pub fn new(source: &str) -> LineStarts {
8        Self(
9            std::iter::once(0)
10                .chain(source.match_indices('\n').map(|(i, _)| i + 1))
11                .collect(),
12        )
13    }
14
15    pub fn append(&mut self, start: usize, appended: &str) {
16        self.0
17            .extend(appended.match_indices('\n').map(|(i, _)| i + 1 + start))
18    }
19
20    pub fn byte_indexes_on_same_line(&self, pos1: usize, pos2: usize) -> bool {
21        debug_assert!(pos1 <= pos2);
22        self.0
23            .windows(2)
24            .find_map(|w| {
25                let range = w[0]..=w[1];
26                range.contains(&pos1).then_some(range)
27            })
28            .expect("could not find splits for pos1")
29            .contains(&pos2)
30    }
31
32    pub fn byte_indexes_crosses_lines(&self, pos1: usize, pos2: usize) -> usize {
33        debug_assert!(pos1 <= pos2);
34        let first_line_backwards = self.get_line_pos_is_on(pos1);
35        let second_line_backwards = self.get_line_pos_is_on(pos2);
36        second_line_backwards - first_line_backwards
37    }
38
39    pub fn byte_indexes_on_different_lines(&self, pos1: usize, pos2: usize) -> bool {
40        self.byte_indexes_crosses_lines(pos1, pos2) > 0
41    }
42
43    /// 0 indexed
44    pub(crate) fn get_line_pos_is_on(&self, pos: usize) -> usize {
45        self.get_line_and_column_pos_is_on(pos).0
46    }
47
48    /// 0 indexed
49    pub(crate) fn get_line_and_column_pos_is_on(&self, pos: usize) -> (usize, usize) {
50        let backwards_index = self
51            .0
52            .iter()
53            .rev()
54            .position(|index| pos >= *index)
55            .expect("pos out of bounds");
56
57        let line = (self.0.len() - 1) - backwards_index;
58        (line, pos - self.0[line])
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::LineStarts;
65
66    fn get_source() -> String {
67        std::fs::read_to_string("README.md").expect("No README")
68    }
69
70    #[test]
71    fn split_lines() {
72        let source = get_source();
73
74        let line_starts = LineStarts::new(&source);
75        let expected_lines = source.lines().collect::<Vec<_>>();
76        let mut actual_lines = Vec::new();
77
78        let mut iterator = line_starts.0.into_iter();
79        let mut last = iterator.next().unwrap();
80        for part in iterator {
81            let value = &source[last..part];
82            let value = value.strip_suffix('\n').unwrap();
83            let value = value.strip_suffix('\r').unwrap_or(value);
84            actual_lines.push(value);
85            last = part;
86        }
87
88        assert_eq!(expected_lines, actual_lines);
89    }
90
91    #[test]
92    fn append() {
93        let source = get_source();
94
95        let whole = LineStarts::new(&source);
96        let at = 100;
97        let (left, right) = source.split_at(at);
98
99        let mut left = LineStarts::new(left);
100        left.append(at, right);
101
102        assert_eq!(whole.0, left.0);
103    }
104
105    #[test]
106    fn byte_indexes_crosses_lines() {
107        let source = get_source();
108
109        let line_starts = LineStarts::new(&source);
110
111        let start = 100;
112        let end = 200;
113        let lines_in_between = source[start..end].chars().filter(|c| *c == '\n').count();
114
115        assert_eq!(
116            line_starts.byte_indexes_crosses_lines(start, end),
117            lines_in_between
118        );
119    }
120
121    #[test]
122    fn byte_indexes_on_same_line() {
123        let source = get_source();
124
125        let line_starts = LineStarts::new(&source);
126
127        let start = 100;
128        let end = start
129            + source[start..]
130                .chars()
131                .take_while(|c| *c == '\n')
132                .map(|c| c.len_utf16())
133                .sum::<usize>();
134
135        assert!(line_starts.byte_indexes_on_same_line(start, end));
136    }
137}