use std::fmt;
use std::num::NonZeroUsize;
use text_size::TextSize;
macro_rules! define_zero_based_coordinate {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct $name(usize);
impl $name {
#[must_use]
pub const fn new(value: usize) -> Self {
Self(value)
}
}
impl Coordinate for $name {
fn get(self) -> usize {
self.0
}
}
impl fmt::Debug for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({})", stringify!($name), self.0)
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
};
}
macro_rules! define_one_based_coordinate {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct $name(NonZeroUsize);
impl $name {
#[must_use]
pub const fn try_new(value: usize) -> Option<Self> {
match NonZeroUsize::new(value) {
Some(v) => Some(Self(v)),
None => None,
}
}
#[must_use]
pub const fn new(value: usize) -> Self {
match NonZeroUsize::new(value) {
Some(v) => Self(v),
None => panic!("1-based coordinates must be non-zero"),
}
}
}
impl Coordinate for $name {
fn get(self) -> usize {
self.0.get()
}
}
impl fmt::Debug for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({})", stringify!($name), self.0)
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
};
}
pub trait Coordinate {
fn get(self) -> usize;
}
define_one_based_coordinate!(
LineNumber
);
define_one_based_coordinate!(
ByteColumn
);
define_one_based_coordinate!(
CharColumn
);
define_zero_based_coordinate!(
ByteOffset
);
define_zero_based_coordinate!(
Utf16Offset
);
impl From<TextSize> for ByteOffset {
fn from(offset: TextSize) -> Self {
Self::new(usize::from(offset))
}
}
impl TryFrom<ByteOffset> for TextSize {
type Error = std::num::TryFromIntError;
fn try_from(offset: ByteOffset) -> Result<Self, Self::Error> {
Self::try_from(offset.get())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ByteLineCol {
pub line: LineNumber,
pub column: ByteColumn,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CharLineCol {
pub line: LineNumber,
pub column: CharColumn,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn assert_distinct_coordinate_types() {
let line = LineNumber::new(1);
let byte_col = ByteColumn::new(4);
let char_col = CharColumn::new(4);
let byte = ByteOffset::new(10);
let utf16 = Utf16Offset::new(10);
assert_eq!(line.get(), 1);
assert_eq!(byte_col.get(), char_col.get());
assert_ne!(byte.get(), line.get());
let _: ByteColumn = byte_col;
let _: CharColumn = char_col;
let _: ByteOffset = byte;
let _: Utf16Offset = utf16;
}
#[test]
fn coordinate_newtypes_are_not_interchangeable() {
assert_distinct_coordinate_types();
}
#[test]
fn one_based_coordinates_reject_zero() {
assert!(LineNumber::try_new(0).is_none());
assert!(ByteColumn::try_new(0).is_none());
assert!(CharColumn::try_new(0).is_none());
}
#[test]
fn one_based_coordinates_accept_one() {
assert_eq!(LineNumber::try_new(1), Some(LineNumber::new(1)));
assert_eq!(ByteColumn::try_new(1), Some(ByteColumn::new(1)));
assert_eq!(CharColumn::try_new(1), Some(CharColumn::new(1)));
}
#[test]
fn byte_offset_converts_to_text_size() {
let offset = ByteOffset::new(5);
assert_eq!(TextSize::try_from(offset).unwrap(), TextSize::from(5));
assert_eq!(ByteOffset::from(TextSize::from(5)), ByteOffset::new(5));
}
}