1use std::path::Path;
5use std::{fmt, fs, io};
6
7#[derive(Debug, Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
9pub struct SourceFile {
10 pub contents: String,
12 file_names: Vec<String>,
14 file_lines: Vec<usize>,
16 line_lengths: Vec<usize>,
18}
19
20impl SourceFile {
21 pub fn new() -> Self {
23 Default::default()
24 }
25
26 pub fn add_file(&mut self, filename: impl AsRef<Path>) -> io::Result<()> {
31 let filename = filename.as_ref();
32 let file = fs::read_to_string(filename)?;
33
34 self.add_file_raw(filename.display(), file);
36 Ok(())
37 }
38
39 pub fn add_file_raw(&mut self, name: impl fmt::Display, contents: impl Into<String>) {
40 let contents = contents.into();
41 if contents.is_empty() {
43 return;
44 }
45
46 let mut num_lines = 0;
47 let mut lines = contents.split('\n').peekable();
49 while let Some(line) = lines.next() {
50 if lines.peek().is_some() {
51 num_lines += 1;
53 self.line_lengths.push(line.len() + 1);
54 } else if line.is_empty() {
55 } else {
57 num_lines += 1;
59 self.line_lengths.push(line.len());
60 }
61 }
62
63 self.file_names.push(name.to_string());
65 self.file_lines.push(num_lines);
67 self.contents += &contents;
68 }
69
70 pub fn resolve_offset<'a>(&'a self, offset: usize) -> Option<Position<'a>> {
76 let mut line_acc = *self.line_lengths.get(0)?;
78 let mut line_idx = 0;
79 while line_acc <= offset {
80 line_idx += 1;
81 line_acc += *self.line_lengths.get(line_idx)?;
83 }
84 line_acc -= self.line_lengths[line_idx];
86
87 let mut file_acc = self.file_lines[0];
89 let mut file_idx = 0;
90 while file_acc <= line_idx {
91 file_idx += 1;
92 file_acc += self.file_lines[file_idx];
93 }
94 file_acc -= self.file_lines[file_idx];
96
97 Some(Position::new(
98 &self.file_names[file_idx],
99 line_idx - file_acc,
100 offset - line_acc,
101 ))
102 }
103
104 pub fn resolve_offset_span<'a>(&'a self, start: usize, end: usize) -> Option<Span<'a>> {
108 if end < start {
109 return None;
110 }
111 Some(Span {
112 start: self.resolve_offset(start)?,
113 end: self.resolve_offset(end)?,
114 })
115 }
116}
117
118#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
120pub struct Position<'a> {
121 pub filename: &'a str,
123 pub line: usize,
125 pub col: usize,
127}
128
129impl<'a> Position<'a> {
130 fn new(filename: &'a str, line: usize, col: usize) -> Position<'a> {
132 Position {
133 filename: filename.as_ref(),
134 line,
135 col,
136 }
137 }
138}
139
140#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
142pub struct Span<'a> {
143 pub start: Position<'a>,
144 pub end: Position<'a>,
145}
146
147#[cfg(test)]
148mod tests {
149 extern crate tempfile;
150
151 use self::tempfile::NamedTempFile;
152 use super::{Position, SourceFile, Span};
153 use std::io::Write;
154
155 #[test]
156 fn empty() {
157 let sourcefile = SourceFile::default();
158 assert!(sourcefile.resolve_offset(0).is_none());
159 }
160
161 #[test]
162 fn smoke() {
163 test_files(
164 &[
165 "A file with\ntwo lines.\n",
166 "Another file with\ntwo more lines.\n",
167 ],
168 &[
169 (0, (0, 0, 0)), (5, (0, 0, 5)), (11, (0, 0, 11)), (12, (0, 1, 0)), (13, (0, 1, 1)),
174 (13, (0, 1, 1)),
175 (22, (0, 1, 10)),
176 (23, (1, 0, 0)),
177 (24, (1, 0, 1)),
178 (40, (1, 0, 17)),
179 (41, (1, 1, 0)),
180 (42, (1, 1, 1)),
181 (56, (1, 1, 15)),
182 ],
184 &[((0, 5), (0, 0, 0), (0, 0, 5))],
185 )
186 }
187
188 fn test_files<'a>(
189 files: &[impl AsRef<str>],
190 offset_tests: &[(usize, (usize, usize, usize))],
191 offset_span_tests: &[((usize, usize), (usize, usize, usize), (usize, usize, usize))],
192 ) {
193 let mut sourcefile = SourceFile::default();
194 let mut file_handles = Vec::new(); for contents in files {
196 let mut file = NamedTempFile::new().unwrap();
197 write!(file, "{}", contents.as_ref()).unwrap();
198 sourcefile.add_file(file.path()).unwrap();
199 file_handles.push(file);
200 }
201
202 for &(offset, (file_idx, line, col)) in offset_tests {
203 let filename = format!("{}", file_handles[file_idx].path().display());
204 let pos = sourcefile.resolve_offset(offset);
205 assert_eq!(pos.unwrap(), Position::new(&filename, line, col));
206 }
207
208 for &(
209 (start, end),
210 (file_idx_start, line_start, col_start),
211 (file_idx_end, line_end, col_end),
212 ) in offset_span_tests
213 {
214 let start_filename = format!("{}", file_handles[file_idx_start].path().display());
215 let end_filename = format!("{}", file_handles[file_idx_end].path().display());
216 assert_eq!(
217 sourcefile.resolve_offset_span(start, end).unwrap(),
218 Span {
219 start: Position::new(&start_filename, line_start, col_start),
220 end: Position::new(&end_filename, line_end, col_end),
221 }
222 );
223 }
224 }
225
226 #[test]
227 fn test_raw() {
228 let mut sourcefile = SourceFile::new();
229 sourcefile.add_file_raw("test", " ");
230 assert_eq!(*sourcefile.line_lengths.last().unwrap(), 1);
231 }
232}