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.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]
237 fn test_map_range() {
238 let mut ctx = SourceContext::new();
239 let file_id = ctx.add_file("test.qmd".to_string(), Some("hello\nworld".to_string()));
240
241 let info = SourceInfo::from_range(
242 file_id,
243 Range {
244 start: Location {
245 offset: 0,
246 row: 0,
247 column: 0,
248 },
249 end: Location {
250 offset: 11,
251 row: 1,
252 column: 5,
253 },
254 },
255 );
256
257 let (start, end) = info.map_range(0, 5, &ctx).unwrap();
259 assert_eq!(start.file_id, file_id);
260 assert_eq!(start.location.offset, 0);
261 assert_eq!(end.file_id, file_id);
262 assert_eq!(end.location.offset, 5);
263 }
264}