use crate::Span;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SourceId(pub usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourcePosition {
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone)]
pub struct SourceFile {
name: String,
text: String,
line_starts: Vec<usize>,
complete: bool,
omissions: Vec<crate::OmittedSourceLine>,
frames: Option<crate::resource::SourceLineFrames>,
}
impl SourceFile {
pub fn name(&self) -> &str {
&self.name
}
pub fn text(&self) -> &str {
&self.text
}
pub fn is_complete(&self) -> bool {
self.complete
}
pub fn omitted_lines(&self) -> &[crate::OmittedSourceLine] {
&self.omissions
}
pub fn omitted_slice(&self, span: crate::OmittedSourceSpan) -> Option<&[u8]> {
self.omissions
.iter()
.find(|line| line.line == span.line)?
.bytes
.get(span.start..span.end)
}
pub(crate) fn include_lines(&self) -> Vec<crate::include_indent::Line> {
crate::resource::framed_snapshot_lines(&self.text, &self.omissions, self.frames.as_ref())
}
pub(crate) fn has_line_frames(&self) -> bool {
self.frames.is_some()
}
pub(crate) fn bom_normalized(&self) -> bool {
self.frames.as_ref().is_some_and(|frames| frames.normalized)
}
pub(crate) fn stored_bytes(&self) -> usize {
self.omissions.iter().fold(self.text.len(), |size, line| {
size.saturating_add(line.bytes.len())
})
}
pub fn position(&self, offset: usize) -> Option<SourcePosition> {
self.text.get(..offset)?;
let line = self.line_starts.partition_point(|&start| start <= offset) - 1;
Some(SourcePosition {
line: line
+ 1
+ self
.omissions
.iter()
.filter(|omitted| omitted.offset <= offset && omitted.bytes.ends_with(b"\n"))
.count(),
column: self.text[self.line_starts[line]..offset].chars().count() + 1,
})
}
}
#[derive(Debug, Clone, Default)]
pub struct SourceMap {
files: Vec<SourceFile>,
}
impl SourceMap {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, name: impl Into<String>, text: impl Into<String>) -> SourceId {
self.add_snapshot(name.into(), text.into(), true, Vec::new(), None)
}
fn add_snapshot(
&mut self,
name: String,
text: String,
complete: bool,
omissions: Vec<crate::OmittedSourceLine>,
frames: Option<crate::resource::SourceLineFrames>,
) -> SourceId {
let line_starts = if let Some(frames) = &frames {
let mut starts: Vec<_> = frames.ranges.iter().map(|&(start, _)| start).collect();
if starts.is_empty() {
starts.push(0);
}
if frames.trailing_lf && starts.last() != Some(&text.len()) {
starts.push(text.len());
}
starts
} else {
let mut starts = vec![0];
starts.extend(text.match_indices('\n').map(|(index, _)| index + 1));
starts
};
let id = SourceId(self.files.len());
self.files.push(SourceFile {
name,
text,
line_starts,
complete,
omissions,
frames,
});
id
}
pub fn get(&self, id: SourceId) -> Option<&SourceFile> {
self.files.get(id.0)
}
pub fn load(
&mut self,
target: &str,
relative_to: Option<SourceId>,
provider: &dyn crate::ResourceProvider,
) -> crate::ResourceResult<SourceId> {
self.load_using(target, relative_to, |request| {
provider.load(request).map(Into::into)
})
}
pub fn load_document(
&mut self,
target: &str,
relative_to: Option<SourceId>,
provider: &dyn crate::ResourceProvider,
) -> crate::ResourceResult<SourceId> {
self.load_using(target, relative_to, |request| {
crate::resource::load_source_text(provider, request).map(Into::into)
})
}
pub fn load_with_encoding(
&mut self,
target: &str,
relative_to: Option<SourceId>,
provider: &dyn crate::ResourceProvider,
encoding: Option<&str>,
) -> crate::ResourceResult<SourceId> {
self.load_using(target, relative_to, |request| {
provider
.load_with_encoding(request, encoding)
.map(Into::into)
})
}
pub fn load_include(
&mut self,
target: &str,
relative_to: Option<SourceId>,
provider: &dyn crate::ResourceProvider,
encoding: Option<&str>,
line_limit: Option<usize>,
) -> crate::ResourceResult<SourceId> {
self.load_using(target, relative_to, |request| {
provider
.load_include(request, encoding, line_limit)
.map(Into::into)
})
}
pub(crate) fn load_selected_include(
&mut self,
target: &str,
relative_to: Option<SourceId>,
provider: &dyn crate::ResourceProvider,
encoding: Option<&str>,
selection: &crate::include_selection::Selection,
) -> crate::ResourceResult<(SourceId, Option<crate::include_selection::Selected>)> {
let mut prepared = None;
let id = self.load_using(target, relative_to, |request| {
match provider.prepare_include(request, encoding, selection.line_limit())? {
crate::IncludeInput::Decoded(resource) => Ok(resource.into()),
crate::IncludeInput::Deferred { resource, binary } => selection
.recover_bytes(resource, binary)
.map(|(snapshot, selected)| {
prepared = selected;
snapshot
}),
crate::IncludeInput::Unconverted { resource, encoding } => selection
.recover_unconverted(resource, encoding.as_deref())
.map(|(snapshot, selected)| {
prepared = selected;
snapshot
}),
}
})?;
if let Some(selected) = &mut prepared {
for diagnostic in &mut selected.diagnostics {
diagnostic.span.source_id = id;
}
}
Ok((id, prepared))
}
fn load_using(
&mut self,
target: &str,
relative_to: Option<SourceId>,
load: impl FnOnce(
crate::ResourceRequest<'_>,
) -> crate::ResourceResult<crate::resource::IncludeSnapshot>,
) -> crate::ResourceResult<SourceId> {
let referrer = relative_to
.map(|id| {
self.get(id).map(SourceFile::name).ok_or_else(|| {
crate::ResourceError::new(
crate::ResourceErrorKind::InvalidTarget,
target,
format!("unknown referring source id {}", id.0),
)
})
})
.transpose()?;
let crate::resource::IncludeSnapshot {
decoded: crate::IncludeResource { resource, complete },
omissions,
frames,
} = load(crate::ResourceRequest { target, referrer })?;
if let Some(index) = self.files.iter().position(|file| {
file.name == resource.name
&& file.text == resource.text
&& file.complete == complete
&& file.omissions == omissions
&& file.frames == frames
}) {
return Ok(SourceId(index));
}
Ok(self.add_snapshot(resource.name, resource.text, complete, omissions, frames))
}
pub fn slice(&self, span: Span) -> Option<&str> {
self.get(span.source_id)?.text.get(span.start..span.end)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn physical_reader_frames_participate_in_identity_without_inserting_text() {
fn snapshot(
ranges: Vec<(usize, usize)>,
trailing_lf: bool,
) -> crate::resource::IncludeSnapshot {
crate::resource::IncludeSnapshot {
decoded: crate::IncludeResource {
resource: crate::Resource {
name: "/frames.adoc".into(),
text: "a上b".into(),
},
complete: false,
},
omissions: Vec::new(),
frames: Some(crate::resource::SourceLineFrames {
ranges,
trailing_lf,
normalized: false,
}),
}
}
let mut sources = SourceMap::new();
let first = sources
.load_using("/frames.adoc", None, |_| {
Ok(snapshot(vec![(0, 4), (4, 5)], false))
})
.unwrap();
assert_eq!(
sources
.load_using("/frames.adoc", None, |_| Ok(snapshot(
vec![(0, 4), (4, 5)],
false
)))
.unwrap(),
first
);
let second = sources
.load_using("/frames.adoc", None, |_| {
Ok(snapshot(vec![(0, 1), (1, 5)], false))
})
.unwrap();
assert_ne!(first, second);
assert_eq!(sources.get(first).unwrap().text(), "a上b");
assert_eq!(sources.get(second).unwrap().text(), "a上b");
assert_eq!(
sources.get(first).unwrap().position(4),
Some(SourcePosition { line: 2, column: 1 })
);
assert_eq!(
sources.get(second).unwrap().position(4),
Some(SourcePosition { line: 2, column: 2 })
);
assert_eq!(sources.get(first).unwrap().position(2), None);
let lines = sources.get(first).unwrap().include_lines();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].text.as_ref(), "a上");
assert_eq!(lines[1].text.as_ref(), "b");
assert_eq!(sources.slice(Span::in_source(first, 0, 4)), Some("a上"));
let eof = sources
.load_using("/frames.adoc", None, |_| {
Ok(snapshot(vec![(0, 4), (4, 5)], true))
})
.unwrap();
assert_ne!(eof, first);
assert_eq!(
sources.get(eof).unwrap().position(5),
Some(SourcePosition { line: 3, column: 1 })
);
}
#[test]
fn unicode_crlf_and_end_of_file_positions_are_checked() {
let mut sources = SourceMap::new();
let id = sources.add("example.adoc", "Я🙂\r\nnext\n");
let file = sources.get(id).unwrap();
assert_eq!(
file.position(6),
Some(SourcePosition { line: 1, column: 3 })
);
assert_eq!(
file.position(8),
Some(SourcePosition { line: 2, column: 1 })
);
assert_eq!(
file.position(13),
Some(SourcePosition { line: 3, column: 1 })
);
assert_eq!(file.position(1), None);
assert_eq!(file.position(14), None);
assert_eq!(sources.slice(Span::in_source(id, 2, 6)), Some("🙂"));
assert_eq!(sources.slice(Span::in_source(id, 6, 2)), None);
assert_eq!(sources.slice(Span::in_source(SourceId(99), 0, 0)), None);
}
}