summaryrefslogtreecommitdiff
path: root/src/syntax/lines.rs
blob: 86fc461bd55d514d2279c970856890f069ef88aa (plain) (blame)
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
//! Conversion of byte positions to line/column locations.

use std::fmt::{self, Debug, Display, Formatter};

use super::Pos;
use crate::parse::is_newline_char;

/// Enables conversion of byte position to locations.
pub struct LineMap<'s> {
    src: &'s str,
    line_starts: Vec<Pos>,
}

impl<'s> LineMap<'s> {
    /// Create a new line map for a source string.
    pub fn new(src: &'s str) -> Self {
        let mut line_starts = vec![Pos::ZERO];
        let mut iter = src.char_indices().peekable();

        while let Some((mut i, c)) = iter.next() {
            if is_newline_char(c) {
                i += c.len_utf8();
                if c == '\r' && matches!(iter.peek(), Some((_, '\n'))) {
                    i += '\n'.len_utf8();
                    iter.next();
                }

                line_starts.push(Pos(i as u32));
            }
        }

        Self { src, line_starts }
    }

    /// Convert a byte position to a location.
    ///
    /// # Panics
    /// This panics if the position is out of bounds.
    pub fn location(&self, pos: Pos) -> Location {
        let line_index = match self.line_starts.binary_search(&pos) {
            Ok(i) => i,
            Err(i) => i - 1,
        };

        let line_start = self.line_starts[line_index];
        let head = &self.src[line_start.to_usize() .. pos.to_usize()];
        let column_index = head.chars().count();

        Location {
            line: 1 + line_index as u32,
            column: 1 + column_index as u32,
        }
    }
}

/// One-indexed line-column position in source code.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct Location {
    /// The one-indexed line.
    pub line: u32,
    /// The one-indexed column.
    pub column: u32,
}

impl Location {
    /// Create a new location from line and column.
    pub fn new(line: u32, column: u32) -> Self {
        Self { line, column }
    }
}

impl Debug for Location {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        Display::fmt(self, f)
    }
}

impl Display for Location {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "{}:{}", self.line, self.column)
    }
}

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

    const TEST: &str = "äbcde\nf💛g\r\nhi\rjkl";

    #[test]
    fn test_line_map_new() {
        let map = LineMap::new(TEST);
        assert_eq!(map.line_starts, vec![Pos(0), Pos(7), Pos(15), Pos(18)]);
    }

    #[test]
    fn test_line_map_location() {
        let map = LineMap::new(TEST);
        assert_eq!(map.location(Pos(0)), Location::new(1, 1));
        assert_eq!(map.location(Pos(2)), Location::new(1, 2));
        assert_eq!(map.location(Pos(6)), Location::new(1, 6));
        assert_eq!(map.location(Pos(7)), Location::new(2, 1));
        assert_eq!(map.location(Pos(8)), Location::new(2, 2));
        assert_eq!(map.location(Pos(12)), Location::new(2, 3));
        assert_eq!(map.location(Pos(21)), Location::new(4, 4));
    }

    #[test]
    #[should_panic]
    fn test_line_map_panics_out_of_bounds() {
        LineMap::new(TEST).location(Pos(22));
    }
}