Files
galaxy/crates/string-offset/src/lib.rs
T

216 lines
5.4 KiB
Rust

//! Base types for representing offset-based text locations.
use std::ops::{Add, AddAssign, Range, Sub, SubAssign};
use get_size::GetSize;
use serde::{Deserialize, Serialize};
/// An offset within a piece of text, in terms of characters (for Rust's definition
/// of `char`).
#[derive(
Clone,
Copy,
Debug,
Default,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
Serialize,
Deserialize,
GetSize,
)]
pub struct CharOffset(usize);
/// An offset within a piece of text, in terms of bytes.
#[derive(
Clone,
Copy,
Debug,
Default,
Hash,
Eq,
Ord,
PartialEq,
PartialOrd,
GetSize,
Serialize,
Deserialize,
)]
pub struct ByteOffset(usize);
#[macro_export]
macro_rules! impl_offset {
($typ:ty) => {
impl $typ {
pub const fn zero() -> Self {
Self(0)
}
pub fn as_usize(self) -> usize {
self.0
}
/// Returns a zero-length range at this offset.
pub fn empty_range(self) -> Range<Self> {
self..self
}
/// Add a signed integer amount to this offset. In debug builds, this will panic on
/// overflow.
pub fn add_signed(self, rhs: isize) -> Self {
let (inner, overflow) = self.0.overflowing_add_signed(rhs);
debug_assert!(!overflow, "arithmetic overflow");
Self(inner)
}
pub fn range(range: Range<usize>) -> Range<Self> {
range.start.into()..range.end.into()
}
}
impl From<usize> for $typ {
fn from(value: usize) -> Self {
Self(value)
}
}
impl AddAssign for $typ {
fn add_assign(&mut self, rhs: Self) {
*self += rhs.0
}
}
impl AddAssign<usize> for $typ {
fn add_assign(&mut self, rhs: usize) {
self.0 += rhs
}
}
impl SubAssign for $typ {
fn sub_assign(&mut self, rhs: Self) {
*self -= rhs.0
}
}
impl SubAssign<usize> for $typ {
fn sub_assign(&mut self, rhs: usize) {
self.0 -= rhs
}
}
impl Add for $typ {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl Add<usize> for $typ {
type Output = Self;
fn add(self, rhs: usize) -> Self::Output {
Self(self.0 + rhs)
}
}
impl Sub for $typ {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self(self.0 - rhs.0)
}
}
impl Sub<usize> for $typ {
type Output = Self;
fn sub(self, rhs: usize) -> Self::Output {
Self(self.0 - rhs)
}
}
impl num_traits::SaturatingSub for $typ {
#[inline]
fn saturating_sub(&self, rhs: &Self) -> Self {
Self(self.0.saturating_sub(rhs.0))
}
}
impl std::fmt::Display for $typ {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.0.fmt(f)
}
}
};
}
impl_offset!(CharOffset);
impl_offset!(ByteOffset);
impl AddAssign<i32> for CharOffset {
fn add_assign(&mut self, rhs: i32) {
if rhs.is_positive() {
self.0 += rhs as usize;
} else {
self.0 -= rhs.unsigned_abs() as usize;
}
}
}
/// A utility for counting characters while iterating through a string from left to right.
///
/// # Example
///
/// ```
/// # use string_offset::CharCounter;
/// let text = "abc🔥abc☄️abc😬";
/// let mut counter = CharCounter::new(text);
/// let matches: Vec<_> = text.match_indices("abc")
/// .map(|(byte_start, _)| (byte_start, counter.char_offset(byte_start).unwrap().as_usize()))
/// .collect();
/// assert_eq!(matches, vec![(0, 0), (7, 4), (16, 9)]);
/// ```
pub struct CharCounter<'a> {
current_offset: CharOffset,
char_indices: std::str::CharIndices<'a>,
}
impl<'a> CharCounter<'a> {
pub fn new(str: &'a str) -> Self {
Self {
char_indices: str.char_indices(),
current_offset: CharOffset::zero(),
}
}
/// Get the character offset that corresponds to `byte_offset`. That is, if the `nth` item of
/// [`str::char_indices`] had `byte_offset` as its position, this would return `n`.
///
/// This returns `None` if:
/// * `byte_offset` is past the length of the backing string
/// * `byte_offset` is not the start of a character
/// * The counter has already advanced past `byte_offset`
pub fn char_offset(&mut self, byte_offset: impl Into<ByteOffset>) -> Option<CharOffset> {
let byte_offset = byte_offset.into();
if self.char_indices.offset() > byte_offset.as_usize() {
None
} else {
for (next_byte_offset, _) in self.char_indices.by_ref() {
self.current_offset += 1;
if next_byte_offset == byte_offset.as_usize() {
return Some(self.current_offset - 1);
}
}
None
}
}
}
#[cfg(test)]
#[path = "lib_tests.rs"]
mod tests;