use alloc::vec::Vec;
use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ColumnEncoding {
Utf8,
Scalar,
Utf16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Position {
pub line: usize,
pub column: usize,
}
impl Position {
pub fn one_based(self) -> Result<Self, PositionError> {
Ok(Self {
line: self.line.checked_add(1).ok_or(PositionError::OutOfBounds)?,
column: self
.column
.checked_add(1)
.ok_or(PositionError::OutOfBounds)?,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PositionError {
OutOfBounds,
InvalidUtf8Boundary,
SplitSurrogate,
InsideCrLf,
}
impl fmt::Display for PositionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::OutOfBounds => "source position is out of bounds",
Self::InvalidUtf8Boundary => "source offset is not a UTF-8 character boundary",
Self::SplitSurrogate => "UTF-16 column splits a surrogate pair",
Self::InsideCrLf => "source offset lies inside a CRLF line ending",
})
}
}
impl core::error::Error for PositionError {}
#[derive(Debug, Clone, Copy)]
struct Line {
start: usize,
end: usize,
}
#[derive(Debug, Clone)]
pub struct LineIndex<'src> {
source: &'src str,
lines: Vec<Line>,
}
impl<'src> LineIndex<'src> {
#[must_use]
pub fn new(source: &'src str) -> Self {
let bytes = source.as_bytes();
let mut lines = Vec::new();
let mut start = 0;
let mut offset = 0;
while offset < bytes.len() {
match bytes[offset] {
b'\r' | b'\n' => {
lines.push(Line { start, end: offset });
if bytes[offset] == b'\r' && bytes.get(offset + 1) == Some(&b'\n') {
offset += 2;
} else {
offset += 1;
}
start = offset;
}
_ => offset += 1,
}
}
lines.push(Line {
start,
end: source.len(),
});
Self { source, lines }
}
#[must_use]
pub fn line_count(&self) -> usize {
self.lines.len()
}
pub fn to_position(
&self,
offset: usize,
encoding: ColumnEncoding,
) -> Result<Position, PositionError> {
if offset > self.source.len() {
return Err(PositionError::OutOfBounds);
}
let line = self.lines.partition_point(|line| line.start <= offset) - 1;
let bounds = self.lines[line];
if offset > bounds.end {
return Err(PositionError::InsideCrLf);
}
let column = match encoding {
ColumnEncoding::Utf8 => offset - bounds.start,
ColumnEncoding::Scalar | ColumnEncoding::Utf16 => {
if !self.source.is_char_boundary(offset) {
return Err(PositionError::InvalidUtf8Boundary);
}
let prefix = &self.source[bounds.start..offset];
match encoding {
ColumnEncoding::Scalar => prefix.chars().count(),
ColumnEncoding::Utf16 => prefix.chars().map(char::len_utf16).sum(),
ColumnEncoding::Utf8 => unreachable!(),
}
}
};
Ok(Position { line, column })
}
pub fn to_offset(
&self,
position: Position,
encoding: ColumnEncoding,
) -> Result<usize, PositionError> {
let line = self
.lines
.get(position.line)
.ok_or(PositionError::OutOfBounds)?;
let content = &self.source[line.start..line.end];
if position.column > content.len() {
return Err(PositionError::OutOfBounds);
}
if encoding == ColumnEncoding::Utf8 {
return Ok(line.start + position.column);
}
let mut column = 0;
for (offset, ch) in content.char_indices() {
if column == position.column {
return Ok(line.start + offset);
}
column += match encoding {
ColumnEncoding::Scalar => 1,
ColumnEncoding::Utf16 => ch.len_utf16(),
ColumnEncoding::Utf8 => unreachable!(),
};
if column > position.column {
return Err(PositionError::SplitSurrogate);
}
}
if column == position.column {
Ok(line.end)
} else {
Err(PositionError::OutOfBounds)
}
}
}