1use std::path::PathBuf;
5
6use crate::span::FileId;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
12struct LineIndex {
13 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 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#[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 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 let file = SourceFile::new(FileId(0), "test.vr", "let x;\nlet y;\n");
86 assert_eq!(file.line_col(7), (2, 1));
88 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}