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 { &text[start..end] } else { "" }
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn cursor_is_collapsed() {
129 let cursor = TextRange::cursor(5);
130 assert!(cursor.collapsed());
131 assert_eq!(cursor.length(), 0);
132 assert_eq!(cursor.start, 5);
133 assert_eq!(cursor.end, 5);
134 }
135
136 #[test]
137 fn selection_is_not_collapsed() {
138 let selection = TextRange::new(2, 7);
139 assert!(!selection.collapsed());
140 assert_eq!(selection.length(), 5);
141 }
142
143 #[test]
144 fn reverse_selection_length() {
145 let reverse = TextRange::new(7, 2);
146 assert_eq!(reverse.length(), 5);
147 assert_eq!(reverse.min(), 2);
148 assert_eq!(reverse.max(), 7);
149 }
150
151 #[test]
152 fn coerce_in_bounds() {
153 let range = TextRange::new(5, 100);
154 let coerced = range.coerce_in(10);
155 assert_eq!(coerced.start, 5);
156 assert_eq!(coerced.end, 10);
157 }
158
159 #[test]
160 fn contains_index() {
161 let range = TextRange::new(2, 5);
162 assert!(!range.contains(1));
163 assert!(range.contains(2));
164 assert!(range.contains(3));
165 assert!(range.contains(4));
166 assert!(!range.contains(5)); }
168
169 #[test]
170 fn safe_slice_basic() {
171 let range = TextRange::new(0, 5);
172 assert_eq!(range.safe_slice("Hello World"), "Hello");
173 }
174
175 #[test]
176 fn safe_slice_beyond_bounds() {
177 let range = TextRange::new(0, 100);
178 assert_eq!(range.safe_slice("Hello"), "Hello");
179 }
180
181 #[test]
182 fn safe_slice_unicode() {
183 let text = "Hello 🌍";
185 let range = TextRange::new(0, 7);
187 let slice = range.safe_slice(text);
188 assert!(slice == "Hello " || slice == "Hello 🌍");
190 }
191}