Unnamed repository; edit this file 'description' to name the repository.
Merge pull request rust-analyzer/text-size#2 from CAD97/text-size
Large refactor: text-size almost-1.0 crate
| -rw-r--r-- | lib/text-size/Cargo.toml | 22 | ||||
| -rw-r--r-- | lib/text-size/src/lib.rs | 469 | ||||
| -rw-r--r-- | lib/text-size/src/range.rs | 198 | ||||
| -rw-r--r-- | lib/text-size/src/serde_impls.rs | 40 | ||||
| -rw-r--r-- | lib/text-size/src/size.rs | 185 | ||||
| -rw-r--r-- | lib/text-size/src/traits.rs | 35 | ||||
| -rw-r--r-- | lib/text-size/tests/main.rs | 73 | ||||
| -rw-r--r-- | lib/text-size/tests/serde.rs | 57 |
8 files changed, 613 insertions, 466 deletions
diff --git a/lib/text-size/Cargo.toml b/lib/text-size/Cargo.toml index 9080b3fad8..95c11472cd 100644 --- a/lib/text-size/Cargo.toml +++ b/lib/text-size/Cargo.toml @@ -1,12 +1,24 @@ [package] -name = "text_unit" -version = "0.1.10" -authors = ["Aleksey Kladov <[email protected]>"] +name = "text-size" +version = "0.99.0-dev.2" +edition = "2018" + +authors = [ + "Aleksey Kladov <[email protected]>", + "Christopher Durham (CAD97) <[email protected]>" +] description = "Newtypes for text offsets" license = "MIT OR Apache-2.0" repository = "https://github.com/matklad/text_unit" documentation = "https://docs.rs/text_unit" [dependencies] -serde = { version = "1", optional = true, default_features = false } -deepsize = { version = "0.1", optional = true, default_features = false } +serde = { version = "1.0", optional = true, default_features = false } + +[dev-dependencies] +serde_test = "1.0" + +[[test]] +name = "serde" +path = "tests/serde.rs" +required-features = ["serde"] diff --git a/lib/text-size/src/lib.rs b/lib/text-size/src/lib.rs index bd8e820e29..dc1a09b22b 100644 --- a/lib/text-size/src/lib.rs +++ b/lib/text-size/src/lib.rs @@ -1,466 +1,13 @@ -#[cfg(feature = "serde")] -extern crate serde; - -use std::{fmt, iter, ops}; - -/// An offset into text. -/// Offset is represented as `u32` storing number of utf8-bytes, -/// but most of the clients should treat it like opaque measure. -// BREAK: TextSize(u32) -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] -pub struct TextUnit(u32); - -impl TextUnit { - // BREAK: consider renaming? - /// `TextUnit` equal to the length of this char. - #[inline(always)] - pub fn of_char(c: char) -> TextUnit { - TextUnit(c.len_utf8() as u32) - } - - // BREAK: consider renaming? - /// `TextUnit` equal to the length of this string. - /// - /// # Panics - /// Panics if the length of the string is greater than `u32::max_value()` - #[inline(always)] - pub fn of_str(s: &str) -> TextUnit { - if s.len() > u32::max_value() as usize { - panic!("string is to long") - } - TextUnit(s.len() as u32) - } - - #[inline(always)] - pub fn checked_sub(self, other: TextUnit) -> Option<TextUnit> { - self.0.checked_sub(other.0).map(TextUnit) - } - - #[inline(always)] - pub fn from_usize(size: usize) -> TextUnit { - #[cfg(debug_assertions)] - { - if size > u32::max_value() as usize { - panic!("overflow when converting to TextUnit: {}", size) - } - } - (size as u32).into() - } - - #[inline(always)] - pub fn to_usize(self) -> usize { - u32::from(self) as usize - } -} - -impl fmt::Debug for TextUnit { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - <Self as fmt::Display>::fmt(self, f) - } -} - -impl fmt::Display for TextUnit { - #[inline(always)] - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl From<TextUnit> for u32 { - #[inline(always)] - fn from(tu: TextUnit) -> u32 { - tu.0 - } -} - -impl From<u32> for TextUnit { - #[inline(always)] - fn from(tu: u32) -> TextUnit { - TextUnit(tu) - } -} - -macro_rules! unit_ops_impls { - ($T:ident, $f:ident, $op:tt, $AT:ident, $af:ident) => { - -impl ops::$T<TextUnit> for TextUnit { - type Output = TextUnit; - #[inline(always)] - fn $f(self, rhs: TextUnit) -> TextUnit { - TextUnit(self.0 $op rhs.0) - } -} - -impl<'a> ops::$T<&'a TextUnit> for TextUnit { - type Output = TextUnit; - #[inline(always)] - fn $f(self, rhs: &'a TextUnit) -> TextUnit { - ops::$T::$f(self, *rhs) - } -} - -impl<'a> ops::$T<TextUnit> for &'a TextUnit { - type Output = TextUnit; - #[inline(always)] - fn $f(self, rhs: TextUnit) -> TextUnit { - ops::$T::$f(*self, rhs) - } -} - -impl<'a, 'b> ops::$T<&'a TextUnit> for &'b TextUnit { - type Output = TextUnit; - #[inline(always)] - fn $f(self, rhs: &'a TextUnit) -> TextUnit { - ops::$T::$f(*self, *rhs) - } -} - -impl ops::$AT<TextUnit> for TextUnit { - #[inline(always)] - fn $af(&mut self, rhs: TextUnit) { - self.0 = self.0 $op rhs.0 - } -} - -impl<'a> ops::$AT<&'a TextUnit> for TextUnit { - #[inline(always)] - fn $af(&mut self, rhs: &'a TextUnit) { - ops::$AT::$af(self, *rhs) - } -} - }; -} - -macro_rules! range_ops_impls { - ($T:ident, $f:ident, $op:tt, $AT:ident, $af:ident) => { - -impl ops::$T<TextUnit> for TextRange { - type Output = TextRange; - #[inline(always)] - fn $f(self, rhs: TextUnit) -> TextRange { - TextRange::from_to( - self.start() $op rhs, - self.end() $op rhs, - ) - } -} - -impl<'a> ops::$T<&'a TextUnit> for TextRange { - type Output = TextRange; - #[inline(always)] - fn $f(self, rhs: &'a TextUnit) -> TextRange { - TextRange::from_to( - self.start() $op rhs, - self.end() $op rhs, - ) - } -} - -impl<'a> ops::$T<TextUnit> for &'a TextRange { - type Output = TextRange; - #[inline(always)] - fn $f(self, rhs: TextUnit) -> TextRange { - TextRange::from_to( - self.start() $op rhs, - self.end() $op rhs, - ) - } -} - -impl<'a, 'b> ops::$T<&'a TextUnit> for &'b TextRange { - type Output = TextRange; - #[inline(always)] - fn $f(self, rhs: &'a TextUnit) -> TextRange { - TextRange::from_to( - self.start() $op rhs, - self.end() $op rhs, - ) - } -} - -impl ops::$AT<TextUnit> for TextRange { - #[inline(always)] - fn $af(&mut self, rhs: TextUnit) { - *self = *self $op rhs - } -} - -impl<'a> ops::$AT<&'a TextUnit> for TextRange { - #[inline(always)] - fn $af(&mut self, rhs: &'a TextUnit) { - *self = *self $op rhs - } -} - }; -} - -unit_ops_impls!(Add, add, +, AddAssign, add_assign); -unit_ops_impls!(Sub, sub, -, SubAssign, sub_assign); -range_ops_impls!(Add, add, +, AddAssign, add_assign); -range_ops_impls!(Sub, sub, -, SubAssign, sub_assign); - -impl<'a> iter::Sum<&'a TextUnit> for TextUnit { - fn sum<I: Iterator<Item = &'a TextUnit>>(iter: I) -> TextUnit { - iter.fold(TextUnit::from(0), ops::Add::add) - } -} - -impl iter::Sum<TextUnit> for TextUnit { - fn sum<I: Iterator<Item = TextUnit>>(iter: I) -> TextUnit { - iter.fold(TextUnit::from(0), ops::Add::add) - } -} - -/// A range in the text, represented as a pair of `TextUnit`s. -/// -/// # Panics -/// Slicing a `&str` with `TextRange` panics if the result is -/// not a valid utf8 string. -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub struct TextRange { - start: TextUnit, - end: TextUnit, -} - -impl fmt::Debug for TextRange { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - <Self as fmt::Display>::fmt(self, f) - } -} - -impl fmt::Display for TextRange { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "[{}; {})", self.start(), self.end()) - } -} +//! Newtypes for working with text sizes/ranges in a more type-safe manner. -impl TextRange { - // BREAK: TextRange::new(from..to)? - // BREAK: TextRange(from, to)? - /// The left-inclusive range (`[from..to)`) between to points in the text - #[inline(always)] - pub fn from_to(from: TextUnit, to: TextUnit) -> TextRange { - assert!(from <= to, "Invalid text range [{}; {})", from, to); - TextRange { - start: from, - end: to, - } - } +#![forbid(unsafe_code)] +#![warn(missing_debug_implementations, missing_docs)] - /// The left-inclusive range (`[offset..offset + len)`) between to points in the text - #[inline(always)] - pub fn offset_len(offset: TextUnit, len: TextUnit) -> TextRange { - TextRange::from_to(offset, offset + len) - } - - // BREAK: pass by value - /// The inclusive start of this range - #[inline(always)] - pub fn start(&self) -> TextUnit { - self.start - } - - // BREAK: pass by value - /// The exclusive end of this range - #[inline(always)] - pub fn end(&self) -> TextUnit { - self.end - } - - // BREAK: pass by value - /// The length of this range - #[inline(always)] - pub fn len(&self) -> TextUnit { - self.end - self.start - } - - // BREAK: pass by value - /// Is this range empty of any content? - #[inline(always)] - pub fn is_empty(&self) -> bool { - self.start() == self.end() - } - - // BREAK: pass by value - #[inline(always)] - pub fn is_subrange(&self, other: &TextRange) -> bool { - other.start() <= self.start() && self.end() <= other.end() - } - - // BREAK: pass by value - #[inline(always)] - pub fn intersection(&self, other: &TextRange) -> Option<TextRange> { - let start = self.start.max(other.start()); - let end = self.end.min(other.end()); - if start <= end { - Some(TextRange::from_to(start, end)) - } else { - None - } - } - - // BREAK: pass by value - #[inline(always)] - /// The smallest range that contains both ranges - pub fn extend_to(&self, other: &TextRange) -> TextRange { - let start = self.start().min(other.start()); - let end = self.end().max(other.end()); - TextRange::from_to(start, end) - } - - // BREAK: pass by value - #[inline(always)] - pub fn contains(&self, offset: TextUnit) -> bool { - self.start() <= offset && offset < self.end() - } - - // BREAK: pass by value - #[inline(always)] - pub fn contains_inclusive(&self, offset: TextUnit) -> bool { - self.start() <= offset && offset <= self.end() - } - - #[inline(always)] - pub fn checked_sub(self, other: TextUnit) -> Option<TextRange> { - let res = TextRange::offset_len( - self.start().checked_sub(other)?, - self.len() - ); - Some(res) - } -} - -impl ops::RangeBounds<TextUnit> for TextRange { - fn start_bound(&self) -> ops::Bound<&TextUnit> { - ops::Bound::Included(&self.start) - } - - fn end_bound(&self) -> ops::Bound<&TextUnit> { - ops::Bound::Excluded(&self.end) - } -} - -impl ops::Index<TextRange> for str { - type Output = str; - - fn index(&self, index: TextRange) -> &str { - &self[index.start().0 as usize..index.end().0 as usize] - } -} - -impl ops::Index<TextRange> for String { - type Output = str; - - fn index(&self, index: TextRange) -> &str { - &self.as_str()[index] - } -} +mod range; +mod size; +mod traits; #[cfg(feature = "serde")] -mod serde_impls { - use serde::{Deserialize, Deserializer, Serialize, Serializer}; - use {TextRange, TextUnit}; - - impl Serialize for TextUnit { - fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { - self.0.serialize(serializer) - } - } - - impl<'de> Deserialize<'de> for TextUnit { - fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { - let value = Deserialize::deserialize(deserializer)?; - Ok(TextUnit(value)) - } - } - - impl Serialize for TextRange { - fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { - (self.start, self.end).serialize(serializer) - } - } - - impl<'de> Deserialize<'de> for TextRange { - fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { - let (start, end) = Deserialize::deserialize(deserializer)?; - Ok(TextRange { start, end }) - } - } -} - -#[cfg(feature = "deepsize")] -mod deepsize_impls { - deepsize::known_deep_size!(0, crate::TextUnit, crate::TextRange); -} - -#[cfg(test)] -mod tests { - use super::*; - - fn r(from: u32, to: u32) -> TextRange { - TextRange::from_to(from.into(), to.into()) - } - - #[test] - fn test_sum() { - let xs: Vec<TextUnit> = vec![0.into(), 1.into(), 2.into()]; - assert_eq!(xs.iter().sum::<TextUnit>(), 3.into()); - assert_eq!(xs.into_iter().sum::<TextUnit>(), 3.into()); - } - - #[test] - fn test_ops() { - let range = r(10, 20); - let u: TextUnit = 5.into(); - assert_eq!(range + u, r(15, 25)); - assert_eq!(range - u, r(5, 15)); - } - - #[test] - fn test_checked_ops() { - let x: TextUnit = 1.into(); - assert_eq!(x.checked_sub(1.into()), Some(0.into())); - assert_eq!(x.checked_sub(2.into()), None); - - assert_eq!(r(1, 2).checked_sub(1.into()), Some(r(0, 1))); - assert_eq!(x.checked_sub(2.into()), None); - } - - #[test] - fn test_subrange() { - let r1 = r(2, 4); - let r2 = r(2, 3); - let r3 = r(1, 3); - assert!(r2.is_subrange(&r1)); - assert!(!r3.is_subrange(&r1)); - } - - #[test] - fn check_intersection() { - assert_eq!(r(1, 2).intersection(&r(2, 3)), Some(r(2, 2))); - assert_eq!(r(1, 5).intersection(&r(2, 3)), Some(r(2, 3))); - assert_eq!(r(1, 2).intersection(&r(3, 4)), None); - } - - #[test] - fn check_extend_to() { - assert_eq!(r(1, 2).extend_to(&r(2, 3)), r(1, 3)); - assert_eq!(r(1, 5).extend_to(&r(2, 3)), r(1, 5)); - assert_eq!(r(1, 2).extend_to(&r(4, 5)), r(1, 5)); - } - - #[test] - fn check_contains() { - assert!(!r(1, 3).contains(0.into())); - assert!(r(1, 3).contains(1.into())); - assert!(r(1, 3).contains(2.into())); - assert!(!r(1, 3).contains(3.into())); - assert!(!r(1, 3).contains(4.into())); +mod serde_impls; - assert!(!r(1, 3).contains_inclusive(0.into())); - assert!(r(1, 3).contains_inclusive(1.into())); - assert!(r(1, 3).contains_inclusive(2.into())); - assert!(r(1, 3).contains_inclusive(3.into())); - assert!(!r(1, 3).contains_inclusive(4.into())); - } -} +pub use crate::{range::TextRange, size::TextSize, traits::TextSized}; diff --git a/lib/text-size/src/range.rs b/lib/text-size/src/range.rs new file mode 100644 index 0000000000..fe227cdaff --- /dev/null +++ b/lib/text-size/src/range.rs @@ -0,0 +1,198 @@ +use { + crate::TextSize, + std::{ + cmp, + convert::{TryFrom, TryInto}, + fmt, + ops::{Bound, Index, IndexMut, Range, RangeBounds}, + }, +}; + +/// A range in text, represented as a pair of [`TextSize`][struct@TextSize]. +/// +/// It is a logical error to have `end() < start()`, but +/// code must not assume this is true for `unsafe` guarantees. +/// +/// # Translation from `text_unit` +/// +/// - `TextRange::from_to(from, to)` ⟹ `TextRange::from(from..to)` +/// - `TextRange::offset_len(offset, size)` ⟹ `TextRange::from(offset..offset + size)` +/// - `range.start()` ⟹ `range.start()` +/// - `range.end()` ⟹ `range.end()` +/// - `range.len()` ⟹ `range.len()`<sup>†</sup> +/// - `range.is_empty()` ⟹ `range.is_empty()` +/// - `a.is_subrange(b)` ⟹ `b.contains(a)` +/// - `a.intersection(b)` ⟹ `TextRange::intersection(a, b)` +/// - `a.extend_to(b)` ⟹ `TextRange::covering(a, b)` +/// - `range.contains(offset)` ⟹ `range.contains_exclusive(point)` +/// - `range.contains_inclusive(offset)` ⟹ `range.contains_inclusive(point)` +/// +/// † See the note on [`TextRange::len`] for differing behavior for incorrect reverse ranges. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct TextRange { + start: TextSize, + end: TextSize, +} + +#[allow(non_snake_case)] +pub(crate) const fn TextRange(start: TextSize, end: TextSize) -> TextRange { + TextRange { start, end } +} + +impl fmt::Debug for TextRange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[{}..{})", self.start(), self.end()) + } +} + +/// Identity methods. +impl TextRange { + /// The start point of this range. + pub const fn start(self) -> TextSize { + self.start + } + + /// The end point of this range. + pub const fn end(self) -> TextSize { + self.end + } + + /// The size of this range. + /// + /// # Panics + /// + /// When `end() < start()`, triggers a subtraction overflow. + /// This will panic with debug assertions, and overflow without. + pub const fn len(self) -> TextSize { + // HACK for const fn: math on primitives only + TextSize(self.end().raw - self.start().raw) + } + + /// Check if this range empty or reversed. + /// + /// When `end() < start()`, this returns false. + /// Code should prefer `is_empty()` to `len() == 0`, + /// as this safeguards against incorrect reverse ranges. + pub const fn is_empty(self) -> bool { + // HACK for const fn: math on primitives only + self.start().raw >= self.end().raw + } +} + +/// Manipulation methods. +impl TextRange { + /// Check if this range completely contains another range. + pub fn contains(self, other: TextRange) -> bool { + self.start() <= other.start() && other.end() <= self.end() + } + + /// The range covered by both ranges, if it exists. + /// If the ranges touch but do not overlap, the output range is empty. + pub fn intersection(lhs: TextRange, rhs: TextRange) -> Option<TextRange> { + let start = cmp::max(lhs.start(), rhs.start()); + let end = cmp::min(lhs.end(), rhs.end()); + Some(TextRange(start, end)).filter(|_| start <= end) + } + + /// The smallest range that completely contains both ranges. + pub fn covering(lhs: TextRange, rhs: TextRange) -> TextRange { + let start = cmp::min(lhs.start(), rhs.start()); + let end = cmp::max(lhs.end(), rhs.end()); + TextRange(start, end) + } + + /// Check if this range contains a point. + /// + /// The end index is considered excluded. + pub fn contains_exclusive(self, point: impl Into<TextSize>) -> bool { + let point = point.into(); + self.start() <= point && point < self.end() + } + + /// Check if this range contains a point. + /// + /// The end index is considered included. + pub fn contains_inclusive(self, point: impl Into<TextSize>) -> bool { + let point = point.into(); + self.start() <= point && point <= self.end() + } +} + +fn ix(size: TextSize) -> usize { + size.try_into() + .unwrap_or_else(|_| panic!("overflow when converting TextSize to usize index")) +} + +impl Index<TextRange> for str { + type Output = str; + fn index(&self, index: TextRange) -> &Self::Output { + &self[ix(index.start())..ix(index.end())] + } +} + +impl IndexMut<TextRange> for str { + fn index_mut(&mut self, index: TextRange) -> &mut Self::Output { + &mut self[ix(index.start())..ix(index.end())] + } +} + +impl RangeBounds<TextSize> for TextRange { + fn start_bound(&self) -> Bound<&TextSize> { + Bound::Included(&self.start) + } + + fn end_bound(&self) -> Bound<&TextSize> { + Bound::Excluded(&self.end) + } +} + +macro_rules! conversions { + (From<$lte:ident> for TextRange) => { + impl From<Range<$lte>> for TextRange { + fn from(value: Range<$lte>) -> TextRange { + TextRange(value.start.into(), value.end.into()) + } + } + // Just support `start..end` for now, not `..end`, `start..=end`, `..=end`. + }; + (TryFrom<$gt:ident> for TextRange) => { + impl TryFrom<Range<$gt>> for TextRange { + type Error = <$gt as TryInto<u32>>::Error; + fn try_from(value: Range<$gt>) -> Result<TextRange, Self::Error> { + Ok(TextRange(value.start.try_into()?, value.end.try_into()?)) + } + } + // Just support `start..end` for now, not `..end`, `start..=end`, `..=end`. + }; + { + lt TextSize [$($lt:ident)*] + eq TextSize [$($eq:ident)*] + gt TextSize [$($gt:ident)*] + varries [$($var:ident)*] + } => { + $( + conversions!(From<$lt> for TextRange); + // unlike TextSize, we do not provide conversions in the "out" direction. + )* + + $( + conversions!(From<$eq> for TextRange); + )* + + $( + conversions!(TryFrom<$gt> for TextRange); + )* + + $( + conversions!(TryFrom<$var> for TextRange); + )* + }; +} + +// FIXME: when `default impl` is usable, change to blanket impls for [Try]Into<TextSize> instead +conversions! { + lt TextSize [u8 u16] + eq TextSize [u32 TextSize] + gt TextSize [u64] + varries [usize] +} diff --git a/lib/text-size/src/serde_impls.rs b/lib/text-size/src/serde_impls.rs new file mode 100644 index 0000000000..1963413fd8 --- /dev/null +++ b/lib/text-size/src/serde_impls.rs @@ -0,0 +1,40 @@ +use { + crate::{TextRange, TextSize}, + serde::{Deserialize, Deserializer, Serialize, Serializer}, +}; + +impl Serialize for TextSize { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + self.raw.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for TextSize { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + Deserialize::deserialize(deserializer).map(TextSize) + } +} + +impl Serialize for TextRange { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + (self.start(), self.end()).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for TextRange { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + Deserialize::deserialize(deserializer).map(|(start, end)| TextRange(start, end)) + } +} diff --git a/lib/text-size/src/size.rs b/lib/text-size/src/size.rs new file mode 100644 index 0000000000..43bf19dac9 --- /dev/null +++ b/lib/text-size/src/size.rs @@ -0,0 +1,185 @@ +use { + crate::TextSized, + std::{ + convert::{TryFrom, TryInto}, + fmt, iter, + num::TryFromIntError, + ops::{Add, AddAssign, Sub, SubAssign}, + u32, + }, +}; + +/// A measure of text length. Also, equivalently, an index into text. +/// +/// This is a utf8-bytes-offset stored as `u32`, but +/// most clients should treat it as an opaque measure. +/// +/// # Translation from `text_unit` +/// +/// - `TextUnit::of_char(c)` ⟹ `TextSize::of(c)` +/// - `TextUnit::of_str(s)` ⟹ `TextSize:of(s)` +/// - `TextUnit::from_usize(size)` ⟹ `TextSize::try_from(size).unwrap_or_else(|| panic!(_))` +/// - `unit.to_usize()` ⟹ `usize::try_from(size).unwrap_or_else(|| panic!(_))` +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct TextSize { + pub(crate) raw: u32, +} + +#[allow(non_snake_case)] +pub(crate) const fn TextSize(raw: u32) -> TextSize { + TextSize { raw } +} + +impl fmt::Debug for TextSize { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl fmt::Display for TextSize { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.raw, f) + } +} + +impl TextSize { + /// The text size of some text-like object. + pub fn of(text: impl TextSized) -> TextSize { + text.text_size() + } + + /// A size of zero. + /// + /// This is equivalent to `TextSize::default()` or [`TextSize::MIN`], + /// but is more explicit on intent. + pub const fn zero() -> TextSize { + TextSize(0) + } +} + +/// Methods to act like a primitive integer type, where reasonably applicable. +// Last updated for parity with Rust 1.42.0. +impl TextSize { + /// The smallest representable text size. (`u32::MIN`) + pub const MIN: TextSize = TextSize(u32::MIN); + /// The largest representable text size. (`u32::MAX`) + pub const MAX: TextSize = TextSize(u32::MAX); + + #[allow(missing_docs)] + pub fn checked_add(self, rhs: TextSize) -> Option<TextSize> { + self.raw.checked_add(rhs.raw).map(TextSize) + } + + #[allow(missing_docs)] + pub fn checked_sub(self, rhs: TextSize) -> Option<TextSize> { + self.raw.checked_sub(rhs.raw).map(TextSize) + } +} + +macro_rules! conversions { + (From<TextSize> for $gte:ident) => { + impl From<TextSize> for $gte { + fn from(value: TextSize) -> $gte { + value.raw.into() + } + } + }; + (From<$lte:ident> for TextSize) => { + impl From<$lte> for TextSize { + fn from(value: $lte) -> TextSize { + TextSize(value.into()) + } + } + }; + (TryFrom<TextSize> for $lt:ident) => { + impl TryFrom<TextSize> for $lt { + type Error = TryFromIntError; + fn try_from(value: TextSize) -> Result<$lt, Self::Error> { + value.raw.try_into() + } + } + }; + (TryFrom<$gt:ident> for TextSize) => { + impl TryFrom<$gt> for TextSize { + type Error = <$gt as TryInto<u32>>::Error; + fn try_from(value: $gt) -> Result<TextSize, Self::Error> { + value.try_into().map(TextSize) + } + } + }; + { + lt u32 [$($lt:ident)*] + eq u32 [$($eq:ident)*] + gt u32 [$($gt:ident)*] + varries [$($var:ident)*] + } => { + $( + conversions!(From<$lt> for TextSize); + conversions!(TryFrom<TextSize> for $lt); + )* + + $( + conversions!(From<$eq> for TextSize); + conversions!(From<TextSize> for $eq); + )* + + $( + conversions!(TryFrom<$gt> for TextSize); + conversions!(From<TextSize> for $gt); + )* + + $( + conversions!(TryFrom<$var> for TextSize); + conversions!(TryFrom<TextSize> for $var); + )* + }; +} + +conversions! { + lt u32 [u8 u16] + eq u32 [u32] + gt u32 [u64] + varries [usize] +} + +// NB: We do not provide the transparent-ref impls like the stdlib does. +impl Add for TextSize { + type Output = TextSize; + fn add(self, rhs: TextSize) -> TextSize { + TextSize(self.raw + rhs.raw) + } +} + +impl<A> AddAssign<A> for TextSize +where + TextSize: Add<A, Output = TextSize>, +{ + fn add_assign(&mut self, rhs: A) { + *self = *self + rhs + } +} + +impl Sub for TextSize { + type Output = TextSize; + fn sub(self, rhs: TextSize) -> TextSize { + TextSize(self.raw - rhs.raw) + } +} + +impl<S> SubAssign<S> for TextSize +where + TextSize: Sub<S, Output = TextSize>, +{ + fn sub_assign(&mut self, rhs: S) { + *self = *self - rhs + } +} + +impl<A> iter::Sum<A> for TextSize +where + TextSize: Add<A, Output = TextSize>, +{ + fn sum<I: Iterator<Item = A>>(iter: I) -> TextSize { + iter.fold(TextSize::zero(), Add::add) + } +} diff --git a/lib/text-size/src/traits.rs b/lib/text-size/src/traits.rs new file mode 100644 index 0000000000..877f057895 --- /dev/null +++ b/lib/text-size/src/traits.rs @@ -0,0 +1,35 @@ +use { + crate::{TextRange, TextSize}, + std::convert::TryInto, +}; + +/// Text-like structures that have a text size. +pub trait TextSized: Copy { + /// The size of this text-alike. + fn text_size(self) -> TextSize; +} + +impl TextSized for &'_ str { + fn text_size(self) -> TextSize { + let len = self.len(); + if let Ok(size) = len.try_into() { + size + } else if cfg!(debug_assertions) { + panic!("overflow when converting to TextSize"); + } else { + TextSize(len as u32) + } + } +} + +impl TextSized for char { + fn text_size(self) -> TextSize { + TextSize(self.len_utf8() as u32) + } +} + +impl TextSized for TextRange { + fn text_size(self) -> TextSize { + self.len() + } +} diff --git a/lib/text-size/tests/main.rs b/lib/text-size/tests/main.rs new file mode 100644 index 0000000000..3288c27320 --- /dev/null +++ b/lib/text-size/tests/main.rs @@ -0,0 +1,73 @@ +use {std::ops, text_size::*}; + +fn size(x: u32) -> TextSize { + TextSize::from(x) +} + +fn range(x: ops::Range<u32>) -> TextRange { + TextRange::from(x) +} + +#[test] +fn sum() { + let xs: Vec<TextSize> = vec![size(0), size(1), size(2)]; + assert_eq!(xs.iter().copied().sum::<TextSize>(), size(3)); + assert_eq!(xs.into_iter().sum::<TextSize>(), size(3)); +} + +#[test] +fn math() { + assert_eq!(size(10) + size(5), size(15)); + assert_eq!(size(10) - size(5), size(5)); +} + +#[test] +fn checked_math() { + assert_eq!(size(1).checked_add(size(1)), Some(size(2))); + assert_eq!(size(1).checked_sub(size(1)), Some(size(0))); + assert_eq!(size(1).checked_sub(size(2)), None); + assert_eq!(TextSize::MAX.checked_add(size(1)), None); +} + +#[test] +#[rustfmt::skip] +fn contains() { + assert!( range(2..4).contains(range(2..3))); + assert!( ! range(2..4).contains(range(1..3))); +} + +#[test] +fn intersection() { + assert_eq!( + TextRange::intersection(range(1..2), range(2..3)), + Some(range(2..2)) + ); + assert_eq!( + TextRange::intersection(range(1..5), range(2..3)), + Some(range(2..3)) + ); + assert_eq!(TextRange::intersection(range(1..2), range(3..4)), None); +} + +#[test] +fn covering() { + assert_eq!(TextRange::covering(range(1..2), range(2..3)), range(1..3)); + assert_eq!(TextRange::covering(range(1..5), range(2..3)), range(1..5)); + assert_eq!(TextRange::covering(range(1..2), range(4..5)), range(1..5)); +} + +#[test] +#[rustfmt::skip] +fn contains_point() { + assert!( ! range(1..3).contains_exclusive(size(0))); + assert!( range(1..3).contains_exclusive(size(1))); + assert!( range(1..3).contains_exclusive(size(2))); + assert!( ! range(1..3).contains_exclusive(size(3))); + assert!( ! range(1..3).contains_exclusive(size(4))); + + assert!( ! range(1..3).contains_inclusive(size(0))); + assert!( range(1..3).contains_inclusive(size(1))); + assert!( range(1..3).contains_inclusive(size(2))); + assert!( range(1..3).contains_inclusive(size(3))); + assert!( ! range(1..3).contains_inclusive(size(4))); +} diff --git a/lib/text-size/tests/serde.rs b/lib/text-size/tests/serde.rs new file mode 100644 index 0000000000..62254634dd --- /dev/null +++ b/lib/text-size/tests/serde.rs @@ -0,0 +1,57 @@ +use {serde_test::*, std::ops, text_size::*}; + +fn size(x: u32) -> TextSize { + TextSize::from(x) +} + +fn range(x: ops::Range<u32>) -> TextRange { + TextRange::from(x) +} + +#[test] +fn size_serialization() { + assert_tokens(&size(00), &[Token::U32(00)]); + assert_tokens(&size(10), &[Token::U32(10)]); + assert_tokens(&size(20), &[Token::U32(20)]); + assert_tokens(&size(30), &[Token::U32(30)]); +} + +#[test] +fn range_serialization() { + assert_tokens( + &range(00..10), + &[ + Token::Tuple { len: 2 }, + Token::U32(00), + Token::U32(10), + Token::TupleEnd, + ], + ); + assert_tokens( + &range(10..20), + &[ + Token::Tuple { len: 2 }, + Token::U32(10), + Token::U32(20), + Token::TupleEnd, + ], + ); + assert_tokens( + &range(20..30), + &[ + Token::Tuple { len: 2 }, + Token::U32(20), + Token::U32(30), + Token::TupleEnd, + ], + ); + assert_tokens( + &range(30..40), + &[ + Token::Tuple { len: 2 }, + Token::U32(30), + Token::U32(40), + Token::TupleEnd, + ], + ); +} |