1use crate::Span;
6
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub struct SourceId(pub usize);
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct SourcePosition {
13 pub line: usize,
14 pub column: usize,
15}
16
17#[derive(Debug, Clone)]
18pub struct SourceFile {
19 name: String,
20 text: String,
21 line_starts: Vec<usize>,
22 complete: bool,
23 omissions: Vec<crate::OmittedSourceLine>,
24 frames: Option<crate::resource::SourceLineFrames>,
25}
26
27impl SourceFile {
28 pub fn name(&self) -> &str {
29 &self.name
30 }
31 pub fn text(&self) -> &str {
32 &self.text
33 }
34
35 pub fn is_complete(&self) -> bool {
40 self.complete
41 }
42
43 pub fn omitted_lines(&self) -> &[crate::OmittedSourceLine] {
46 &self.omissions
47 }
48
49 pub fn omitted_slice(&self, span: crate::OmittedSourceSpan) -> Option<&[u8]> {
51 self.omissions
52 .iter()
53 .find(|line| line.line == span.line)?
54 .bytes
55 .get(span.start..span.end)
56 }
57
58 pub(crate) fn include_lines(&self) -> Vec<crate::include_indent::Line> {
59 crate::resource::framed_snapshot_lines(&self.text, &self.omissions, self.frames.as_ref())
60 }
61
62 pub(crate) fn has_line_frames(&self) -> bool {
63 self.frames.is_some()
64 }
65
66 pub(crate) fn bom_normalized(&self) -> bool {
69 self.frames.as_ref().is_some_and(|frames| frames.normalized)
70 }
71
72 pub(crate) fn stored_bytes(&self) -> usize {
73 self.omissions.iter().fold(self.text.len(), |size, line| {
74 size.saturating_add(line.bytes.len())
75 })
76 }
77
78 pub fn position(&self, offset: usize) -> Option<SourcePosition> {
79 self.text.get(..offset)?;
80 let line = self.line_starts.partition_point(|&start| start <= offset) - 1;
81 Some(SourcePosition {
82 line: line
83 + 1
84 + self
85 .omissions
86 .iter()
87 .filter(|omitted| omitted.offset <= offset && omitted.bytes.ends_with(b"\n"))
88 .count(),
89 column: self.text[self.line_starts[line]..offset].chars().count() + 1,
90 })
91 }
92}
93
94#[derive(Debug, Clone, Default)]
95pub struct SourceMap {
96 files: Vec<SourceFile>,
97}
98
99impl SourceMap {
100 pub fn new() -> Self {
101 Self::default()
102 }
103
104 pub fn add(&mut self, name: impl Into<String>, text: impl Into<String>) -> SourceId {
106 self.add_snapshot(name.into(), text.into(), true, Vec::new(), None)
107 }
108
109 fn add_snapshot(
110 &mut self,
111 name: String,
112 text: String,
113 complete: bool,
114 omissions: Vec<crate::OmittedSourceLine>,
115 frames: Option<crate::resource::SourceLineFrames>,
116 ) -> SourceId {
117 let line_starts = if let Some(frames) = &frames {
118 let mut starts: Vec<_> = frames.ranges.iter().map(|&(start, _)| start).collect();
119 if starts.is_empty() {
120 starts.push(0);
121 }
122 if frames.trailing_lf && starts.last() != Some(&text.len()) {
123 starts.push(text.len());
124 }
125 starts
126 } else {
127 let mut starts = vec![0];
128 starts.extend(text.match_indices('\n').map(|(index, _)| index + 1));
129 starts
130 };
131 let id = SourceId(self.files.len());
132 self.files.push(SourceFile {
133 name,
134 text,
135 line_starts,
136 complete,
137 omissions,
138 frames,
139 });
140 id
141 }
142
143 pub fn get(&self, id: SourceId) -> Option<&SourceFile> {
144 self.files.get(id.0)
145 }
146
147 pub fn load(
152 &mut self,
153 target: &str,
154 relative_to: Option<SourceId>,
155 provider: &dyn crate::ResourceProvider,
156 ) -> crate::ResourceResult<SourceId> {
157 self.load_using(target, relative_to, |request| {
158 provider.load(request).map(Into::into)
159 })
160 }
161
162 pub fn load_document(
168 &mut self,
169 target: &str,
170 relative_to: Option<SourceId>,
171 provider: &dyn crate::ResourceProvider,
172 ) -> crate::ResourceResult<SourceId> {
173 self.load_using(target, relative_to, |request| {
174 crate::resource::load_source_text(provider, request).map(Into::into)
175 })
176 }
177
178 pub fn load_with_encoding(
183 &mut self,
184 target: &str,
185 relative_to: Option<SourceId>,
186 provider: &dyn crate::ResourceProvider,
187 encoding: Option<&str>,
188 ) -> crate::ResourceResult<SourceId> {
189 self.load_using(target, relative_to, |request| {
190 provider
191 .load_with_encoding(request, encoding)
192 .map(Into::into)
193 })
194 }
195
196 pub fn load_include(
200 &mut self,
201 target: &str,
202 relative_to: Option<SourceId>,
203 provider: &dyn crate::ResourceProvider,
204 encoding: Option<&str>,
205 line_limit: Option<usize>,
206 ) -> crate::ResourceResult<SourceId> {
207 self.load_using(target, relative_to, |request| {
208 provider
209 .load_include(request, encoding, line_limit)
210 .map(Into::into)
211 })
212 }
213
214 pub(crate) fn load_selected_include(
215 &mut self,
216 target: &str,
217 relative_to: Option<SourceId>,
218 provider: &dyn crate::ResourceProvider,
219 encoding: Option<&str>,
220 selection: &crate::include_selection::Selection,
221 ) -> crate::ResourceResult<(SourceId, Option<crate::include_selection::Selected>)> {
222 let mut prepared = None;
223 let id = self.load_using(target, relative_to, |request| {
224 match provider.prepare_include(request, encoding, selection.line_limit())? {
225 crate::IncludeInput::Decoded(resource) => Ok(resource.into()),
226 crate::IncludeInput::Deferred { resource, binary } => selection
227 .recover_bytes(resource, binary)
228 .map(|(snapshot, selected)| {
229 prepared = selected;
230 snapshot
231 }),
232 crate::IncludeInput::Unconverted { resource, encoding } => selection
233 .recover_unconverted(resource, encoding.as_deref())
234 .map(|(snapshot, selected)| {
235 prepared = selected;
236 snapshot
237 }),
238 }
239 })?;
240 if let Some(selected) = &mut prepared {
241 for diagnostic in &mut selected.diagnostics {
242 diagnostic.span.source_id = id;
243 }
244 }
245 Ok((id, prepared))
246 }
247
248 fn load_using(
249 &mut self,
250 target: &str,
251 relative_to: Option<SourceId>,
252 load: impl FnOnce(
253 crate::ResourceRequest<'_>,
254 ) -> crate::ResourceResult<crate::resource::IncludeSnapshot>,
255 ) -> crate::ResourceResult<SourceId> {
256 let referrer = relative_to
257 .map(|id| {
258 self.get(id).map(SourceFile::name).ok_or_else(|| {
259 crate::ResourceError::new(
260 crate::ResourceErrorKind::InvalidTarget,
261 target,
262 format!("unknown referring source id {}", id.0),
263 )
264 })
265 })
266 .transpose()?;
267 let crate::resource::IncludeSnapshot {
268 decoded: crate::IncludeResource { resource, complete },
269 omissions,
270 frames,
271 } = load(crate::ResourceRequest { target, referrer })?;
272 if let Some(index) = self.files.iter().position(|file| {
273 file.name == resource.name
274 && file.text == resource.text
275 && file.complete == complete
276 && file.omissions == omissions
277 && file.frames == frames
278 }) {
279 return Ok(SourceId(index));
280 }
281 Ok(self.add_snapshot(resource.name, resource.text, complete, omissions, frames))
282 }
283
284 pub fn slice(&self, span: Span) -> Option<&str> {
286 self.get(span.source_id)?.text.get(span.start..span.end)
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn physical_reader_frames_participate_in_identity_without_inserting_text() {
296 fn snapshot(
297 ranges: Vec<(usize, usize)>,
298 trailing_lf: bool,
299 ) -> crate::resource::IncludeSnapshot {
300 crate::resource::IncludeSnapshot {
301 decoded: crate::IncludeResource {
302 resource: crate::Resource {
303 name: "/frames.adoc".into(),
304 text: "a上b".into(),
305 },
306 complete: false,
307 },
308 omissions: Vec::new(),
309 frames: Some(crate::resource::SourceLineFrames {
310 ranges,
311 trailing_lf,
312 normalized: false,
313 }),
314 }
315 }
316 let mut sources = SourceMap::new();
317 let first = sources
318 .load_using("/frames.adoc", None, |_| {
319 Ok(snapshot(vec![(0, 4), (4, 5)], false))
320 })
321 .unwrap();
322 assert_eq!(
323 sources
324 .load_using("/frames.adoc", None, |_| Ok(snapshot(
325 vec![(0, 4), (4, 5)],
326 false
327 )))
328 .unwrap(),
329 first
330 );
331 let second = sources
332 .load_using("/frames.adoc", None, |_| {
333 Ok(snapshot(vec![(0, 1), (1, 5)], false))
334 })
335 .unwrap();
336 assert_ne!(first, second);
337 assert_eq!(sources.get(first).unwrap().text(), "a上b");
338 assert_eq!(sources.get(second).unwrap().text(), "a上b");
339 assert_eq!(
340 sources.get(first).unwrap().position(4),
341 Some(SourcePosition { line: 2, column: 1 })
342 );
343 assert_eq!(
344 sources.get(second).unwrap().position(4),
345 Some(SourcePosition { line: 2, column: 2 })
346 );
347 assert_eq!(sources.get(first).unwrap().position(2), None);
348 let lines = sources.get(first).unwrap().include_lines();
349 assert_eq!(lines.len(), 2);
350 assert_eq!(lines[0].text.as_ref(), "a上");
351 assert_eq!(lines[1].text.as_ref(), "b");
352 assert_eq!(sources.slice(Span::in_source(first, 0, 4)), Some("a上"));
353 let eof = sources
354 .load_using("/frames.adoc", None, |_| {
355 Ok(snapshot(vec![(0, 4), (4, 5)], true))
356 })
357 .unwrap();
358 assert_ne!(eof, first);
359 assert_eq!(
360 sources.get(eof).unwrap().position(5),
361 Some(SourcePosition { line: 3, column: 1 })
362 );
363 }
364
365 #[test]
366 fn unicode_crlf_and_end_of_file_positions_are_checked() {
367 let mut sources = SourceMap::new();
368 let id = sources.add("example.adoc", "Я🙂\r\nnext\n");
369 let file = sources.get(id).unwrap();
370 assert_eq!(
371 file.position(6),
372 Some(SourcePosition { line: 1, column: 3 })
373 );
374 assert_eq!(
375 file.position(8),
376 Some(SourcePosition { line: 2, column: 1 })
377 );
378 assert_eq!(
379 file.position(13),
380 Some(SourcePosition { line: 3, column: 1 })
381 );
382 assert_eq!(file.position(1), None);
383 assert_eq!(file.position(14), None);
384 assert_eq!(sources.slice(Span::in_source(id, 2, 6)), Some("🙂"));
385 assert_eq!(sources.slice(Span::in_source(id, 6, 2)), None);
386 assert_eq!(sources.slice(Span::in_source(SourceId(99), 0, 0)), None);
387 }
388}