Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
authors = ["Warp Team <dev@warp.dev>"]
|
||||
edition = "2021"
|
||||
name = "string-offset"
|
||||
publish.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
get-size.workspace = true
|
||||
num-traits.workspace = true
|
||||
serde.workspace = true
|
||||
@@ -0,0 +1,215 @@
|
||||
//! 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;
|
||||
@@ -0,0 +1,51 @@
|
||||
use crate::CharCounter;
|
||||
|
||||
#[test]
|
||||
fn test_char_counter_sequential() {
|
||||
let mut counter = CharCounter::new("It’s a 🌯!");
|
||||
assert_eq!(counter.char_offset(0), Some(0.into())); // I
|
||||
assert_eq!(counter.char_offset(1), Some(1.into())); // t
|
||||
assert_eq!(counter.char_offset(2), Some(2.into())); // ’ (3 bytes)
|
||||
assert_eq!(counter.char_offset(5), Some(3.into())); // s
|
||||
assert_eq!(counter.char_offset(6), Some(4.into())); // <space>
|
||||
assert_eq!(counter.char_offset(7), Some(5.into())); // a
|
||||
assert_eq!(counter.char_offset(8), Some(6.into())); // <space>
|
||||
assert_eq!(counter.char_offset(9), Some(7.into())); // 🌯 (4 bytes)
|
||||
assert_eq!(counter.char_offset(13), Some(8.into())); // !
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_char_counter_out_of_order_fails() {
|
||||
let mut counter = CharCounter::new("words");
|
||||
assert_eq!(counter.char_offset(2), Some(2.into()));
|
||||
// Because the iterator only advances forwards, using an earlier offset is unsupported.
|
||||
assert_eq!(counter.char_offset(1), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_char_counter_non_boundary() {
|
||||
let mut counter = CharCounter::new("It’s a 🌯!");
|
||||
// Byte 11 is in the middle of the emoji.
|
||||
assert_eq!(counter.char_offset(11), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_char_counter_out_of_bounds() {
|
||||
let mut counter = CharCounter::new("short string");
|
||||
assert_eq!(counter.char_offset(7), Some(7.into()));
|
||||
assert_eq!(counter.char_offset(100), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_char_counter_nonsequential() {
|
||||
let mut counter = CharCounter::new("It’s a 🌯!");
|
||||
|
||||
// Skip ahead to the t.
|
||||
assert_eq!(counter.char_offset(1), Some(1.into()));
|
||||
|
||||
// Now, skip past the multi-byte apostrophe to the first space.
|
||||
assert_eq!(counter.char_offset(6), Some(4.into()));
|
||||
|
||||
// Skip ahead to the emoji.
|
||||
assert_eq!(counter.char_offset(9), Some(7.into()));
|
||||
}
|
||||
Reference in New Issue
Block a user