1use std::ops::Range;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
11pub struct FileId(u32);
12
13impl FileId {
14 pub const ZERO: FileId = FileId(0);
16
17 pub fn index(self) -> usize {
19 self.0 as usize
20 }
21}
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
25pub struct Span {
26 pub file: FileId,
28 pub start: u32,
30 pub end: u32,
32}
33
34impl Span {
35 pub fn new(file: FileId, start: u32, end: u32) -> Span {
37 Span {
38 file,
39 start,
40 end: end.max(start),
41 }
42 }
43
44 pub fn range(self) -> Range<usize> {
46 self.start as usize..self.end as usize
47 }
48
49 pub fn len(self) -> u32 {
51 self.end - self.start
52 }
53
54 pub fn is_empty(self) -> bool {
56 self.start == self.end
57 }
58
59 pub fn to(self, other: Span) -> Span {
62 if self.file != other.file {
63 return self;
64 }
65 Span::new(
66 self.file,
67 self.start.min(other.start),
68 self.end.max(other.end),
69 )
70 }
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub struct LineCol {
76 pub line: u32,
78 pub column: u32,
80}
81
82pub const MAX_FILE_SIZE: usize = u32::MAX as usize;
84
85#[derive(Debug)]
87pub struct SourceFile {
88 name: String,
89 text: String,
90 line_starts: Vec<u32>,
91}
92
93impl SourceFile {
94 fn new(name: String, text: String) -> SourceFile {
95 let mut line_starts = vec![0];
96 for (offset, byte) in text.bytes().enumerate() {
97 if byte == b'\n' {
98 line_starts.push(to_u32(offset + 1));
99 }
100 }
101 SourceFile {
102 name,
103 text,
104 line_starts,
105 }
106 }
107
108 pub fn name(&self) -> &str {
110 &self.name
111 }
112
113 pub fn text(&self) -> &str {
115 &self.text
116 }
117
118 pub fn line_index(&self, offset: u32) -> usize {
120 match self.line_starts.binary_search(&offset) {
121 Ok(line) => line,
122 Err(next) => next.saturating_sub(1),
123 }
124 }
125
126 pub fn line_range(&self, index: usize) -> Option<Range<usize>> {
129 let start = *self.line_starts.get(index)? as usize;
130 let end = match self.line_starts.get(index + 1) {
131 Some(next) => *next as usize,
132 None => self.text.len(),
133 };
134 Some(start..end)
135 }
136
137 pub fn line_col(&self, offset: u32) -> LineCol {
140 let line = self.line_index(offset);
141 let line_start = self.line_starts.get(line).copied().unwrap_or(0) as usize;
142 let offset = (offset as usize).min(self.text.len()).max(line_start);
143 let prefix = self.text.get(line_start..offset).unwrap_or("");
144 LineCol {
145 line: to_u32(line + 1),
146 column: to_u32(prefix.chars().count() + 1),
147 }
148 }
149}
150
151#[derive(Debug, Default)]
153pub struct SourceMap {
154 files: Vec<SourceFile>,
155}
156
157impl SourceMap {
158 pub fn new() -> SourceMap {
160 SourceMap::default()
161 }
162
163 pub fn add(&mut self, name: impl Into<String>, mut text: String) -> FileId {
166 #[allow(clippy::absurd_extreme_comparisons)]
168 if text.len() > MAX_FILE_SIZE {
169 let mut cut = MAX_FILE_SIZE;
170 while !text.is_char_boundary(cut) {
171 cut -= 1;
172 }
173 text.truncate(cut);
174 }
175 let id = FileId(to_u32(self.files.len()));
176 self.files.push(SourceFile::new(name.into(), text));
177 id
178 }
179
180 pub fn get(&self, id: FileId) -> Option<&SourceFile> {
182 self.files.get(id.index())
183 }
184
185 pub fn iter(&self) -> impl Iterator<Item = (FileId, &SourceFile)> {
187 self.files
188 .iter()
189 .enumerate()
190 .map(|(index, file)| (FileId(to_u32(index)), file))
191 }
192}
193
194fn to_u32(value: usize) -> u32 {
197 u32::try_from(value).unwrap_or(u32::MAX)
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 fn file(text: &str) -> SourceFile {
205 SourceFile::new("test.wolf".to_string(), text.to_string())
206 }
207
208 #[test]
209 fn line_col_counts_scalar_values() {
210 let f = file("ab\nпривет x\n");
211 assert_eq!(f.line_col(0), LineCol { line: 1, column: 1 });
212 assert_eq!(f.line_col(3), LineCol { line: 2, column: 1 });
213 assert_eq!(f.line_col(3 + 13), LineCol { line: 2, column: 8 });
215 }
216
217 #[test]
218 fn line_col_at_end_of_file() {
219 let f = file("a\nb");
220 assert_eq!(f.line_col(3), LineCol { line: 2, column: 2 });
221 assert_eq!(f.line_col(100), LineCol { line: 2, column: 2 });
222 }
223
224 #[test]
225 fn line_col_inside_a_character_is_clamped() {
226 let f = file("я");
227 assert_eq!(f.line_col(1).line, 1);
228 }
229
230 #[test]
231 fn line_ranges_include_terminators() {
232 let f = file("a\r\nbc\n");
233 assert_eq!(f.line_range(0), Some(0..3));
234 assert_eq!(f.line_range(1), Some(3..6));
235 assert_eq!(f.line_range(2), Some(6..6));
236 assert_eq!(f.line_range(3), None);
237 }
238
239 #[test]
240 fn span_to_merges() {
241 let mut map = SourceMap::new();
242 let id = map.add("a", "hello".to_string());
243 let a = Span::new(id, 1, 2);
244 let b = Span::new(id, 3, 5);
245 assert_eq!(a.to(b), Span::new(id, 1, 5));
246 assert_eq!(Span::new(id, 4, 2).len(), 0);
247 }
248}