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