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 if text.len() > MAX_FILE_SIZE {
167 let mut cut = MAX_FILE_SIZE;
168 while !text.is_char_boundary(cut) {
169 cut -= 1;
170 }
171 text.truncate(cut);
172 }
173 let id = FileId(to_u32(self.files.len()));
174 self.files.push(SourceFile::new(name.into(), text));
175 id
176 }
177
178 pub fn get(&self, id: FileId) -> Option<&SourceFile> {
180 self.files.get(id.index())
181 }
182
183 pub fn iter(&self) -> impl Iterator<Item = (FileId, &SourceFile)> {
185 self.files
186 .iter()
187 .enumerate()
188 .map(|(index, file)| (FileId(to_u32(index)), file))
189 }
190}
191
192fn to_u32(value: usize) -> u32 {
195 u32::try_from(value).unwrap_or(u32::MAX)
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 fn file(text: &str) -> SourceFile {
203 SourceFile::new("test.wolf".to_string(), text.to_string())
204 }
205
206 #[test]
207 fn line_col_counts_scalar_values() {
208 let f = file("ab\nпривет x\n");
209 assert_eq!(f.line_col(0), LineCol { line: 1, column: 1 });
210 assert_eq!(f.line_col(3), LineCol { line: 2, column: 1 });
211 assert_eq!(f.line_col(3 + 13), LineCol { line: 2, column: 8 });
213 }
214
215 #[test]
216 fn line_col_at_end_of_file() {
217 let f = file("a\nb");
218 assert_eq!(f.line_col(3), LineCol { line: 2, column: 2 });
219 assert_eq!(f.line_col(100), LineCol { line: 2, column: 2 });
220 }
221
222 #[test]
223 fn line_col_inside_a_character_is_clamped() {
224 let f = file("я");
225 assert_eq!(f.line_col(1).line, 1);
226 }
227
228 #[test]
229 fn line_ranges_include_terminators() {
230 let f = file("a\r\nbc\n");
231 assert_eq!(f.line_range(0), Some(0..3));
232 assert_eq!(f.line_range(1), Some(3..6));
233 assert_eq!(f.line_range(2), Some(6..6));
234 assert_eq!(f.line_range(3), None);
235 }
236
237 #[test]
238 fn span_to_merges() {
239 let mut map = SourceMap::new();
240 let id = map.add("a", "hello".to_string());
241 let a = Span::new(id, 1, 2);
242 let b = Span::new(id, 3, 5);
243 assert_eq!(a.to(b), Span::new(id, 1, 5));
244 assert_eq!(Span::new(id, 4, 2).len(), 0);
245 }
246}