cranpose_foundation/text/
range.rs1#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Hash)]
12pub struct TextRange {
13 pub start: usize,
15 pub end: usize,
17}
18
19impl TextRange {
20 pub const fn new(start: usize, end: usize) -> Self {
22 Self { start, end }
23 }
24
25 pub const fn cursor(position: usize) -> Self {
27 Self {
28 start: position,
29 end: position,
30 }
31 }
32
33 pub const fn zero() -> Self {
35 Self { start: 0, end: 0 }
36 }
37
38 pub const fn collapsed(&self) -> bool {
40 self.start == self.end
41 }
42
43 pub fn length(&self) -> usize {
45 self.end.abs_diff(self.start)
46 }
47
48 pub fn min(&self) -> usize {
50 self.start.min(self.end)
51 }
52
53 pub fn max(&self) -> usize {
55 self.start.max(self.end)
56 }
57
58 pub fn contains(&self, index: usize) -> bool {
60 index >= self.min() && index < self.max()
61 }
62
63 pub fn coerce_in(&self, max: usize) -> Self {
65 Self {
66 start: self.start.min(max),
67 end: self.end.min(max),
68 }
69 }
70
71 pub const fn all(length: usize) -> Self {
73 Self {
74 start: 0,
75 end: length,
76 }
77 }
78
79 pub fn safe_slice<'a>(&self, text: &'a str) -> &'a str {
87 if text.is_empty() {
88 return "";
89 }
90
91 let start = self.min().min(text.len());
92 let end = self.max().min(text.len());
93
94 let start = if text.is_char_boundary(start) {
95 start
96 } else {
97 (0..start)
98 .rev()
99 .find(|&i| text.is_char_boundary(i))
100 .unwrap_or(0)
101 };
102
103 let end = if text.is_char_boundary(end) {
104 end
105 } else {
106 (end..=text.len())
107 .find(|&i| text.is_char_boundary(i))
108 .unwrap_or(text.len())
109 };
110
111 if start <= end { &text[start..end] } else { "" }
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn cursor_is_collapsed() {
121 let cursor = TextRange::cursor(5);
122 assert!(cursor.collapsed());
123 assert_eq!(cursor.length(), 0);
124 assert_eq!(cursor.start, 5);
125 assert_eq!(cursor.end, 5);
126 }
127
128 #[test]
129 fn selection_is_not_collapsed() {
130 let selection = TextRange::new(2, 7);
131 assert!(!selection.collapsed());
132 assert_eq!(selection.length(), 5);
133 }
134
135 #[test]
136 fn reverse_selection_length() {
137 let reverse = TextRange::new(7, 2);
138 assert_eq!(reverse.length(), 5);
139 assert_eq!(reverse.min(), 2);
140 assert_eq!(reverse.max(), 7);
141 }
142
143 #[test]
144 fn coerce_in_bounds() {
145 let range = TextRange::new(5, 100);
146 let coerced = range.coerce_in(10);
147 assert_eq!(coerced.start, 5);
148 assert_eq!(coerced.end, 10);
149 }
150
151 #[test]
152 fn contains_index() {
153 let range = TextRange::new(2, 5);
154 assert!(!range.contains(1));
155 assert!(range.contains(2));
156 assert!(range.contains(3));
157 assert!(range.contains(4));
158 assert!(!range.contains(5));
159 }
160
161 #[test]
162 fn safe_slice_basic() {
163 let range = TextRange::new(0, 5);
164 assert_eq!(range.safe_slice("Hello World"), "Hello");
165 }
166
167 #[test]
168 fn safe_slice_beyond_bounds() {
169 let range = TextRange::new(0, 100);
170 assert_eq!(range.safe_slice("Hello"), "Hello");
171 }
172
173 #[test]
174 fn safe_slice_unicode() {
175 let text = "Hello 🌍";
176 let range = TextRange::new(0, 7);
177 let slice = range.safe_slice(text);
178 assert!(slice == "Hello " || slice == "Hello 🌍");
179 }
180}