Skip to main content

hermes_support/
line_index.rs

1//! Line-start index for a source buffer: maps byte offsets to 1-based
2//! (line, column) and back. Column is the byte distance from the line start,
3//! matching LLVH `SourceMgr` so that caret rendering is byte-compatible.
4
5/// Cached table of line-start byte offsets for one buffer.
6pub struct LineIndex {
7    /// `line_starts[i]` = byte offset of the start of (1-based) line `i + 1`.
8    /// Always begins with 0.
9    line_starts: Vec<u32>,
10}
11
12impl LineIndex {
13    /// Build the index over `bytes` (the buffer contents, excluding the NUL
14    /// terminator). A line starts at offset 0 and after each `\n`.
15    pub fn build(bytes: &[u8]) -> LineIndex {
16        let mut line_starts = Vec::with_capacity(64);
17        line_starts.push(0u32);
18        for (i, &b) in bytes.iter().enumerate() {
19            if b == b'\n' {
20                line_starts.push((i + 1) as u32);
21            }
22        }
23        LineIndex { line_starts }
24    }
25
26    /// Return 1-based `(line, col)` for `offset`. `col` is 1-based byte distance
27    /// from the line start.
28    pub fn line_col(&self, offset: u32) -> (u32, u32) {
29        // Largest line whose start is <= offset.
30        let line0 = match self.line_starts.binary_search(&offset) {
31            Ok(i) => i,
32            Err(i) => i - 1,
33        };
34        let col = offset - self.line_starts[line0] + 1;
35        ((line0 + 1) as u32, col)
36    }
37
38    /// Return the slice of `bytes` for 1-based `line`, including its trailing
39    /// EOL if present. The final line may have no EOL.
40    pub fn line_ref<'a>(&self, bytes: &'a [u8], line: u32) -> &'a [u8] {
41        let start = self.line_starts[(line - 1) as usize] as usize;
42        let end = self
43            .line_starts
44            .get(line as usize)
45            .map(|&e| e as usize)
46            .unwrap_or(bytes.len());
47        &bytes[start..end]
48    }
49
50    /// Number of lines.
51    #[allow(dead_code)]
52    pub fn line_count(&self) -> u32 {
53        self.line_starts.len() as u32
54    }
55
56    /// Byte offset where 1-based `line` starts (= `line_starts[line - 1]`).
57    /// Panics if `line` is out of range; callers should guard with `line_count()`.
58    pub fn line_start(&self, line: u32) -> u32 {
59        self.line_starts[(line - 1) as usize]
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn lf_line_and_col() {
69        // "ab\ncde\nf"  offsets: a0 b1 \n2 c3 d4 e5 \n6 f7
70        let idx = LineIndex::build(b"ab\ncde\nf");
71        assert_eq!(idx.line_col(0), (1, 1)); // 'a'
72        assert_eq!(idx.line_col(1), (1, 2)); // 'b'
73        assert_eq!(idx.line_col(3), (2, 1)); // 'c'
74        assert_eq!(idx.line_col(5), (2, 3)); // 'e'
75        assert_eq!(idx.line_col(7), (3, 1)); // 'f' (last line, no EOL)
76    }
77
78    #[test]
79    fn crlf_column_counts_bytes() {
80        // "a\r\nb": a0 \r1 \n2 b3 -> 'b' is line 2 col 1
81        let idx = LineIndex::build(b"a\r\nb");
82        assert_eq!(idx.line_col(0), (1, 1));
83        assert_eq!(idx.line_col(3), (2, 1));
84    }
85
86    #[test]
87    fn line_ref_excludes_nothing_before_eol() {
88        let bytes = b"ab\ncde\nf";
89        let idx = LineIndex::build(bytes);
90        // 1-based line number -> the line slice including its EOL if present.
91        assert_eq!(idx.line_ref(bytes, 1), b"ab\n");
92        assert_eq!(idx.line_ref(bytes, 3), b"f");
93    }
94
95    #[test]
96    fn empty_buffer_has_one_line() {
97        let idx = LineIndex::build(b"");
98        assert_eq!(idx.line_count(), 1);
99        assert_eq!(idx.line_col(0), (1, 1));
100        assert_eq!(idx.line_ref(b"", 1), b"");
101    }
102
103    #[test]
104    fn trailing_newline_starts_an_empty_last_line() {
105        // A trailing '\n' terminates line 1; line 2 exists but is empty.
106        // Matches LLVH SourceMgr's line counting.
107        let bytes = b"a\n";
108        let idx = LineIndex::build(bytes);
109        assert_eq!(idx.line_count(), 2);
110        assert_eq!(idx.line_ref(bytes, 1), b"a\n");
111        assert_eq!(idx.line_ref(bytes, 2), b"");
112        assert_eq!(idx.line_col(2), (2, 1));
113    }
114}