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
Aleksey Kladov 2020-03-09
parent 2965e2a · parent 2748c49 · commit 72aef8c
-rw-r--r--lib/text-size/Cargo.toml22
-rw-r--r--lib/text-size/src/lib.rs469
-rw-r--r--lib/text-size/src/range.rs198
-rw-r--r--lib/text-size/src/serde_impls.rs40
-rw-r--r--lib/text-size/src/size.rs185
-rw-r--r--lib/text-size/src/traits.rs35
-rw-r--r--lib/text-size/tests/main.rs73
-rw-r--r--lib/text-size/tests/serde.rs57
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,
+ ],
+ );
+}