1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/// Contains the byte indexes of when line starts
#[derive(Clone, Debug)]
pub struct LineStarts(pub(crate) Vec<usize>);

impl LineStarts {
    /// Implementation copied from [codespan-reporting](https://docs.rs/codespan-reporting/0.11.1/codespan_reporting/)
    pub fn new(source: &str) -> LineStarts {
        Self(
            std::iter::once(0)
                .chain(source.match_indices('\n').map(|(i, _)| i + 1))
                .collect(),
        )
    }

    pub fn append(&mut self, start: usize, appended: &str) {
        self.0
            .extend(appended.match_indices('\n').map(|(i, _)| i + 1 + start))
    }

    pub fn byte_indexes_on_same_line(&self, pos1: usize, pos2: usize) -> bool {
        debug_assert!(pos1 <= pos2);
        self.0
            .windows(2)
            .find_map(|w| {
                let range = w[0]..=w[1];
                range.contains(&pos1).then_some(range)
            })
            .expect("could not find splits for pos1")
            .contains(&pos2)
    }

    pub fn byte_indexes_crosses_lines(&self, pos1: usize, pos2: usize) -> usize {
        debug_assert!(pos1 <= pos2);
        let first_line_backwards = self.get_index_of_line_pos_is_on(pos1);
        let second_line_backwards = self.get_index_of_line_pos_is_on(pos2);
        second_line_backwards - first_line_backwards
    }

    pub fn byte_indexes_on_different_lines(&self, pos1: usize, pos2: usize) -> bool {
        self.byte_indexes_crosses_lines(pos1, pos2) > 0
    }

    pub(crate) fn get_index_of_line_pos_is_on(&self, pos: usize) -> usize {
        let backwards_index = self
            .0
            .iter()
            .rev()
            .position(|index| pos >= *index)
            .expect("pos1 out of bounds");

        (self.0.len() - 1) - backwards_index
    }
}

#[cfg(test)]
mod tests {
    use super::LineStarts;

    fn get_source() -> String {
        std::fs::read_to_string("README.md").expect("No README")
    }

    #[test]
    fn split_lines() {
        let source = get_source();

        let line_starts = LineStarts::new(&source);
        let expected_lines = source.lines().collect::<Vec<_>>();
        let mut actual_lines = Vec::new();

        let mut iterator = line_starts.0.into_iter();
        let mut last = iterator.next().unwrap();
        for part in iterator {
            let value = &source[last..part];
            let value = value.strip_suffix('\n').unwrap();
            let value = value.strip_suffix('\r').unwrap_or(value);
            actual_lines.push(value);
            last = part;
        }

        assert_eq!(expected_lines, actual_lines);
    }

    #[test]
    fn append() {
        let source = get_source();

        let whole = LineStarts::new(&source);
        let at = 100;
        let (left, right) = source.split_at(at);

        let mut left = LineStarts::new(left);
        left.append(at, right);

        assert_eq!(whole.0, left.0);
    }

    #[test]
    fn byte_indexes_crosses_lines() {
        let source = get_source();

        let line_starts = LineStarts::new(&source);

        let start = 100;
        let end = 200;
        let lines_in_between = source[start..end].chars().filter(|c| *c == '\n').count();

        assert_eq!(
            line_starts.byte_indexes_crosses_lines(start, end),
            lines_in_between
        );
    }

    #[test]
    fn byte_indexes_on_same_line() {
        let source = get_source();

        let line_starts = LineStarts::new(&source);

        let start = 100;
        let end = start
            + source[start..]
                .chars()
                .take_while(|c| *c == '\n')
                .map(|c| c.len_utf16())
                .sum::<usize>();

        assert!(line_starts.byte_indexes_on_same_line(start, end));
    }
}