1use std::path::PathBuf;
7
8pub use bock_errors::{FileId, Span};
9
10pub struct SourceFile {
14 pub id: FileId,
15 pub path: PathBuf,
16 pub content: String,
17 line_starts: Vec<usize>,
19}
20
21impl SourceFile {
22 #[must_use]
24 pub fn new(id: FileId, path: PathBuf, content: String) -> Self {
25 let line_starts = std::iter::once(0)
26 .chain(content.match_indices('\n').map(|(i, _)| i + 1))
27 .collect();
28 Self {
29 id,
30 path,
31 content,
32 line_starts,
33 }
34 }
35
36 #[must_use]
44 pub fn line_col(&self, offset: usize) -> (usize, usize) {
45 assert!(offset <= self.content.len(), "offset out of range");
46
47 let line_idx = match self.line_starts.binary_search(&offset) {
49 Ok(i) => i,
50 Err(i) => i - 1,
51 };
52
53 let line_start = self.line_starts[line_idx];
54 let col = self.content[line_start..offset].chars().count() + 1;
55 (line_idx + 1, col)
56 }
57
58 #[must_use]
64 pub fn line_content(&self, line: usize) -> &str {
65 assert!(line >= 1, "line must be 1-indexed");
66 let idx = line - 1;
67 let start = self.line_starts[idx];
68 let end = self
69 .line_starts
70 .get(idx + 1)
71 .map(|&s| {
72 if s > 0 && self.content.as_bytes()[s - 1] == b'\n' {
74 s - 1
75 } else {
76 s
77 }
78 })
79 .unwrap_or(self.content.len());
80 &self.content[start..end]
81 }
82
83 #[must_use]
88 pub fn slice(&self, span: Span) -> &str {
89 &self.content[span.start..span.end]
90 }
91}
92
93#[derive(Default)]
97pub struct SourceMap {
98 files: Vec<SourceFile>,
99}
100
101impl SourceMap {
102 #[must_use]
104 pub fn new() -> Self {
105 Self::default()
106 }
107
108 pub fn add_file(&mut self, path: PathBuf, content: String) -> FileId {
110 let id = FileId(self.files.len() as u32);
111 self.files.push(SourceFile::new(id, path, content));
112 id
113 }
114
115 #[must_use]
120 pub fn get_file(&self, id: FileId) -> &SourceFile {
121 &self.files[id.0 as usize]
122 }
123}
124
125#[cfg(test)]
128mod tests {
129 use super::*;
130
131 fn make_file(content: &str) -> SourceFile {
132 SourceFile::new(FileId(1), PathBuf::from("test.bock"), content.to_string())
133 }
134
135 #[test]
138 fn line_col_start_of_file() {
139 let f = make_file("hello\nworld");
140 assert_eq!(f.line_col(0), (1, 1));
141 }
142
143 #[test]
144 fn line_col_end_of_first_line() {
145 let f = make_file("hello\nworld");
146 assert_eq!(f.line_col(4), (1, 5));
148 }
149
150 #[test]
151 fn line_col_start_of_second_line() {
152 let f = make_file("hello\nworld");
153 assert_eq!(f.line_col(6), (2, 1));
155 }
156
157 #[test]
158 fn line_col_end_of_file() {
159 let f = make_file("ab\ncd");
160 assert_eq!(f.line_col(5), (2, 3));
161 }
162
163 #[test]
164 fn line_col_single_line() {
165 let f = make_file("abcde");
166 assert_eq!(f.line_col(3), (1, 4));
167 }
168
169 #[test]
172 fn line_col_multibyte_char() {
173 let f = make_file("aéx");
175 let e_offset = "a".len(); let x_offset = "aé".len(); assert_eq!(f.line_col(e_offset), (1, 2)); assert_eq!(f.line_col(x_offset), (1, 3));
179 }
180
181 #[test]
182 fn line_col_emoji() {
183 let f = make_file("a🦀b");
185 let crab_offset = 1_usize;
186 let b_offset = 1 + "🦀".len(); assert_eq!(f.line_col(crab_offset), (1, 2));
188 assert_eq!(f.line_col(b_offset), (1, 3));
189 }
190
191 #[test]
192 fn line_col_multibyte_on_second_line() {
193 let f = make_file("hello\nwörld");
194 let o_offset = "hello\nw".len(); assert_eq!(f.line_col(o_offset), (2, 2));
197 }
198
199 #[test]
202 fn line_content_first_line() {
203 let f = make_file("hello\nworld");
204 assert_eq!(f.line_content(1), "hello");
205 }
206
207 #[test]
208 fn line_content_second_line() {
209 let f = make_file("hello\nworld");
210 assert_eq!(f.line_content(2), "world");
211 }
212
213 #[test]
214 fn line_content_single_line_no_newline() {
215 let f = make_file("only");
216 assert_eq!(f.line_content(1), "only");
217 }
218
219 #[test]
220 fn line_content_empty_line() {
221 let f = make_file("a\n\nb");
222 assert_eq!(f.line_content(2), "");
223 }
224
225 #[test]
228 fn slice_basic() {
229 let f = make_file("hello world");
230 let span = Span {
231 file: FileId(1),
232 start: 6,
233 end: 11,
234 };
235 assert_eq!(f.slice(span), "world");
236 }
237
238 #[test]
239 fn slice_whole_file() {
240 let f = make_file("abc");
241 let span = Span {
242 file: FileId(1),
243 start: 0,
244 end: 3,
245 };
246 assert_eq!(f.slice(span), "abc");
247 }
248
249 #[test]
250 fn slice_multibyte() {
251 let f = make_file("a🦀b");
252 let span = Span {
253 file: FileId(1),
254 start: 1,
255 end: 5,
256 };
257 assert_eq!(f.slice(span), "🦀");
258 }
259
260 #[test]
263 fn source_map_add_and_get() {
264 let mut map = SourceMap::new();
265 let id = map.add_file(PathBuf::from("a.bock"), "fn main() {}".to_string());
266 assert_eq!(id, FileId(0));
267 let file = map.get_file(id);
268 assert_eq!(file.content, "fn main() {}");
269 }
270
271 #[test]
272 fn source_map_multiple_files() {
273 let mut map = SourceMap::new();
274 let id0 = map.add_file(PathBuf::from("a.bock"), "aaa".to_string());
275 let id1 = map.add_file(PathBuf::from("b.bock"), "bbb".to_string());
276 assert_eq!(id0, FileId(0));
277 assert_eq!(id1, FileId(1));
278 assert_eq!(map.get_file(id0).content, "aaa");
279 assert_eq!(map.get_file(id1).content, "bbb");
280 }
281
282 #[test]
283 fn source_map_file_id_matches() {
284 let mut map = SourceMap::new();
285 let id = map.add_file(PathBuf::from("x.bock"), "x".to_string());
286 assert_eq!(map.get_file(id).id, id);
287 }
288}