1use crate::types::{FileId, Location};
4use crate::{SourceContext, SourceInfo};
5
6#[derive(Debug, Clone, PartialEq)]
8pub struct MappedLocation {
9 pub file_id: FileId,
11 pub location: Location,
13}
14
15impl SourceInfo {
16 pub fn map_offset(&self, offset: usize, ctx: &SourceContext) -> Option<MappedLocation> {
18 match self {
19 SourceInfo::Original {
20 file_id,
21 start_offset,
22 ..
23 } => {
24 let file = ctx.get_file(*file_id)?;
26 let file_info = file.file_info.as_ref()?;
27
28 let absolute_offset = start_offset + offset;
30
31 let content = match &file.content {
33 Some(c) => c.clone(),
34 None => std::fs::read_to_string(&file.path).ok()?,
35 };
36
37 let location = file_info.offset_to_location(absolute_offset, &content)?;
39
40 Some(MappedLocation {
41 file_id: *file_id,
42 location,
43 })
44 }
45 SourceInfo::Substring {
46 parent,
47 start_offset,
48 ..
49 } => {
50 let parent_offset = start_offset + offset;
52 parent.map_offset(parent_offset, ctx)
53 }
54 SourceInfo::Concat { pieces } => {
55 for piece in pieces {
57 let piece_start = piece.offset_in_concat;
58 let piece_end = piece_start + piece.length;
59
60 if offset >= piece_start && offset < piece_end {
61 let offset_in_piece = offset - piece_start;
63 return piece.source_info.map_offset(offset_in_piece, ctx);
64 }
65 }
66 if let Some(last) = pieces.last()
69 && offset == last.offset_in_concat + last.length
70 {
71 return last.source_info.map_offset(last.source_info.length(), ctx);
72 }
73 None }
75 SourceInfo::Generated { .. } => {
76 None
79 }
80 }
81 }
82
83 pub fn map_range(
85 &self,
86 start: usize,
87 end: usize,
88 ctx: &SourceContext,
89 ) -> Option<(MappedLocation, MappedLocation)> {
90 let start_mapped = self.map_offset(start, ctx)?;
91 let end_mapped = self.map_offset(end, ctx)?;
92 Some((start_mapped, end_mapped))
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use crate::types::{Location, Range};
99 use crate::{SourceContext, SourceInfo};
100
101 #[test]
102 fn test_map_offset_original() {
103 let mut ctx = SourceContext::new();
104 let file_id = ctx.add_file("test.qmd".to_string(), Some("hello\nworld".to_string()));
105
106 let info = SourceInfo::from_range(
107 file_id,
108 Range {
109 start: Location {
110 offset: 0,
111 row: 0,
112 column: 0,
113 },
114 end: Location {
115 offset: 11,
116 row: 1,
117 column: 5,
118 },
119 },
120 );
121
122 let mapped = info.map_offset(0, &ctx).unwrap();
124 assert_eq!(mapped.file_id, file_id);
125 assert_eq!(mapped.location.offset, 0);
126 assert_eq!(mapped.location.row, 0);
127 assert_eq!(mapped.location.column, 0);
128
129 let mapped = info.map_offset(6, &ctx).unwrap();
131 assert_eq!(mapped.file_id, file_id);
132 assert_eq!(mapped.location.offset, 6);
133 assert_eq!(mapped.location.row, 1);
134 assert_eq!(mapped.location.column, 0);
135 }
136
137 #[test]
138 fn test_map_offset_substring() {
139 let mut ctx = SourceContext::new();
140 let file_id = ctx.add_file("test.qmd".to_string(), Some("0123456789".to_string()));
141
142 let original = SourceInfo::from_range(
143 file_id,
144 Range {
145 start: Location {
146 offset: 0,
147 row: 0,
148 column: 0,
149 },
150 end: Location {
151 offset: 10,
152 row: 0,
153 column: 10,
154 },
155 },
156 );
157
158 let substring = SourceInfo::substring(original, 3, 7);
160
161 let mapped = substring.map_offset(0, &ctx).unwrap();
163 assert_eq!(mapped.file_id, file_id);
164 assert_eq!(mapped.location.offset, 3);
165
166 let mapped = substring.map_offset(2, &ctx).unwrap();
168 assert_eq!(mapped.file_id, file_id);
169 assert_eq!(mapped.location.offset, 5);
170 }
171
172 #[test]
173 fn test_map_offset_concat() {
174 let mut ctx = SourceContext::new();
175 let file_id1 = ctx.add_file("first.qmd".to_string(), Some("AAA".to_string()));
176 let file_id2 = ctx.add_file("second.qmd".to_string(), Some("BBB".to_string()));
177
178 let info1 = SourceInfo::from_range(
179 file_id1,
180 Range {
181 start: Location {
182 offset: 0,
183 row: 0,
184 column: 0,
185 },
186 end: Location {
187 offset: 3,
188 row: 0,
189 column: 3,
190 },
191 },
192 );
193
194 let info2 = SourceInfo::from_range(
195 file_id2,
196 Range {
197 start: Location {
198 offset: 0,
199 row: 0,
200 column: 0,
201 },
202 end: Location {
203 offset: 3,
204 row: 0,
205 column: 3,
206 },
207 },
208 );
209
210 let concat = SourceInfo::concat(vec![(info1, 3), (info2, 3)]);
212
213 let mapped = concat.map_offset(1, &ctx).unwrap();
215 assert_eq!(mapped.file_id, file_id1);
216 assert_eq!(mapped.location.offset, 1);
217
218 let mapped = concat.map_offset(4, &ctx).unwrap();
220 assert_eq!(mapped.file_id, file_id2);
221 assert_eq!(mapped.location.offset, 1);
222
223 let mapped = concat.map_offset(6, &ctx).unwrap();
225 assert_eq!(mapped.file_id, file_id2);
226 assert_eq!(mapped.location.offset, 3);
227
228 let (start, end) = concat.map_range(0, 6, &ctx).unwrap();
230 assert_eq!(start.file_id, file_id1);
231 assert_eq!(start.location.offset, 0);
232 assert_eq!(end.file_id, file_id2);
233 assert_eq!(end.location.offset, 3);
234 }
235
236 #[test]
244 fn test_map_offset_concat_exclusive_end_all_verbatim_is_gating() {
245 let mut ctx = SourceContext::new();
251 let file_id = ctx.add_file("test.qmd".to_string(), Some("hello world".to_string()));
252
253 let first = SourceInfo::from_range(
254 file_id,
255 Range {
256 start: Location {
257 offset: 0,
258 row: 0,
259 column: 0,
260 },
261 end: Location {
262 offset: 4,
263 row: 0,
264 column: 4,
265 },
266 },
267 );
268 let last = SourceInfo::from_range(
269 file_id,
270 Range {
271 start: Location {
272 offset: 4,
273 row: 0,
274 column: 4,
275 },
276 end: Location {
277 offset: 9,
278 row: 0,
279 column: 9,
280 },
281 },
282 );
283 let concat = SourceInfo::concat(vec![(first, 4), (last, 5)]);
284
285 let mapped = concat.map_offset(9, &ctx).unwrap();
287 assert_eq!(mapped.location.offset, 9);
288 }
289
290 #[test]
291 fn test_map_offset_concat_exclusive_end_replacement_terminated() {
292 let mut ctx = SourceContext::new();
298 let file_id = ctx.add_file("test.qmd".to_string(), Some("012345678".to_string()));
299
300 let first = SourceInfo::from_range(
301 file_id,
302 Range {
303 start: Location {
304 offset: 0,
305 row: 0,
306 column: 0,
307 },
308 end: Location {
309 offset: 7,
310 row: 0,
311 column: 7,
312 },
313 },
314 );
315 let replacement = SourceInfo::from_range(
316 file_id,
317 Range {
318 start: Location {
319 offset: 7,
320 row: 0,
321 column: 7,
322 },
323 end: Location {
324 offset: 9,
325 row: 0,
326 column: 9,
327 },
328 },
329 );
330 let concat = SourceInfo::concat(vec![(first, 7), (replacement, 1)]);
333
334 let mapped = concat.map_offset(8, &ctx).unwrap();
336 assert_eq!(mapped.location.offset, 9);
337 }
338
339 #[test]
340 fn test_map_offset_concat_exclusive_end_synthesis_terminated() {
341 let mut ctx = SourceContext::new();
349 let file_id = ctx.add_file("test.qmd".to_string(), Some("hello world".to_string())); let synthesis = SourceInfo::from_range(
352 file_id,
353 Range {
354 start: Location {
355 offset: 11,
356 row: 0,
357 column: 11,
358 },
359 end: Location {
360 offset: 11,
361 row: 0,
362 column: 11,
363 },
364 },
365 );
366 let concat = SourceInfo::concat(vec![(synthesis, 1)]);
367
368 let mapped = concat.map_offset(1, &ctx).unwrap();
370 assert_eq!(mapped.location.offset, 11);
371 }
372
373 #[test]
374 fn test_map_range() {
375 let mut ctx = SourceContext::new();
376 let file_id = ctx.add_file("test.qmd".to_string(), Some("hello\nworld".to_string()));
377
378 let info = SourceInfo::from_range(
379 file_id,
380 Range {
381 start: Location {
382 offset: 0,
383 row: 0,
384 column: 0,
385 },
386 end: Location {
387 offset: 11,
388 row: 1,
389 column: 5,
390 },
391 },
392 );
393
394 let (start, end) = info.map_range(0, 5, &ctx).unwrap();
396 assert_eq!(start.file_id, file_id);
397 assert_eq!(start.location.offset, 0);
398 assert_eq!(end.file_id, file_id);
399 assert_eq!(end.location.offset, 5);
400 }
401}