#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Position {
pub line: usize,
pub column: usize,
pub offset: usize,
}
impl Position {
pub fn new() -> Self {
Self {
line: 1,
column: 1,
offset: 0,
}
}
pub fn advance(&mut self, ch: char) {
self.offset += ch.len_utf8();
if ch == '\n' {
self.line += 1;
self.column = 1;
} else {
self.column += 1;
}
}
pub fn advanced_by(mut self, text: &str) -> Self {
for ch in text.chars() {
self.advance(ch);
}
self
}
pub fn rebased(self, base: Position) -> Self {
Self {
line: base.line + self.line.saturating_sub(1),
column: if self.line <= 1 {
base.column + self.column.saturating_sub(1)
} else {
self.column
},
offset: base.offset + self.offset,
}
}
}
impl std::fmt::Display for Position {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.line, self.column)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Span {
pub start: Position,
pub end: Position,
}
impl Span {
pub fn new() -> Self {
Self::default()
}
pub fn from_positions(start: Position, end: Position) -> Self {
Self { start, end }
}
pub fn at(pos: Position) -> Self {
Self {
start: pos,
end: pos,
}
}
pub fn merge(self, other: Span) -> Self {
let start = if self.start.offset <= other.start.offset {
self.start
} else {
other.start
};
let end = if self.end.offset >= other.end.offset {
self.end
} else {
other.end
};
Self { start, end }
}
pub fn rebased(self, base: Position) -> Self {
Self {
start: self.start.rebased(base),
end: self.end.rebased(base),
}
}
pub fn slice<'a>(&self, source: &'a str) -> &'a str {
slice_with_byte_offsets(source, self.start.offset, self.end.offset)
}
pub fn to_range(self) -> TextRange {
TextRange::new(
TextSize::new(self.start.offset as u32),
TextSize::new(self.end.offset as u32),
)
}
pub fn line(&self) -> usize {
self.start.line
}
}
impl std::fmt::Display for Span {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.start.line == self.end.line {
write!(f, "line {}", self.start.line)
} else {
write!(f, "lines {}-{}", self.start.line, self.end.line)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct TextSize(u32);
impl TextSize {
pub const fn new(raw: u32) -> Self {
Self(raw)
}
pub const fn to_u32(self) -> u32 {
self.0
}
}
impl From<u32> for TextSize {
fn from(raw: u32) -> Self {
Self(raw)
}
}
impl From<TextSize> for usize {
fn from(size: TextSize) -> Self {
size.0 as usize
}
}
impl std::ops::Add for TextSize {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self(self.0 + rhs.0)
}
}
impl std::ops::Sub for TextSize {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self(self.0 - rhs.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct TextRange {
start: TextSize,
end: TextSize,
}
impl TextRange {
pub const fn new(start: TextSize, end: TextSize) -> Self {
Self { start, end }
}
pub const fn start(self) -> TextSize {
self.start
}
pub const fn end(self) -> TextSize {
self.end
}
pub const fn len(self) -> TextSize {
TextSize(self.end.0 - self.start.0)
}
pub const fn is_empty(self) -> bool {
self.start.0 == self.end.0
}
pub fn slice<'a>(&self, source: &'a str) -> &'a str {
slice_with_byte_offsets(source, usize::from(self.start), usize::from(self.end))
}
pub fn offset_by(self, base: TextSize) -> Self {
Self {
start: self.start + base,
end: self.end + base,
}
}
}
fn slice_with_byte_offsets(source: &str, start: usize, end: usize) -> &str {
if start > end || end > source.len() {
return "";
}
if let Some(slice) = source.get(start..end) {
return slice;
}
let start = floor_char_boundary(source, start);
let end = ceil_char_boundary(source, end);
source.get(start..end).unwrap_or("")
}
fn floor_char_boundary(source: &str, offset: usize) -> usize {
let mut offset = offset.min(source.len());
while offset > 0 && !source.is_char_boundary(offset) {
offset -= 1;
}
offset
}
fn ceil_char_boundary(source: &str, offset: usize) -> usize {
let mut offset = offset.min(source.len());
while offset < source.len() && !source.is_char_boundary(offset) {
offset += 1;
}
offset
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_position_advance() {
let mut pos = Position::new();
assert_eq!(pos.line, 1);
assert_eq!(pos.column, 1);
assert_eq!(pos.offset, 0);
pos.advance('a');
assert_eq!(pos.line, 1);
assert_eq!(pos.column, 2);
assert_eq!(pos.offset, 1);
pos.advance('\n');
assert_eq!(pos.line, 2);
assert_eq!(pos.column, 1);
assert_eq!(pos.offset, 2);
pos.advance('b');
assert_eq!(pos.line, 2);
assert_eq!(pos.column, 2);
assert_eq!(pos.offset, 3);
}
#[test]
fn test_position_display() {
let pos = Position {
line: 5,
column: 10,
offset: 50,
};
assert_eq!(format!("{}", pos), "5:10");
}
#[test]
fn test_span_merge() {
let span1 = Span {
start: Position {
line: 1,
column: 1,
offset: 0,
},
end: Position {
line: 1,
column: 5,
offset: 4,
},
};
let span2 = Span {
start: Position {
line: 1,
column: 10,
offset: 9,
},
end: Position {
line: 2,
column: 3,
offset: 15,
},
};
let merged = span1.merge(span2);
assert_eq!(merged.start.offset, 0);
assert_eq!(merged.end.offset, 15);
}
#[test]
fn test_span_display() {
let single_line = Span {
start: Position {
line: 3,
column: 1,
offset: 0,
},
end: Position {
line: 3,
column: 10,
offset: 9,
},
};
assert_eq!(format!("{}", single_line), "line 3");
let multi_line = Span {
start: Position {
line: 1,
column: 1,
offset: 0,
},
end: Position {
line: 5,
column: 1,
offset: 50,
},
};
assert_eq!(format!("{}", multi_line), "lines 1-5");
}
#[test]
fn span_slice_handles_non_char_boundaries() {
let source = "a─b";
let span = Span::from_positions(
Position {
line: 1,
column: 2,
offset: 1,
},
Position {
line: 1,
column: 3,
offset: 3,
},
);
assert_eq!(span.slice(source), "─");
}
#[test]
fn text_range_slice_handles_non_char_boundaries() {
let source = "x🔉y";
let range = TextRange::new(TextSize::new(1), TextSize::new(4));
assert_eq!(range.slice(source), "🔉");
}
}