Unnamed repository; edit this file 'description' to name the repository.
Aggressive refactor
CAD97 2020-03-08
parent 2965e2a · commit 18f7f27
-rw-r--r--lib/text-size/Cargo.toml21
-rw-r--r--lib/text-size/src/lib.rs468
-rw-r--r--lib/text-size/src/range.rs355
-rw-r--r--lib/text-size/src/serde_impls.rs40
-rw-r--r--lib/text-size/src/size.rs246
-rw-r--r--lib/text-size/src/traits.rs29
-rw-r--r--lib/text-size/tests/main.rs67
-rw-r--r--lib/text-size/tests/serde.rs49
8 files changed, 812 insertions, 463 deletions
diff --git a/lib/text-size/Cargo.toml b/lib/text-size/Cargo.toml
index 9080b3fad8..b5d42f0182 100644
--- a/lib/text-size/Cargo.toml
+++ b/lib/text-size/Cargo.toml
@@ -1,12 +1,25 @@
[package]
-name = "text_unit"
-version = "0.1.10"
-authors = ["Aleksey Kladov <[email protected]>"]
+name = "text-size"
+version = "0.99.0-dev.1"
+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 }
+serde = { version = "1.0", optional = true, default_features = false }
deepsize = { version = "0.1", 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..66bc653790 100644
--- a/lib/text-size/src/lib.rs
+++ b/lib/text-size/src/lib.rs
@@ -1,466 +1,16 @@
-#[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)
- }
-}
+//! Newtypes for working with text sizes/ranges in a more type-safe manner.
-impl fmt::Display for TextRange {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "[{}; {})", self.start(), self.end())
- }
-}
+#![forbid(unsafe_code)]
+#![warn(missing_debug_implementations, missing_docs)]
-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,
- }
- }
-
- /// 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)
- }
- }
+mod serde_impls;
- 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 })
- }
- }
-}
+pub use crate::{range::TextRange, size::TextSize, traits::TextSized};
#[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()));
-
- 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()));
- }
-}
+deepsize::known_deep_size!(0, TextSize, TextRange);
diff --git a/lib/text-size/src/range.rs b/lib/text-size/src/range.rs
new file mode 100644
index 0000000000..89012f2779
--- /dev/null
+++ b/lib/text-size/src/range.rs
@@ -0,0 +1,355 @@
+use {
+ crate::{TextSize, TextSized},
+ std::{
+ cmp,
+ convert::{TryFrom, TryInto},
+ fmt,
+ num::TryFromIntError,
+ ops::{
+ Add, AddAssign, Bound, Index, IndexMut, Range, RangeBounds, RangeInclusive, RangeTo,
+ RangeToInclusive, Sub, SubAssign,
+ },
+ },
+};
+
+/// 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::at(offset).with_len(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_point(point)`
+/// - `range.contains_inclusive(offset)` ⟹ `range.contains_point_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 {
+ 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())
+ }
+}
+
+/// 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 {
+ /// A range covering the text size of some text-like object.
+ pub fn of(size: impl TextSized) -> TextRange {
+ TextRange(0.into(), size.text_size())
+ }
+
+ /// An empty range at some text size offset.
+ pub fn at(size: impl Into<TextSize>) -> TextRange {
+ let size = size.into();
+ TextRange(size, size)
+ }
+
+ /// Set the length without changing the starting offset.
+ pub fn with_len(self, len: impl Into<TextSize>) -> TextRange {
+ TextRange(self.start(), self.start() + len.into())
+ }
+
+ /// Set the starting offset without changing the length.
+ pub fn with_offset(self, offset: impl Into<TextSize>) -> TextRange {
+ TextRange::at(offset).with_len(self.len())
+ }
+
+ /// 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_point(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_point_inclusive(self, point: impl Into<TextSize>) -> bool {
+ let point = point.into();
+ self.start() <= point && point <= self.end()
+ }
+
+ /// Offset the entire range by some text size.
+ pub fn checked_add(self, rhs: impl TryInto<TextSize>) -> Option<TextRange> {
+ let rhs = rhs.try_into().ok()?;
+ Some(TextRange(
+ self.start().checked_add(rhs)?,
+ self.end().checked_add(rhs)?,
+ ))
+ }
+
+ /// Offset the entire range by some text size.
+ pub fn checked_sub(self, rhs: impl TryInto<TextSize>) -> Option<TextRange> {
+ let rhs = rhs.try_into().ok()?;
+ Some(TextRange(
+ self.start().checked_sub(rhs)?,
+ self.end().checked_sub(rhs)?,
+ ))
+ }
+}
+
+impl Index<TextRange> for str {
+ type Output = str;
+ fn index(&self, index: TextRange) -> &Self::Output {
+ &self[index.start().ix()..index.end().ix()]
+ }
+}
+
+impl IndexMut<TextRange> for str {
+ fn index_mut(&mut self, index: TextRange) -> &mut Self::Output {
+ &mut self[index.start().ix()..index.end().ix()]
+ }
+}
+
+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())
+ }
+ }
+ impl TryFrom<RangeInclusive<$lte>> for TextRange {
+ type Error = TryFromIntError;
+ fn try_from(value: RangeInclusive<$lte>) -> Result<TextRange, Self::Error> {
+ let (start, end) = value.into_inner();
+ let end: TextSize = end.into();
+ // This is the only way to get a TryFromIntError currently.
+ let end = end.checked_add(1).ok_or_else(|| u8::try_from(-1).unwrap_err())?;
+ Ok(TextRange(start.into(), end))
+ }
+ }
+ impl From<RangeTo<$lte>> for TextRange {
+ fn from(value: RangeTo<$lte>) -> TextRange {
+ TextRange(0.into(), value.end.into())
+ }
+ }
+ impl TryFrom<RangeToInclusive<$lte>> for TextRange {
+ type Error = TryFromIntError;
+ fn try_from(value: RangeToInclusive<$lte>) -> Result<TextRange, Self::Error> {
+ let start: TextSize = 0.into();
+ let end: TextSize = value.end.into();
+ TextRange::try_from(start..=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()?))
+ }
+ }
+ impl TryFrom<RangeInclusive<$gt>> for TextRange {
+ type Error = TryFromIntError;
+ fn try_from(value: RangeInclusive<$gt>) -> Result<TextRange, Self::Error> {
+ let (start, end) = value.into_inner();
+ let end: TextSize = end.try_into()?;
+ // This is the only way to get a TryFromIntError currently.
+ let end = end.checked_add(1).ok_or_else(|| u8::try_from(-1).unwrap_err())?;
+ Ok(TextRange(start.try_into()?, end))
+ }
+ }
+ impl TryFrom<RangeTo<$gt>> for TextRange {
+ type Error = TryFromIntError;
+ fn try_from(value: RangeTo<$gt>) -> Result<TextRange, Self::Error> {
+ Ok(TextRange(0.into(), value.end.try_into()?))
+ }
+ }
+ impl TryFrom<RangeToInclusive<$gt>> for TextRange {
+ type Error = TryFromIntError;
+ fn try_from(value: RangeToInclusive<$gt>) -> Result<TextRange, Self::Error> {
+ let start: TextSize = 0.into();
+ let end: TextSize = value.end.try_into()?;
+ TextRange::try_from(start..=end)
+ }
+ }
+ };
+ {
+ lt TextSize [$($lt:ident)*]
+ eq TextSize [$($eq:ident)*]
+ gt TextSize [$($gt:ident)*]
+ varries [$($var:ident)*]
+ } => {
+ $(
+ // Not `From` yet because of integer type fallback. We want e.g.
+ // `TextRange::from(0)` and `range + 1` to work, and more `From`
+ // impls means that this will try (and fail) to use i32 rather
+ // than one of the unsigned integer types that actually work.
+ conversions!(TryFrom<$lt> for TextRange);
+ )*
+
+ $(
+ 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]
+}
+
+impl Into<TextRange> for &'_ TextRange {
+ fn into(self) -> TextRange {
+ *self
+ }
+}
+
+impl Into<TextRange> for &'_ mut TextRange {
+ fn into(self) -> TextRange {
+ *self
+ }
+}
+
+macro_rules! op {
+ (impl $Op:ident for TextRange by fn $f:ident = $op:tt) => {
+ impl<IntoSize: Copy> $Op<IntoSize> for TextRange
+ where
+ TextSize: $Op<IntoSize, Output = TextSize>,
+ {
+ type Output = TextRange;
+ fn $f(self, rhs: IntoSize) -> TextRange {
+ TextRange(self.start() $op rhs, self.end() $op rhs)
+ }
+ }
+ impl<IntoSize> $Op<IntoSize> for &'_ TextRange
+ where
+ TextRange: $Op<IntoSize, Output = TextRange>,
+ {
+ type Output = TextRange;
+ fn $f(self, rhs: IntoSize) -> TextRange {
+ *self $op rhs
+ }
+ }
+ impl<IntoSize> $Op<IntoSize> for &'_ mut TextRange
+ where
+ TextRange: $Op<IntoSize, Output = TextRange>,
+ {
+ type Output = TextRange;
+ fn $f(self, rhs: IntoSize) -> TextRange {
+ *self $op rhs
+ }
+ }
+ };
+}
+
+op!(impl Add for TextRange by fn add = +);
+op!(impl Sub for TextRange by fn sub = -);
+
+impl<A> AddAssign<A> for TextRange
+where
+ TextRange: Add<A, Output = TextRange>,
+{
+ fn add_assign(&mut self, rhs: A) {
+ *self = *self + rhs
+ }
+}
+
+impl<S> SubAssign<S> for TextRange
+where
+ TextRange: Sub<S, Output = TextRange>,
+{
+ fn sub_assign(&mut self, rhs: S) {
+ *self = *self - rhs
+ }
+}
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..80dba0aba6
--- /dev/null
+++ b/lib/text-size/src/size.rs
@@ -0,0 +1,246 @@
+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::new(size)`
+/// - `unit.to_usize()` ⟹ `size.ix()`
+#[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 text size for some `usize`.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the size is greater than `u32::MAX` and debug assertions are
+ /// enabled. If debug assertions are not enabled, wraps into `u32` space.
+ pub fn new(size: usize) -> TextSize {
+ if let Ok(size) = size.try_into() {
+ size
+ } else if cfg!(debug_assertions) {
+ panic!("overflow when converting to TextSize");
+ } else {
+ TextSize(size as u32)
+ }
+ }
+
+ /// Convert this text size into the standard indexing type.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the size is greater than `usize::MAX`. This can only
+ /// occur on targets where `size_of::<usize>() < size_of::<u32>()`.
+ pub fn ix(self) -> usize {
+ if let Ok(ix) = self.try_into() {
+ ix
+ } else {
+ panic!("overflow when converting TextSize to usize index")
+ }
+ }
+}
+
+/// 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: impl TryInto<TextSize>) -> Option<TextSize> {
+ let rhs = rhs.try_into().ok()?;
+ self.raw.checked_add(rhs.raw).map(TextSize)
+ }
+
+ #[allow(missing_docs)]
+ pub fn checked_sub(self, rhs: impl TryInto<TextSize>) -> Option<TextSize> {
+ let rhs = rhs.try_into().ok()?;
+ 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)*]
+ } => {
+ $(
+ // Not `From` yet because of integer type fallback. We want e.g.
+ // `TextSize::from(0)` and `size + 1` to work, and more `From`
+ // impls means that this will try (and fail) to use i32 rather
+ // than one of the unsigned integer types that actually work.
+ conversions!(TryFrom<$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 i32] // i32 so that `checked_add($lit)` (`try_from($lit)`) can work
+ // this will unfortunately have to hang around even if integer literal type fallback improves
+}
+
+impl Into<TextSize> for &'_ TextSize {
+ fn into(self) -> TextSize {
+ *self
+ }
+}
+
+impl Into<TextSize> for &'_ mut TextSize {
+ fn into(self) -> TextSize {
+ *self
+ }
+}
+
+macro_rules! op {
+ (impl $Op:ident for TextSize by fn $f:ident = $op:tt) => {
+ impl<IntoSize: Into<TextSize>> $Op<IntoSize> for TextSize {
+ type Output = TextSize;
+ fn $f(self, rhs: IntoSize) -> TextSize {
+ TextSize(self.raw $op rhs.into().raw)
+ }
+ }
+ impl<IntoSize> $Op<IntoSize> for &'_ TextSize
+ where
+ TextSize: $Op<IntoSize, Output = TextSize>,
+ {
+ type Output = TextSize;
+ fn $f(self, rhs: IntoSize) -> TextSize {
+ *self $op rhs
+ }
+ }
+ impl<IntoSize> $Op<IntoSize> for &'_ mut TextSize
+ where
+ TextSize: $Op<IntoSize, Output = TextSize>,
+ {
+ type Output = TextSize;
+ fn $f(self, rhs: IntoSize) -> TextSize {
+ *self $op rhs
+ }
+ }
+ };
+}
+
+op!(impl Add for TextSize by fn add = +);
+op!(impl Sub for TextSize by fn sub = -);
+
+impl<A> AddAssign<A> for TextSize
+where
+ TextSize: Add<A, Output = TextSize>,
+{
+ fn add_assign(&mut self, rhs: A) {
+ *self = *self + rhs
+ }
+}
+
+impl<S> SubAssign<S> for TextSize
+where
+ TextSize: Sub<S, Output = TextSize>,
+{
+ fn sub_assign(&mut self, rhs: S) {
+ *self = *self - rhs
+ }
+}
+
+impl iter::Sum for TextSize {
+ fn sum<I: Iterator<Item = TextSize>>(iter: I) -> TextSize {
+ iter.fold(TextSize::default(), Add::add)
+ }
+}
+
+impl<'a> iter::Sum<&'a Self> for TextSize {
+ fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
+ iter.fold(TextSize::default(), Add::add)
+ }
+}
diff --git a/lib/text-size/src/traits.rs b/lib/text-size/src/traits.rs
new file mode 100644
index 0000000000..52601534d2
--- /dev/null
+++ b/lib/text-size/src/traits.rs
@@ -0,0 +1,29 @@
+use {
+ crate::{TextRange, TextSize},
+ std::convert::TryInto,
+};
+
+/// Text-like structures that have a text size.
+pub trait TextSized {
+ /// The size of this text-alike.
+ fn text_size(&self) -> TextSize;
+}
+
+impl TextSized for str {
+ fn text_size(&self) -> TextSize {
+ let len = self.len();
+ TextSize::new(len)
+ }
+}
+
+impl TextSized for char {
+ fn text_size(&self) -> TextSize {
+ self.len_utf8().try_into().unwrap()
+ }
+}
+
+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..a7eef0a2cd
--- /dev/null
+++ b/lib/text-size/tests/main.rs
@@ -0,0 +1,67 @@
+use text_size::*;
+
+fn r(from: u32, to: u32) -> TextRange {
+ TextRange::from(from..to)
+}
+
+#[test]
+fn sum() {
+ let xs: Vec<TextSize> = vec![0.into(), 1.into(), 2.into()];
+ assert_eq!(xs.iter().sum::<TextSize>(), 3.into());
+ assert_eq!(xs.into_iter().sum::<TextSize>(), 3.into());
+}
+
+#[test]
+fn math() {
+ let range = r(10, 20);
+ assert_eq!(range + 5, r(15, 25));
+ assert_eq!(range - 5, r(5, 15));
+}
+
+#[test]
+fn checked_math() {
+ let x: TextSize = 1.into();
+ assert_eq!(x.checked_sub(1), Some(0.into()));
+ assert_eq!(x.checked_sub(2), None);
+
+ assert_eq!(r(1, 2).checked_sub(1), Some(r(0, 1)));
+ assert_eq!(x.checked_sub(2), None);
+}
+
+#[test]
+fn contains() {
+ let r1 = r(2, 4);
+ let r2 = r(2, 3);
+ let r3 = r(1, 3);
+ assert!(r1.contains(r2));
+ assert!(!r1.contains(r3));
+}
+
+#[test]
+fn intersection() {
+ assert_eq!(TextRange::intersection(r(1, 2), r(2, 3)), Some(r(2, 2)));
+ assert_eq!(TextRange::intersection(r(1, 5), r(2, 3)), Some(r(2, 3)));
+ assert_eq!(TextRange::intersection(r(1, 2), r(3, 4)), None);
+}
+
+#[test]
+fn covering() {
+ assert_eq!(TextRange::covering(r(1, 2), r(2, 3)), r(1, 3));
+ assert_eq!(TextRange::covering(r(1, 5), r(2, 3)), r(1, 5));
+ assert_eq!(TextRange::covering(r(1, 2), r(4, 5)), r(1, 5));
+}
+
+#[test]
+fn contains_point() {
+ assert!(!r(1, 3).contains_point(0));
+ assert!(r(1, 3).contains_point(1));
+ assert!(r(1, 3).contains_point(2));
+ assert!(!r(1, 3).contains_point(3));
+ assert!(!r(1, 3).contains_point(4));
+
+ assert!(!r(1, 3).contains_point_inclusive(0));
+ assert!(r(1, 3).contains_point_inclusive(1));
+ assert!(r(1, 3).contains_point_inclusive(2));
+ assert!(r(1, 3).contains_point_inclusive(3));
+ assert!(!r(1, 3).contains_point_inclusive(4));
+}
diff --git a/lib/text-size/tests/serde.rs b/lib/text-size/tests/serde.rs
new file mode 100644
index 0000000000..439b9d71f5
--- /dev/null
+++ b/lib/text-size/tests/serde.rs
@@ -0,0 +1,49 @@
+use {serde_test::*, text_size::*};
+
+#[test]
+fn size() {
+ assert_tokens(&TextSize::new(00), &[Token::U32(00)]);
+ assert_tokens(&TextSize::new(10), &[Token::U32(10)]);
+ assert_tokens(&TextSize::new(20), &[Token::U32(20)]);
+ assert_tokens(&TextSize::new(30), &[Token::U32(30)]);
+}
+
+#[test]
+fn range() {
+ assert_tokens(
+ &TextRange::from(00..10),
+ &[
+ Token::Tuple { len: 2 },
+ Token::U32(00),
+ Token::U32(10),
+ Token::TupleEnd,
+ ],
+ );
+ assert_tokens(
+ &TextRange::from(10..20),
+ &[
+ Token::Tuple { len: 2 },
+ Token::U32(10),
+ Token::U32(20),
+ Token::TupleEnd,
+ ],
+ );
+ assert_tokens(
+ &TextRange::from(20..30),
+ &[
+ Token::Tuple { len: 2 },
+ Token::U32(20),
+ Token::U32(30),
+ Token::TupleEnd,
+ ],
+ );
+ assert_tokens(
+ &TextRange::from(30..40),
+ &[
+ Token::Tuple { len: 2 },
+ Token::U32(30),
+ Token::U32(40),
+ Token::TupleEnd,
+ ],
+ );
+}