use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub struct Span {
pub start: u32,
pub end: u32,
}
impl Span {
#[must_use]
pub const fn new(start: u32, end: u32) -> Self {
Self { start, end }
}
#[must_use]
pub const fn point(offset: u32) -> Self {
Self {
start: offset,
end: offset,
}
}
#[must_use]
pub const fn len(self) -> u32 {
self.end.saturating_sub(self.start)
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.len() == 0
}
#[must_use]
pub const fn union(self, other: Span) -> Span {
let start = if self.start < other.start {
self.start
} else {
other.start
};
let end = if self.end > other.end {
self.end
} else {
other.end
};
Span { start, end }
}
#[must_use]
pub fn slice<'a>(self, src: &'a str) -> &'a str {
let start = self.start as usize;
let end = self.end as usize;
if start >= src.len() {
""
} else {
let end = end.min(src.len());
&src[start..end]
}
}
#[must_use]
pub const fn contains(self, offset: u32) -> bool {
offset >= self.start && offset < self.end
}
}
impl fmt::Display for Span {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}..{}", self.start, self.end)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Position {
pub line: u32,
pub column: u32,
}
impl Position {
#[must_use]
pub const fn new(line: u32, column: u32) -> Self {
Self { line, column }
}
#[must_use]
pub const fn origin() -> Self {
Self::new(1, 1)
}
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.line, self.column)
}
}
#[must_use]
pub const fn line_column(src: &str, offset: u32) -> Position {
let mut line: u32 = 1;
let mut col: u32 = 1;
let offset = offset as usize;
let bytes = src.as_bytes();
let end = if offset < bytes.len() {
offset
} else {
bytes.len()
};
let mut i = 0;
while i < end {
let b = bytes[i];
if b == b'\n' {
line += 1;
col = 1;
} else if b < 0x80 || b >= 0xC0 {
col += 1;
}
i += 1;
}
Position::new(line, col)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn point_span_is_empty() {
let s = Span::point(5);
assert!(s.is_empty());
assert_eq!(s.len(), 0);
}
#[test]
fn union_widens() {
let a = Span::new(2, 5);
let b = Span::new(4, 9);
let u = a.union(b);
assert_eq!(u.start, 2);
assert_eq!(u.end, 9);
}
#[test]
fn union_is_const() {
const A: Span = Span::new(2, 5);
const B: Span = Span::new(4, 9);
const U: Span = A.union(B);
const _: () = assert!(U.start == 2);
const _: () = assert!(U.end == 9);
const _: () = assert!(U.contains(2));
const _: () = assert!(!U.contains(9));
}
#[test]
fn slice_extracts_substring() {
let src = "hello world";
assert_eq!(Span::new(6, 11).slice(src), "world");
}
#[test]
fn line_column_handles_newlines() {
let src = "abc\ndef\nghi";
assert_eq!(line_column(src, 0), Position::origin());
assert_eq!(line_column(src, 4), Position::new(2, 1));
assert_eq!(line_column(src, 9), Position::new(3, 2));
}
#[test]
fn contains_is_half_open() {
let s = Span::new(3, 7);
assert!(!s.contains(2));
assert!(s.contains(3));
assert!(s.contains(6));
assert!(!s.contains(7));
}
#[test]
fn len_and_is_empty_are_const() {
const RANGE: Span = Span::new(3, 7);
const POINT: Span = Span::point(5);
const RANGE_LEN: u32 = RANGE.len();
const POINT_LEN: u32 = POINT.len();
const RANGE_EMPTY: bool = RANGE.is_empty();
const POINT_EMPTY: bool = POINT.is_empty();
const _: () = assert!(RANGE_LEN == 4);
const _: () = assert!(POINT_LEN == 0);
const _: () = assert!(!RANGE_EMPTY);
const _: () = assert!(POINT_EMPTY);
const INVERTED: Span = Span::new(9, 2);
const INVERTED_LEN: u32 = INVERTED.len();
const INVERTED_EMPTY: bool = INVERTED.is_empty();
const _: () = assert!(INVERTED_LEN == 0);
const _: () = assert!(INVERTED_EMPTY);
}
#[test]
fn position_new_and_origin_are_const() {
const AT: Position = Position::new(2, 4);
const ORIGIN: Position = Position::origin();
const AT_LINE: u32 = AT.line;
const AT_COLUMN: u32 = AT.column;
const ORIGIN_LINE: u32 = ORIGIN.line;
const ORIGIN_COLUMN: u32 = ORIGIN.column;
const _: () = assert!(AT_LINE == 2);
const _: () = assert!(AT_COLUMN == 4);
const _: () = assert!(ORIGIN_LINE == 1);
const _: () = assert!(ORIGIN_COLUMN == 1);
}
#[test]
fn line_column_is_const_across_ascii_and_utf8() {
const ASCII: &str = "abc\ndef\nghi";
const ASCII_ORIGIN: Position = line_column(ASCII, 0);
const ASCII_ROW_2: Position = line_column(ASCII, 4);
const ASCII_ROW_3: Position = line_column(ASCII, 9);
const _: () = assert!(ASCII_ORIGIN.line == 1 && ASCII_ORIGIN.column == 1);
const _: () = assert!(ASCII_ROW_2.line == 2 && ASCII_ROW_2.column == 1);
const _: () = assert!(ASCII_ROW_3.line == 3 && ASCII_ROW_3.column == 2);
const ASCII_PAST_END: Position = line_column(ASCII, 100);
const _: () = assert!(ASCII_PAST_END.line == 3 && ASCII_PAST_END.column == 4);
const UTF8_TWO_BYTE: &str = "héllo";
const UTF8_TWO_BYTE_AFTER_H: Position = line_column(UTF8_TWO_BYTE, 1);
const UTF8_TWO_BYTE_MID_E_ACUTE: Position = line_column(UTF8_TWO_BYTE, 2);
const UTF8_TWO_BYTE_AFTER_E_ACUTE: Position = line_column(UTF8_TWO_BYTE, 3);
const UTF8_TWO_BYTE_END: Position = line_column(UTF8_TWO_BYTE, 100);
const _: () = assert!(UTF8_TWO_BYTE_AFTER_H.line == 1 && UTF8_TWO_BYTE_AFTER_H.column == 2);
const _: () =
assert!(UTF8_TWO_BYTE_MID_E_ACUTE.line == 1 && UTF8_TWO_BYTE_MID_E_ACUTE.column == 3);
const _: () = assert!(
UTF8_TWO_BYTE_AFTER_E_ACUTE.line == 1 && UTF8_TWO_BYTE_AFTER_E_ACUTE.column == 3
);
const _: () = assert!(UTF8_TWO_BYTE_END.line == 1 && UTF8_TWO_BYTE_END.column == 6);
const UTF8_FOUR_BYTE: &str = "😀 nice";
const UTF8_FOUR_BYTE_AFTER_EMOJI: Position = line_column(UTF8_FOUR_BYTE, 4);
const UTF8_FOUR_BYTE_AFTER_SPACE: Position = line_column(UTF8_FOUR_BYTE, 5);
const _: () =
assert!(UTF8_FOUR_BYTE_AFTER_EMOJI.line == 1 && UTF8_FOUR_BYTE_AFTER_EMOJI.column == 2);
const _: () =
assert!(UTF8_FOUR_BYTE_AFTER_SPACE.line == 1 && UTF8_FOUR_BYTE_AFTER_SPACE.column == 3);
const UTF8_ACROSS_NEWLINE: &str = "😀\n😀";
const UTF8_ACROSS_NEWLINE_ROW_2: Position = line_column(UTF8_ACROSS_NEWLINE, 5);
const _: () =
assert!(UTF8_ACROSS_NEWLINE_ROW_2.line == 2 && UTF8_ACROSS_NEWLINE_ROW_2.column == 1);
}
#[test]
fn contains_is_const() {
const SPAN: Span = Span::new(3, 7);
const _: () = assert!(!SPAN.contains(2));
const _: () = assert!(SPAN.contains(3));
const _: () = assert!(SPAN.contains(5));
const _: () = assert!(!SPAN.contains(7));
}
}