deed_diagnostics/
source.rs1use crate::span::Span;
8
9#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
11pub struct FileId(u32);
12
13impl FileId {
14 pub fn index(&self) -> u32 {
16 self.0
17 }
18}
19
20#[derive(Clone, Copy, PartialEq, Eq, Debug)]
25pub struct Location {
26 pub line: u32,
27 pub column: u32,
28}
29
30pub struct SourceFile {
32 name: String,
33 text: String,
34 line_starts: Vec<u32>,
35}
36
37impl SourceFile {
38 fn new(name: String, text: String) -> Self {
39 let mut line_starts = vec![0u32];
40 for (offset, byte) in text.bytes().enumerate() {
41 if byte == b'\n' {
42 line_starts.push(offset as u32 + 1);
43 }
44 }
45 Self {
46 name,
47 text,
48 line_starts,
49 }
50 }
51
52 pub fn name(&self) -> &str {
53 &self.name
54 }
55
56 pub fn text(&self) -> &str {
57 &self.text
58 }
59
60 pub fn line_count(&self) -> u32 {
61 self.line_starts.len() as u32
62 }
63
64 pub fn location(&self, offset: u32) -> Location {
69 let offset = offset.min(self.text.len() as u32);
70 let line_index = match self.line_starts.binary_search(&offset) {
71 Ok(exact) => exact,
72 Err(next) => next - 1,
73 };
74 let line_start = self.line_starts[line_index] as usize;
75 let column = self.text[line_start..offset as usize].chars().count() as u32 + 1;
76 Location {
77 line: line_index as u32 + 1,
78 column,
79 }
80 }
81
82 pub fn line_text(&self, line: u32) -> &str {
84 if line == 0 || line > self.line_count() {
85 return "";
86 }
87 let start = self.line_starts[line as usize - 1] as usize;
88 let end = self
89 .line_starts
90 .get(line as usize)
91 .map(|&next| next as usize)
92 .unwrap_or(self.text.len());
93 self.text[start..end].trim_end_matches(['\n', '\r'])
94 }
95
96 pub fn slice(&self, span: Span) -> &str {
97 let end = (span.end as usize).min(self.text.len());
98 let start = (span.start as usize).min(end);
99 &self.text[start..end]
100 }
101}
102
103#[derive(Default)]
105pub struct SourceMap {
106 files: Vec<SourceFile>,
107}
108
109impl SourceMap {
110 pub fn new() -> Self {
111 Self::default()
112 }
113
114 pub fn add(&mut self, name: impl Into<String>, text: impl Into<String>) -> FileId {
115 let id = FileId(self.files.len() as u32);
116 self.files.push(SourceFile::new(name.into(), text.into()));
117 id
118 }
119
120 pub fn file(&self, id: FileId) -> &SourceFile {
125 &self.files[id.0 as usize]
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::SourceMap;
132
133 fn map(text: &str) -> (SourceMap, super::FileId) {
134 let mut map = SourceMap::new();
135 let id = map.add("test.deed", text);
136 (map, id)
137 }
138
139 #[test]
140 fn locations_are_one_based() {
141 let (map, id) = map("abc\ndef\n");
142 let file = map.file(id);
143 assert_eq!(file.location(0), super::Location { line: 1, column: 1 });
144 assert_eq!(file.location(2), super::Location { line: 1, column: 3 });
145 assert_eq!(file.location(4), super::Location { line: 2, column: 1 });
146 }
147
148 #[test]
149 fn newline_belongs_to_the_line_it_ends() {
150 let (map, id) = map("ab\ncd");
151 let file = map.file(id);
152 assert_eq!(file.location(2), super::Location { line: 1, column: 3 });
153 assert_eq!(file.location(3), super::Location { line: 2, column: 1 });
154 }
155
156 #[test]
157 fn columns_count_characters_not_bytes() {
158 let (map, id) = map("çğü x");
159 let file = map.file(id);
160 let offset = "çğü ".len() as u32;
161 assert_eq!(
162 file.location(offset),
163 super::Location { line: 1, column: 5 }
164 );
165 }
166
167 #[test]
168 fn offsets_past_the_end_clamp() {
169 let (map, id) = map("ab");
170 let file = map.file(id);
171 assert_eq!(file.location(999), super::Location { line: 1, column: 3 });
172 }
173
174 #[test]
175 fn line_text_drops_the_newline() {
176 let (map, id) = map("first\r\nsecond\n");
177 let file = map.file(id);
178 assert_eq!(file.line_text(1), "first");
179 assert_eq!(file.line_text(2), "second");
180 assert_eq!(file.line_text(99), "");
181 }
182
183 #[test]
184 fn the_last_line_is_still_a_line() {
185 let (map, id) = map("only");
189 let file = map.file(id);
190 assert_eq!(file.line_count(), 1);
191 assert_eq!(file.line_text(1), "only");
192 assert_eq!(file.line_text(2), "");
193 }
194
195 #[test]
196 fn file_ids_are_the_order_files_were_added() {
197 let mut map = SourceMap::new();
198 let first = map.add("a.deed", "");
199 let second = map.add("b.deed", "");
200 assert_eq!(first.index(), 0);
201 assert_eq!(second.index(), 1);
202 assert_eq!(map.file(first).name(), "a.deed");
205 assert_eq!(map.file(second).name(), "b.deed");
206 }
207}