use std::sync::Arc;
use serde::{
Serialize,
ser::{SerializeMap, SerializeSeq, Serializer},
};
#[derive(Debug, Clone, Default, PartialEq)]
pub(crate) struct LeveloffsetRange {
pub(crate) start_offset: usize,
pub(crate) end_offset: usize,
pub(crate) value: isize,
}
impl LeveloffsetRange {
#[must_use]
pub(crate) fn new(start_offset: usize, end_offset: usize, value: isize) -> Self {
Self {
start_offset,
end_offset,
value,
}
}
#[must_use]
pub(crate) fn contains(&self, byte_offset: usize) -> bool {
byte_offset >= self.start_offset && byte_offset < self.end_offset
}
}
#[must_use]
pub(crate) fn calculate_leveloffset_at(ranges: &[LeveloffsetRange], byte_offset: usize) -> isize {
ranges
.iter()
.filter_map(|r| {
if r.contains(byte_offset) {
Some(r.value)
} else {
None
}
})
.sum()
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct SourceRange {
pub(crate) start_offset: usize,
pub(crate) end_offset: usize,
pub(crate) file: Option<std::path::PathBuf>,
pub(crate) file_chain: Vec<String>,
pub(crate) start_line: usize,
pub(crate) source_start_offset: usize,
pub(crate) column_shift: isize,
}
impl SourceRange {
#[must_use]
pub(crate) fn contains(&self, byte_offset: usize) -> bool {
byte_offset >= self.start_offset && byte_offset < self.end_offset
}
#[must_use]
pub(crate) fn find_containing(ranges: &[SourceRange], offset: usize) -> Option<&SourceRange> {
ranges.iter().rev().find(|r| r.contains(offset))
}
#[must_use]
pub(crate) fn source_offset(&self, offset: usize) -> usize {
self.source_start_offset + offset.saturating_sub(self.start_offset)
}
}
#[derive(Debug, Default, Clone, Hash, Eq, PartialEq)]
#[non_exhaustive]
pub struct Location {
pub absolute_start: usize,
pub absolute_end: usize,
pub start: Position,
pub end: Position,
}
impl Location {
#[must_use]
pub fn point(position: Position) -> Self {
Self {
absolute_start: 0,
absolute_end: 0,
start: position.clone(),
end: position,
}
}
#[must_use]
pub fn byte_len(&self) -> Option<usize> {
(self.start.file == self.end.file).then(|| {
self.absolute_end
.saturating_sub(self.absolute_start)
.saturating_add(1)
})
}
pub fn validate(&self, input: &str) -> Result<(), String> {
if self.absolute_start > self.absolute_end {
return Err(format!(
"Invalid range: start {} > end {}",
self.absolute_start, self.absolute_end
));
}
if self.absolute_end > input.len() {
return Err(format!(
"End offset {} exceeds input length {}",
self.absolute_end,
input.len()
));
}
if !input.is_char_boundary(self.absolute_start) {
return Err(format!(
"Start offset {} not on UTF-8 boundary",
self.absolute_start
));
}
if !input.is_char_boundary(self.absolute_end) {
return Err(format!(
"End offset {} not on UTF-8 boundary",
self.absolute_end
));
}
Ok(())
}
}
impl Serialize for Location {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_seq(Some(2))?;
state.serialize_element(&self.start)?;
state.serialize_element(&self.end)?;
state.end()
}
}
impl std::fmt::Display for Location {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"location.start({}), location.end({})",
self.start, self.end
)
}
}
#[derive(Debug, Default, Clone, Hash, Eq, PartialEq)]
#[non_exhaustive]
pub struct Position {
pub line: u32,
pub column: u32,
pub file: Option<Arc<Vec<String>>>,
}
impl Position {
#[must_use]
pub fn new(line: u32, column: u32) -> Self {
Self {
line,
column,
file: None,
}
}
#[must_use]
pub fn from_line_col(line: usize, column: usize) -> Self {
Self {
line: u32::try_from(line).unwrap_or(u32::MAX),
column: u32::try_from(column).unwrap_or(u32::MAX),
file: None,
}
}
}
impl std::fmt::Display for Position {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "line: {}, column: {}", self.line, self.column)
}
}
impl Serialize for Position {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let chain = self.file.as_deref().filter(|c| !c.is_empty());
let len = if chain.is_some() { 3 } else { 2 };
let mut map = serializer.serialize_map(Some(len))?;
map.serialize_entry("line", &self.line)?;
map.serialize_entry("col", &self.column)?;
if let Some(chain) = chain {
map.serialize_entry("file", chain)?;
}
map.end()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn loc(chain: &[&str]) -> Location {
let file = (!chain.is_empty())
.then(|| Arc::new(chain.iter().map(|s| (*s).to_string()).collect::<Vec<_>>()));
Location {
absolute_start: 0,
absolute_end: 1,
start: Position {
line: 1,
column: 2,
file: file.clone(),
},
end: Position {
line: 3,
column: 4,
file,
},
}
}
#[test]
fn serializes_two_boundaries_without_file_when_primary() -> Result<(), serde_json::Error> {
let json = serde_json::to_value(loc(&[]))?;
assert_eq!(
json,
serde_json::json!([{ "line": 1, "col": 2 }, { "line": 3, "col": 4 }])
);
Ok(())
}
#[test]
fn serializes_file_as_include_chain_on_each_boundary() -> Result<(), serde_json::Error> {
let json = serde_json::to_value(loc(&["a.adoc", "chapters/b.adoc"]))?;
assert_eq!(
json,
serde_json::json!([
{ "line": 1, "col": 2, "file": ["a.adoc", "chapters/b.adoc"] },
{ "line": 3, "col": 4, "file": ["a.adoc", "chapters/b.adoc"] }
])
);
Ok(())
}
#[test]
fn position_equality_is_provenance_aware() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
fn hash(p: &Position) -> u64 {
let mut h = DefaultHasher::new();
p.hash(&mut h);
h.finish()
}
let pos = |chain: &[&str]| {
let mut p = Position::new(1, 1);
if !chain.is_empty() {
p.file = Some(Arc::new(
chain.iter().map(|s| (*s).to_string()).collect::<Vec<_>>(),
));
}
p
};
let primary = pos(&[]);
let included = pos(&["inc.adoc"]);
assert_ne!(primary, included, "primary input vs included differ");
assert_ne!(
included,
pos(&["other.adoc"]),
"different include chains differ"
);
assert_ne!(hash(&primary), hash(&included));
assert_eq!(included, pos(&["inc.adoc"]));
assert_eq!(hash(&included), hash(&pos(&["inc.adoc"])));
}
#[test]
fn byte_len_only_within_a_single_file() {
let chain = |c: &[&str]| Arc::new(c.iter().map(|s| (*s).to_string()).collect::<Vec<_>>());
let same = Location {
absolute_start: 10,
absolute_end: 19,
start: Position::new(1, 1),
end: Position::new(1, 10),
};
assert_eq!(same.byte_len(), Some(10));
let mut start = Position::new(1, 1);
start.file = Some(chain(&["a.adoc"]));
let mut end = Position::new(5, 1);
end.file = Some(chain(&["b.adoc"]));
let cross = Location {
absolute_start: 10,
absolute_end: 400,
start,
end,
};
assert_eq!(cross.byte_len(), None);
}
}