#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Hash)]
pub struct TextRange {
pub start: usize,
pub end: usize,
}
impl TextRange {
pub const fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
pub const fn cursor(position: usize) -> Self {
Self {
start: position,
end: position,
}
}
pub const fn zero() -> Self {
Self { start: 0, end: 0 }
}
pub const fn collapsed(&self) -> bool {
self.start == self.end
}
pub fn length(&self) -> usize {
self.end.abs_diff(self.start)
}
pub fn min(&self) -> usize {
self.start.min(self.end)
}
pub fn max(&self) -> usize {
self.start.max(self.end)
}
pub fn contains(&self, index: usize) -> bool {
index >= self.min() && index < self.max()
}
pub fn coerce_in(&self, max: usize) -> Self {
Self {
start: self.start.min(max),
end: self.end.min(max),
}
}
pub const fn all(length: usize) -> Self {
Self {
start: 0,
end: length,
}
}
pub fn safe_slice<'a>(&self, text: &'a str) -> &'a str {
if text.is_empty() {
return "";
}
let start = self.min().min(text.len());
let end = self.max().min(text.len());
let start = if text.is_char_boundary(start) {
start
} else {
(0..start)
.rev()
.find(|&i| text.is_char_boundary(i))
.unwrap_or(0)
};
let end = if text.is_char_boundary(end) {
end
} else {
(end..=text.len())
.find(|&i| text.is_char_boundary(i))
.unwrap_or(text.len())
};
if start <= end {
&text[start..end]
} else {
""
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cursor_is_collapsed() {
let cursor = TextRange::cursor(5);
assert!(cursor.collapsed());
assert_eq!(cursor.length(), 0);
assert_eq!(cursor.start, 5);
assert_eq!(cursor.end, 5);
}
#[test]
fn selection_is_not_collapsed() {
let selection = TextRange::new(2, 7);
assert!(!selection.collapsed());
assert_eq!(selection.length(), 5);
}
#[test]
fn reverse_selection_length() {
let reverse = TextRange::new(7, 2);
assert_eq!(reverse.length(), 5);
assert_eq!(reverse.min(), 2);
assert_eq!(reverse.max(), 7);
}
#[test]
fn coerce_in_bounds() {
let range = TextRange::new(5, 100);
let coerced = range.coerce_in(10);
assert_eq!(coerced.start, 5);
assert_eq!(coerced.end, 10);
}
#[test]
fn contains_index() {
let range = TextRange::new(2, 5);
assert!(!range.contains(1));
assert!(range.contains(2));
assert!(range.contains(3));
assert!(range.contains(4));
assert!(!range.contains(5)); }
#[test]
fn safe_slice_basic() {
let range = TextRange::new(0, 5);
assert_eq!(range.safe_slice("Hello World"), "Hello");
}
#[test]
fn safe_slice_beyond_bounds() {
let range = TextRange::new(0, 100);
assert_eq!(range.safe_slice("Hello"), "Hello");
}
#[test]
fn safe_slice_unicode() {
let text = "Hello 🌍";
let range = TextRange::new(0, 7);
let slice = range.safe_slice(text);
assert!(slice == "Hello " || slice == "Hello 🌍");
}
}