1use crate::types::{FileId, Location};
4use crate::{SourceContext, SourceInfo};
5use std::borrow::Cow;
6
7#[derive(Debug, Clone, PartialEq)]
9pub struct MappedLocation {
10 pub file_id: FileId,
12 pub location: Location,
14}
15
16impl SourceInfo {
17 pub fn map_offset(&self, offset: usize, ctx: &SourceContext) -> Option<MappedLocation> {
19 match self {
20 SourceInfo::Original {
21 file_id,
22 start_offset,
23 ..
24 } => {
25 let file = ctx.get_file(*file_id)?;
27 let file_info = file.file_info.as_ref()?;
28
29 let absolute_offset = start_offset + offset;
31
32 let content: Cow<'_, str> = match &file.content {
38 Some(c) => Cow::Borrowed(c.as_str()),
39 None => Cow::Owned(std::fs::read_to_string(&file.path).ok()?),
40 };
41
42 let location = file_info.offset_to_location(absolute_offset, &content)?;
44
45 Some(MappedLocation {
46 file_id: *file_id,
47 location,
48 })
49 }
50 SourceInfo::Substring {
51 parent,
52 start_offset,
53 ..
54 } => {
55 let parent_offset = start_offset + offset;
57 parent.map_offset(parent_offset, ctx)
58 }
59 SourceInfo::Concat { pieces } => {
60 for piece in pieces {
62 let piece_start = piece.offset_in_concat;
63 let piece_end = piece_start + piece.length;
64
65 if offset >= piece_start && offset < piece_end {
66 let offset_in_piece = offset - piece_start;
68 return piece.source_info.map_offset(offset_in_piece, ctx);
69 }
70 }
71 if let Some(last) = pieces.last()
74 && offset == last.offset_in_concat + last.length
75 {
76 return last.source_info.map_offset(last.source_info.length(), ctx);
77 }
78 None }
80 SourceInfo::Generated { .. } => {
81 None
84 }
85 }
86 }
87
88 pub fn map_range(
90 &self,
91 start: usize,
92 end: usize,
93 ctx: &SourceContext,
94 ) -> Option<(MappedLocation, MappedLocation)> {
95 let start_mapped = self.map_offset(start, ctx)?;
96 let end_mapped = self.map_offset(end, ctx)?;
97 Some((start_mapped, end_mapped))
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use crate::types::{Location, Range};
104 use crate::{SourceContext, SourceInfo};
105
106 #[test]
107 fn test_map_offset_original() {
108 let mut ctx = SourceContext::new();
109 let file_id = ctx.add_file("test.qmd".to_string(), Some("hello\nworld".to_string()));
110
111 let info = SourceInfo::from_range(
112 file_id,
113 Range {
114 start: Location {
115 offset: 0,
116 row: 0,
117 column: 0,
118 },
119 end: Location {
120 offset: 11,
121 row: 1,
122 column: 5,
123 },
124 },
125 );
126
127 let mapped = info.map_offset(0, &ctx).unwrap();
129 assert_eq!(mapped.file_id, file_id);
130 assert_eq!(mapped.location.offset, 0);
131 assert_eq!(mapped.location.row, 0);
132 assert_eq!(mapped.location.column, 0);
133
134 let mapped = info.map_offset(6, &ctx).unwrap();
136 assert_eq!(mapped.file_id, file_id);
137 assert_eq!(mapped.location.offset, 6);
138 assert_eq!(mapped.location.row, 1);
139 assert_eq!(mapped.location.column, 0);
140 }
141
142 #[test]
143 fn test_map_offset_disk_backed_file() {
144 let dir = std::env::temp_dir().join(format!(
147 "quarto-source-map-map-offset-{}",
148 std::process::id()
149 ));
150 std::fs::create_dir_all(&dir).unwrap();
151 let path = dir.join("disk.qmd");
152 std::fs::write(&path, "hello\nwörld\n").unwrap();
153
154 let mut ctx = SourceContext::new();
155 let file_id = ctx.add_file(path.to_string_lossy().into_owned(), None);
156 assert!(ctx.get_file(file_id).unwrap().content.is_none());
157
158 let info = SourceInfo::original(file_id, 0, 13);
159 let mapped = info.map_offset(9, &ctx).unwrap();
161 assert_eq!(mapped.file_id, file_id);
162 assert_eq!(mapped.location.offset, 9);
163 assert_eq!(mapped.location.row, 1);
164 assert_eq!(mapped.location.column, 2);
165
166 std::fs::remove_dir_all(&dir).unwrap();
167 }
168
169 #[test]
170 fn test_map_offset_substring() {
171 let mut ctx = SourceContext::new();
172 let file_id = ctx.add_file("test.qmd".to_string(), Some("0123456789".to_string()));
173
174 let original = SourceInfo::from_range(
175 file_id,
176 Range {
177 start: Location {
178 offset: 0,
179 row: 0,
180 column: 0,
181 },
182 end: Location {
183 offset: 10,
184 row: 0,
185 column: 10,
186 },
187 },
188 );
189
190 let substring = SourceInfo::substring(original, 3, 7);
192
193 let mapped = substring.map_offset(0, &ctx).unwrap();
195 assert_eq!(mapped.file_id, file_id);
196 assert_eq!(mapped.location.offset, 3);
197
198 let mapped = substring.map_offset(2, &ctx).unwrap();
200 assert_eq!(mapped.file_id, file_id);
201 assert_eq!(mapped.location.offset, 5);
202 }
203
204 #[test]
205 fn test_map_offset_concat() {
206 let mut ctx = SourceContext::new();
207 let file_id1 = ctx.add_file("first.qmd".to_string(), Some("AAA".to_string()));
208 let file_id2 = ctx.add_file("second.qmd".to_string(), Some("BBB".to_string()));
209
210 let info1 = SourceInfo::from_range(
211 file_id1,
212 Range {
213 start: Location {
214 offset: 0,
215 row: 0,
216 column: 0,
217 },
218 end: Location {
219 offset: 3,
220 row: 0,
221 column: 3,
222 },
223 },
224 );
225
226 let info2 = SourceInfo::from_range(
227 file_id2,
228 Range {
229 start: Location {
230 offset: 0,
231 row: 0,
232 column: 0,
233 },
234 end: Location {
235 offset: 3,
236 row: 0,
237 column: 3,
238 },
239 },
240 );
241
242 let concat = SourceInfo::concat(vec![(info1, 3), (info2, 3)]);
244
245 let mapped = concat.map_offset(1, &ctx).unwrap();
247 assert_eq!(mapped.file_id, file_id1);
248 assert_eq!(mapped.location.offset, 1);
249
250 let mapped = concat.map_offset(4, &ctx).unwrap();
252 assert_eq!(mapped.file_id, file_id2);
253 assert_eq!(mapped.location.offset, 1);
254
255 let mapped = concat.map_offset(6, &ctx).unwrap();
257 assert_eq!(mapped.file_id, file_id2);
258 assert_eq!(mapped.location.offset, 3);
259
260 let (start, end) = concat.map_range(0, 6, &ctx).unwrap();
262 assert_eq!(start.file_id, file_id1);
263 assert_eq!(start.location.offset, 0);
264 assert_eq!(end.file_id, file_id2);
265 assert_eq!(end.location.offset, 3);
266 }
267
268 #[test]
276 fn test_map_offset_concat_exclusive_end_all_verbatim_is_gating() {
277 let mut ctx = SourceContext::new();
283 let file_id = ctx.add_file("test.qmd".to_string(), Some("hello world".to_string()));
284
285 let first = SourceInfo::from_range(
286 file_id,
287 Range {
288 start: Location {
289 offset: 0,
290 row: 0,
291 column: 0,
292 },
293 end: Location {
294 offset: 4,
295 row: 0,
296 column: 4,
297 },
298 },
299 );
300 let last = SourceInfo::from_range(
301 file_id,
302 Range {
303 start: Location {
304 offset: 4,
305 row: 0,
306 column: 4,
307 },
308 end: Location {
309 offset: 9,
310 row: 0,
311 column: 9,
312 },
313 },
314 );
315 let concat = SourceInfo::concat(vec![(first, 4), (last, 5)]);
316
317 let mapped = concat.map_offset(9, &ctx).unwrap();
319 assert_eq!(mapped.location.offset, 9);
320 }
321
322 #[test]
323 fn test_map_offset_concat_exclusive_end_replacement_terminated() {
324 let mut ctx = SourceContext::new();
330 let file_id = ctx.add_file("test.qmd".to_string(), Some("012345678".to_string()));
331
332 let first = SourceInfo::from_range(
333 file_id,
334 Range {
335 start: Location {
336 offset: 0,
337 row: 0,
338 column: 0,
339 },
340 end: Location {
341 offset: 7,
342 row: 0,
343 column: 7,
344 },
345 },
346 );
347 let replacement = SourceInfo::from_range(
348 file_id,
349 Range {
350 start: Location {
351 offset: 7,
352 row: 0,
353 column: 7,
354 },
355 end: Location {
356 offset: 9,
357 row: 0,
358 column: 9,
359 },
360 },
361 );
362 let concat = SourceInfo::concat(vec![(first, 7), (replacement, 1)]);
365
366 let mapped = concat.map_offset(8, &ctx).unwrap();
368 assert_eq!(mapped.location.offset, 9);
369 }
370
371 #[test]
372 fn test_map_offset_concat_exclusive_end_synthesis_terminated() {
373 let mut ctx = SourceContext::new();
381 let file_id = ctx.add_file("test.qmd".to_string(), Some("hello world".to_string())); let synthesis = SourceInfo::from_range(
384 file_id,
385 Range {
386 start: Location {
387 offset: 11,
388 row: 0,
389 column: 11,
390 },
391 end: Location {
392 offset: 11,
393 row: 0,
394 column: 11,
395 },
396 },
397 );
398 let concat = SourceInfo::concat(vec![(synthesis, 1)]);
399
400 let mapped = concat.map_offset(1, &ctx).unwrap();
402 assert_eq!(mapped.location.offset, 11);
403 }
404
405 #[test]
406 fn test_map_range() {
407 let mut ctx = SourceContext::new();
408 let file_id = ctx.add_file("test.qmd".to_string(), Some("hello\nworld".to_string()));
409
410 let info = SourceInfo::from_range(
411 file_id,
412 Range {
413 start: Location {
414 offset: 0,
415 row: 0,
416 column: 0,
417 },
418 end: Location {
419 offset: 11,
420 row: 1,
421 column: 5,
422 },
423 },
424 );
425
426 let (start, end) = info.map_range(0, 5, &ctx).unwrap();
428 assert_eq!(start.file_id, file_id);
429 assert_eq!(start.location.offset, 0);
430 assert_eq!(end.file_id, file_id);
431 assert_eq!(end.location.offset, 5);
432 }
433}