Skip to main content

varyk_syntax/
source.rs

1//! Source files and the line index that maps a byte offset to a one-based
2//! line and column.
3
4use std::path::PathBuf;
5
6use crate::span::FileId;
7
8/// Maps byte offsets into a source text to one-based line and column
9/// positions. Built once from the text, from the byte offset where each
10/// line starts.
11#[derive(Debug, Clone, PartialEq, Eq)]
12struct LineIndex {
13    /// Byte offset of the start of each line, `line_starts[0] == 0`.
14    line_starts: Vec<u32>,
15}
16
17impl LineIndex {
18    fn new(text: &str) -> Self {
19        let mut line_starts = vec![0u32];
20        for (offset, byte) in text.bytes().enumerate() {
21            if byte == b'\n' {
22                line_starts.push((offset + 1) as u32);
23            }
24        }
25        Self { line_starts }
26    }
27
28    /// Converts a byte offset to a one-based `(line, column)` pair by
29    /// finding the last line start at or before `offset`.
30    fn line_col(&self, offset: u32) -> (u32, u32) {
31        let line = match self.line_starts.binary_search(&offset) {
32            Ok(exact) => exact,
33            Err(insertion_point) => insertion_point - 1,
34        };
35        let column = offset - self.line_starts[line] + 1;
36        ((line + 1) as u32, column)
37    }
38}
39
40/// A single source file: its id, path, full text, and a line index built
41/// once so byte offsets convert to line/column cheaply.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct SourceFile {
44    pub id: FileId,
45    pub path: PathBuf,
46    pub text: String,
47    line_index: LineIndex,
48}
49
50impl SourceFile {
51    pub fn new(id: FileId, path: impl Into<PathBuf>, text: impl Into<String>) -> Self {
52        let text = text.into();
53        let line_index = LineIndex::new(&text);
54        Self {
55            id,
56            path: path.into(),
57            text,
58            line_index,
59        }
60    }
61
62    /// Converts a byte offset into this file's text to a one-based
63    /// `(line, column)` pair.
64    pub fn line_col(&self, offset: u32) -> (u32, u32) {
65        self.line_index.line_col(offset)
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn first_line_is_one_one() {
75        let file = SourceFile::new(FileId(0), "test.vr", "let x;\n");
76        assert_eq!(file.line_col(0), (1, 1));
77        assert_eq!(file.line_col(4), (1, 5));
78    }
79
80    #[test]
81    fn second_line_offsets_convert_through_the_line_index() {
82        // Byte offsets:
83        // "let x;\n" -> 0..=6, second line starts at byte 7.
84        // "let y;\n" -> second line is "let y;\n"
85        let file = SourceFile::new(FileId(0), "test.vr", "let x;\nlet y;\n");
86        // 'l' of the second "let" is at offset 7: line 2, column 1.
87        assert_eq!(file.line_col(7), (2, 1));
88        // 'y' of the second line is at offset 11: line 2, column 5.
89        assert_eq!(file.line_col(11), (2, 5));
90    }
91
92    #[test]
93    fn third_line_after_two_newlines() {
94        let file = SourceFile::new(FileId(0), "test.vr", "a\nb\nc");
95        assert_eq!(file.line_col(4), (3, 1));
96    }
97}