1use inset_embedder::TextRange;
7use inset_foundation::App;
8use unicode_segmentation::UnicodeSegmentation;
9
10use crate::text_layout_metrics::TextLayoutMetrics;
11
12pub type UntilPredicate = dyn Fn(i32, bool) -> bool;
15
16pub trait TextBoundary {
24 fn get_leading_text_boundary_at(&self, app: &mut App, position: i32) -> Option<i32> {
32 if position < 0 {
33 return None;
34 }
35 let start = self.get_text_boundary_at(app, position).start;
36 (start >= 0).then_some(start)
37 }
38
39 fn get_trailing_text_boundary_at(&self, app: &mut App, position: i32) -> Option<i32> {
47 let end = self.get_text_boundary_at(app, position.max(0)).end;
48 (end >= 0).then_some(end)
49 }
50
51 fn get_text_boundary_at(&self, app: &mut App, position: i32) -> TextRange {
56 let start = self
57 .get_leading_text_boundary_at(app, position)
58 .unwrap_or(-1);
59 let end = self
60 .get_trailing_text_boundary_at(app, position)
61 .unwrap_or(-1);
62 TextRange::new(start, end)
63 }
64}
65
66#[derive(Clone, Debug)]
72pub struct CharacterBoundary {
73 text: String,
74}
75
76impl CharacterBoundary {
77 pub fn new(text: impl Into<String>) -> CharacterBoundary {
79 CharacterBoundary { text: text.into() }
80 }
81}
82
83impl TextBoundary for CharacterBoundary {
84 fn get_leading_text_boundary_at(&self, _app: &mut App, position: i32) -> Option<i32> {
85 if position < 0 {
86 return None;
87 }
88 let grapheme_start = CharacterRange::at(&self.text, position.min(utf16_len(&self.text)))
89 .string_before_length;
90 debug_assert!(CharacterRange::at(&self.text, grapheme_start).is_empty());
91 Some(grapheme_start)
92 }
93
94 fn get_trailing_text_boundary_at(&self, _app: &mut App, position: i32) -> Option<i32> {
95 if position >= utf16_len(&self.text) {
96 return None;
97 }
98 let range_at_position = CharacterRange::at(&self.text, (position + 1).max(0));
99 let next_boundary =
100 range_at_position.string_before_length + range_at_position.current_length;
101 debug_assert!(
102 next_boundary == utf16_len(&self.text)
103 || CharacterRange::at(&self.text, next_boundary).is_empty()
104 );
105 Some(next_boundary)
106 }
107
108 fn get_text_boundary_at(&self, app: &mut App, position: i32) -> TextRange {
109 if position < 0 {
110 return TextRange::new(
111 -1,
112 self.get_trailing_text_boundary_at(app, position)
113 .unwrap_or(-1),
114 );
115 } else if position >= utf16_len(&self.text) {
116 return TextRange::new(
117 self.get_leading_text_boundary_at(app, position)
118 .unwrap_or(-1),
119 -1,
120 );
121 }
122 let range_at_position = CharacterRange::at(&self.text, position);
123 if range_at_position.is_empty() {
124 TextRange::new(
126 range_at_position.string_before_length,
127 self.get_trailing_text_boundary_at(app, position)
128 .unwrap_or(-1),
129 )
130 } else {
131 TextRange::new(
132 range_at_position.string_before_length,
133 range_at_position.string_before_length + range_at_position.current_length,
134 )
135 }
136 }
137}
138
139#[derive(Clone, Debug)]
144pub struct ParagraphBoundary {
145 text: Vec<u16>,
147}
148
149impl ParagraphBoundary {
150 pub fn new(text: &str) -> ParagraphBoundary {
152 ParagraphBoundary {
153 text: text.encode_utf16().collect(),
154 }
155 }
156
157 fn code_unit_at(&self, index: i32) -> i32 {
158 i32::from(self.text[index as usize])
159 }
160
161 fn len(&self) -> i32 {
162 self.text.len() as i32
163 }
164}
165
166impl TextBoundary for ParagraphBoundary {
167 fn get_leading_text_boundary_at(&self, _app: &mut App, position: i32) -> Option<i32> {
171 if position < 0 || self.text.is_empty() {
172 return None;
173 }
174
175 if position >= self.len() {
176 return Some(self.len());
177 }
178
179 if position == 0 {
180 return Some(0);
181 }
182
183 let mut index = position;
184
185 if index > 1 && self.code_unit_at(index) == 0x0A && self.code_unit_at(index - 1) == 0x0D {
186 index -= 2;
187 } else if TextLayoutMetrics::is_line_terminator(self.code_unit_at(index)) {
188 index -= 1;
189 }
190
191 while index > 0 {
192 if TextLayoutMetrics::is_line_terminator(self.code_unit_at(index)) {
193 return Some(index + 1);
194 }
195 index -= 1;
196 }
197
198 Some(index.max(0))
199 }
200
201 fn get_trailing_text_boundary_at(&self, _app: &mut App, position: i32) -> Option<i32> {
206 if position >= self.len() || self.text.is_empty() {
207 return None;
208 }
209
210 if position < 0 {
211 return Some(0);
212 }
213
214 let mut index = position;
215
216 while !TextLayoutMetrics::is_line_terminator(self.code_unit_at(index)) {
217 index += 1;
218 if index == self.len() {
219 return Some(index);
220 }
221 }
222
223 Some(
224 if index < self.len() - 1
225 && self.code_unit_at(index) == 0x0D
226 && self.code_unit_at(index + 1) == 0x0A
227 {
228 index + 2
229 } else {
230 index + 1
231 },
232 )
233 }
234}
235
236#[derive(Clone, Debug)]
238pub struct DocumentBoundary {
239 text: String,
240}
241
242impl DocumentBoundary {
243 pub fn new(text: impl Into<String>) -> DocumentBoundary {
245 DocumentBoundary { text: text.into() }
246 }
247}
248
249impl TextBoundary for DocumentBoundary {
250 fn get_leading_text_boundary_at(&self, _app: &mut App, position: i32) -> Option<i32> {
251 (position >= 0).then_some(0)
252 }
253
254 fn get_trailing_text_boundary_at(&self, _app: &mut App, position: i32) -> Option<i32> {
255 let length = utf16_len(&self.text);
256 (position < length).then_some(length)
257 }
258}
259
260fn utf16_len(text: &str) -> i32 {
262 text.encode_utf16().count() as i32
263}
264
265struct CharacterRange {
269 string_before_length: i32,
271 current_length: i32,
273}
274
275impl CharacterRange {
276 fn at(text: &str, position: i32) -> CharacterRange {
277 let mut start = 0;
278 for cluster in text.graphemes(true) {
279 if position <= start {
280 break;
281 }
282 let length = utf16_len(cluster);
283 if position < start + length {
284 return CharacterRange {
285 string_before_length: start,
286 current_length: length,
287 };
288 }
289 start += length;
290 }
291 CharacterRange {
292 string_before_length: start,
293 current_length: 0,
294 }
295 }
296
297 fn is_empty(&self) -> bool {
298 self.current_length == 0
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use inset_foundation::{App, AppCell};
305
306 use super::{CharacterBoundary, DocumentBoundary, ParagraphBoundary, TextBoundary};
307
308 fn leading(app: &mut App, boundary: &dyn TextBoundary, position: i32) -> Option<i32> {
310 boundary.get_leading_text_boundary_at(app, position)
311 }
312
313 fn trailing(app: &mut App, boundary: &dyn TextBoundary, position: i32) -> Option<i32> {
314 boundary.get_trailing_text_boundary_at(app, position)
315 }
316
317 fn assert_consistent_within_range(app: &mut App, boundary: &dyn TextBoundary, length: i32) {
320 for position in -1..=length {
321 let range = boundary.get_text_boundary_at(app, position);
322 assert_eq!(
323 range.start,
324 leading(app, boundary, position).unwrap_or(-1),
325 "leading disagrees at {position}"
326 );
327 assert_eq!(
328 range.end,
329 trailing(app, boundary, position).unwrap_or(-1),
330 "trailing disagrees at {position}"
331 );
332 }
333 }
334
335 #[test]
336 fn character_boundary_works() {
337 let cell = AppCell::new();
338 let mut app = cell.borrow_mut();
339 let boundary = CharacterBoundary::new("abc");
340 assert_consistent_within_range(&mut app, &boundary, 3);
341
342 assert_eq!(leading(&mut app, &boundary, -1), None);
343 assert_eq!(trailing(&mut app, &boundary, -1), Some(0));
344
345 assert_eq!(leading(&mut app, &boundary, 0), Some(0));
346 assert_eq!(trailing(&mut app, &boundary, 0), Some(1));
347
348 assert_eq!(leading(&mut app, &boundary, 1), Some(1));
349 assert_eq!(trailing(&mut app, &boundary, 1), Some(2));
350
351 assert_eq!(leading(&mut app, &boundary, 3), Some(3));
352 assert_eq!(trailing(&mut app, &boundary, 3), None);
353
354 assert_eq!(leading(&mut app, &boundary, 4), Some(3));
355 assert_eq!(trailing(&mut app, &boundary, 4), None);
356 }
357
358 #[test]
359 fn character_boundary_works_with_grapheme() {
360 let cell = AppCell::new();
361 let mut app = cell.borrow_mut();
362 let text = "a\u{2744}\u{FE0E}c";
365 let boundary = CharacterBoundary::new(text);
366 let length = text.encode_utf16().count() as i32;
367 assert_eq!(length, 4);
368 assert_consistent_within_range(&mut app, &boundary, length);
369
370 assert_eq!(leading(&mut app, &boundary, -1), None);
371 assert_eq!(trailing(&mut app, &boundary, -1), Some(0));
372
373 assert_eq!(leading(&mut app, &boundary, 0), Some(0));
374 assert_eq!(trailing(&mut app, &boundary, 0), Some(1));
375
376 assert_eq!(leading(&mut app, &boundary, 1), Some(1));
377 assert_eq!(trailing(&mut app, &boundary, 1), Some(3));
378
379 assert_eq!(leading(&mut app, &boundary, 2), Some(1));
380 assert_eq!(trailing(&mut app, &boundary, 2), Some(3));
381
382 assert_eq!(leading(&mut app, &boundary, 3), Some(3));
383 assert_eq!(trailing(&mut app, &boundary, 3), Some(4));
384
385 assert_eq!(leading(&mut app, &boundary, length), Some(length));
386 assert_eq!(trailing(&mut app, &boundary, length), None);
387 }
388
389 #[test]
390 fn paragraph_boundary_works_for_simple_cases() {
391 let cell = AppCell::new();
392 let mut app = cell.borrow_mut();
393 let position = 10;
395
396 let boundary = ParagraphBoundary::new("abcd efg hi\njklmno\npqrstuv");
397 assert_eq!(leading(&mut app, &boundary, position), Some(0));
399 assert_eq!(trailing(&mut app, &boundary, position), Some(12));
400
401 let boundary = ParagraphBoundary::new("abcd efg hi\r\njklmno\npqrstuv");
403 assert_eq!(leading(&mut app, &boundary, position), Some(0));
404 assert_eq!(trailing(&mut app, &boundary, position), Some(13));
405 }
406
407 #[test]
408 fn paragraph_boundary_works_for_consecutive_line_terminators_involving_crlf() {
409 let cell = AppCell::new();
410 let mut app = cell.borrow_mut();
411 let boundary = ParagraphBoundary::new(concat!(
412 "Now is the time for\n", "all good people\n\r\n", "to come to the aid\n", "of their country.", ));
417 assert_eq!(leading(&mut app, &boundary, 56), Some(38));
418 assert_eq!(trailing(&mut app, &boundary, 56), Some(57));
419 assert_eq!(leading(&mut app, &boundary, 38), Some(38));
420 assert_eq!(trailing(&mut app, &boundary, 38), Some(57));
421 assert_eq!(leading(&mut app, &boundary, 37), Some(36));
422 assert_eq!(trailing(&mut app, &boundary, 37), Some(38));
423 }
424
425 #[test]
426 fn paragraph_boundary_works_when_position_is_between_two_crlf() {
427 let cell = AppCell::new();
428 let mut app = cell.borrow_mut();
429 let boundary = ParagraphBoundary::new("abcd efg hi\r\nhello\r\n\n");
430 assert_eq!(leading(&mut app, &boundary, 16), Some(13));
431 assert_eq!(trailing(&mut app, &boundary, 16), Some(20));
432 }
433
434 #[test]
435 fn document_boundary_works() {
436 let cell = AppCell::new();
437 let mut app = cell.borrow_mut();
438 let text = "abcd efg hi\njklmno\npqrstuv";
439 let length = text.len() as i32;
440 let boundary = DocumentBoundary::new(text);
441 assert_consistent_within_range(&mut app, &boundary, length);
442
443 assert_eq!(leading(&mut app, &boundary, -1), None);
444 assert_eq!(trailing(&mut app, &boundary, -1), Some(length));
445
446 assert_eq!(leading(&mut app, &boundary, 0), Some(0));
447 assert_eq!(trailing(&mut app, &boundary, 0), Some(length));
448
449 assert_eq!(leading(&mut app, &boundary, 10), Some(0));
450 assert_eq!(trailing(&mut app, &boundary, 10), Some(length));
451
452 assert_eq!(leading(&mut app, &boundary, length), Some(0));
453 assert_eq!(trailing(&mut app, &boundary, length), None);
454
455 assert_eq!(leading(&mut app, &boundary, length + 1), Some(0));
456 assert_eq!(trailing(&mut app, &boundary, length + 1), None);
457 }
458}