1use std::{cmp::Ordering, ops::Range};
2
3use editor::{
4 DisplayPoint, MultiBufferOffset,
5 display_map::{DisplaySnapshot, ToDisplayPoint},
6 movement,
7};
8use language::{CharClassifier, CharKind};
9use text::Bias;
10
11use crate::helix::object::HelixTextObject;
12
13/// Text objects (after helix definition) that can easily be
14/// found by reading a buffer and comparing two neighboring chars
15/// until a start / end is found
16trait BoundedObject {
17 /// The next start since `from` (inclusive).
18 /// If outer is true it is the start of "a" object (m a) rather than "inner" object (m i).
19 fn next_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset>;
20 /// The next end since `from` (inclusive).
21 /// If outer is true it is the end of "a" object (m a) rather than "inner" object (m i).
22 fn next_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset>;
23 /// The previous start since `from` (inclusive).
24 /// If outer is true it is the start of "a" object (m a) rather than "inner" object (m i).
25 fn previous_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset>;
26 /// The previous end since `from` (inclusive).
27 /// If outer is true it is the end of "a" object (m a) rather than "inner" object (m i).
28 fn previous_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset>;
29
30 /// Whether the range inside the object can be zero characters wide.
31 /// If so, the trait assumes that these ranges can't be directly adjacent to each other.
32 fn inner_range_can_be_zero_width(&self) -> bool;
33 /// Whether the "ma" can exceed the "mi" range on both sides at the same time
34 fn surround_on_both_sides(&self) -> bool;
35 /// Whether the outer range of an object could overlap with the outer range of the neighboring
36 /// object. If so, they can't be nested.
37 fn ambiguous_outer(&self) -> bool;
38
39 fn can_be_zero_width(&self, around: bool) -> bool {
40 if around {
41 false
42 } else {
43 self.inner_range_can_be_zero_width()
44 }
45 }
46
47 /// Switches from an "mi" range to an "ma" one.
48 /// Assumes the inner range is valid.
49 fn around(&self, map: &DisplaySnapshot, inner_range: Range<Offset>) -> Range<Offset> {
50 if self.surround_on_both_sides() {
51 let start = self
52 .previous_start(map, inner_range.start, true)
53 .unwrap_or(inner_range.start);
54 let end = self
55 .next_end(map, inner_range.end, true)
56 .unwrap_or(inner_range.end);
57
58 return start..end;
59 }
60
61 let mut start = inner_range.start;
62 let end = self
63 .next_end(map, inner_range.end, true)
64 .unwrap_or(inner_range.end);
65 if end == inner_range.end {
66 start = self
67 .previous_start(map, inner_range.start, true)
68 .unwrap_or(inner_range.start)
69 }
70
71 start..end
72 }
73 /// Switches from an "ma" range to an "mi" one.
74 /// Assumes the inner range is valid.
75 fn inside(&self, map: &DisplaySnapshot, outer_range: Range<Offset>) -> Range<Offset> {
76 let inner_start = self
77 .next_start(map, outer_range.start, false)
78 .unwrap_or_else(|| {
79 log::warn!("The motion might not have found the text object correctly");
80 outer_range.start
81 });
82 let inner_end = self
83 .previous_end(map, outer_range.end, false)
84 .unwrap_or_else(|| {
85 log::warn!("The motion might not have found the text object correctly");
86 outer_range.end
87 });
88 inner_start..inner_end
89 }
90
91 /// The next end since `start` (inclusive) on the same nesting level.
92 fn close_at_end(&self, start: Offset, map: &DisplaySnapshot, outer: bool) -> Option<Offset> {
93 let mut end_search_start = if self.can_be_zero_width(outer) {
94 start
95 } else {
96 start.next(map)?
97 };
98 let mut start_search_start = start.next(map)?;
99
100 loop {
101 let next_end = self.next_end(map, end_search_start, outer)?;
102 let maybe_next_start = self.next_start(map, start_search_start, outer);
103 if let Some(next_start) = maybe_next_start
104 && (next_start.0 < next_end.0
105 || next_start.0 == next_end.0 && self.can_be_zero_width(outer))
106 && !self.ambiguous_outer()
107 {
108 let closing = self.close_at_end(next_start, map, outer)?;
109 end_search_start = closing.next(map)?;
110 start_search_start = if self.can_be_zero_width(outer) {
111 closing.next(map)?
112 } else {
113 closing
114 };
115 } else {
116 return Some(next_end);
117 }
118 }
119 }
120 /// The previous start since `end` (inclusive) on the same nesting level.
121 fn close_at_start(&self, end: Offset, map: &DisplaySnapshot, outer: bool) -> Option<Offset> {
122 let mut start_search_end = if self.can_be_zero_width(outer) {
123 end
124 } else {
125 end.previous(map)?
126 };
127 let mut end_search_end = end.previous(map)?;
128
129 loop {
130 let previous_start = self.previous_start(map, start_search_end, outer)?;
131 let maybe_previous_end = self.previous_end(map, end_search_end, outer);
132 if let Some(previous_end) = maybe_previous_end
133 && (previous_end.0 > previous_start.0
134 || previous_end.0 == previous_start.0 && self.can_be_zero_width(outer))
135 && !self.ambiguous_outer()
136 {
137 let closing = self.close_at_start(previous_end, map, outer)?;
138 start_search_end = closing.previous(map)?;
139 end_search_end = if self.can_be_zero_width(outer) {
140 closing.previous(map)?
141 } else {
142 closing
143 };
144 } else {
145 return Some(previous_start);
146 }
147 }
148 }
149}
150
151#[derive(Clone, Copy, PartialEq, Debug, PartialOrd, Ord, Eq)]
152struct Offset(MultiBufferOffset);
153impl Offset {
154 fn next(self, map: &DisplaySnapshot) -> Option<Self> {
155 let next = Self(
156 map.buffer_snapshot()
157 .clip_offset(self.0 + 1usize, Bias::Right),
158 );
159 (next.0 > self.0).then(|| next)
160 }
161 fn previous(self, map: &DisplaySnapshot) -> Option<Self> {
162 if self.0 == MultiBufferOffset(0) {
163 return None;
164 }
165 Some(Self(
166 map.buffer_snapshot().clip_offset(self.0 - 1, Bias::Left),
167 ))
168 }
169 fn range(
170 start: (DisplayPoint, Bias),
171 end: (DisplayPoint, Bias),
172 map: &DisplaySnapshot,
173 ) -> Range<Self> {
174 Self(start.0.to_offset(map, start.1))..Self(end.0.to_offset(map, end.1))
175 }
176}
177
178impl<B: BoundedObject> HelixTextObject for B {
179 fn range(
180 &self,
181 map: &DisplaySnapshot,
182 relative_to: Range<DisplayPoint>,
183 around: bool,
184 ) -> Option<Range<DisplayPoint>> {
185 let relative_to = Offset::range(
186 (relative_to.start, Bias::Left),
187 (relative_to.end, Bias::Left),
188 map,
189 );
190
191 relative_range(self, around, map, |find_outer| {
192 let search_start = if self.can_be_zero_width(find_outer) {
193 relative_to.end
194 } else {
195 // If the objects can be directly next to each other an object end the
196 // cursor (relative_to) end would not count for close_at_end, so the search
197 // needs to start one character to the left.
198 relative_to.end.previous(map)?
199 };
200 let max_end = self.close_at_end(search_start, map, find_outer)?;
201 let min_start = self.close_at_start(max_end, map, find_outer)?;
202
203 (min_start <= relative_to.start).then(|| min_start..max_end)
204 })
205 }
206
207 fn next_range(
208 &self,
209 map: &DisplaySnapshot,
210 relative_to: Range<DisplayPoint>,
211 around: bool,
212 ) -> Option<Range<DisplayPoint>> {
213 let relative_to = Offset::range(
214 (relative_to.start, Bias::Left),
215 (relative_to.end, Bias::Left),
216 map,
217 );
218
219 relative_range(self, around, map, |find_outer| {
220 let min_start = self.next_start(map, relative_to.end, find_outer)?;
221 let max_end = self.close_at_end(min_start, map, find_outer)?;
222
223 Some(min_start..max_end)
224 })
225 }
226
227 fn previous_range(
228 &self,
229 map: &DisplaySnapshot,
230 relative_to: Range<DisplayPoint>,
231 around: bool,
232 ) -> Option<Range<DisplayPoint>> {
233 let relative_to = Offset::range(
234 (relative_to.start, Bias::Left),
235 (relative_to.end, Bias::Left),
236 map,
237 );
238
239 relative_range(self, around, map, |find_outer| {
240 let max_end = self.previous_end(map, relative_to.start, find_outer)?;
241 let min_start = self.close_at_start(max_end, map, find_outer)?;
242
243 Some(min_start..max_end)
244 })
245 }
246}
247
248fn relative_range<B: BoundedObject>(
249 object: &B,
250 outer: bool,
251 map: &DisplaySnapshot,
252 find_range: impl Fn(bool) -> Option<Range<Offset>>,
253) -> Option<Range<DisplayPoint>> {
254 // The cursor could be inside the outer range, but not the inner range.
255 // Whether that should count as found.
256 let find_outer = object.surround_on_both_sides() && !object.ambiguous_outer();
257 let range = find_range(find_outer)?;
258 let min_start = range.start;
259 let max_end = range.end;
260
261 let wanted_range = if outer && !find_outer {
262 // max_end is not yet the outer end
263 object.around(map, min_start..max_end)
264 } else if !outer && find_outer {
265 // max_end is the outer end, but the final result should have the inner end
266 object.inside(map, min_start..max_end)
267 } else {
268 min_start..max_end
269 };
270
271 let start = wanted_range.start.0.to_display_point(map);
272 let end = wanted_range.end.0.to_display_point(map);
273
274 Some(start..end)
275}
276
277/// A textobject whose boundaries can easily be found between two chars
278pub enum ImmediateBoundary {
279 Word { ignore_punctuation: bool },
280 Subword { ignore_punctuation: bool },
281 AngleBrackets,
282 BackQuotes,
283 CurlyBrackets,
284 DoubleQuotes,
285 Parentheses,
286 SingleQuotes,
287 SquareBrackets,
288 VerticalBars,
289}
290
291/// A textobject whose start and end can be found from an easy-to-find
292/// boundary between two chars by following a simple path from there
293pub enum FuzzyBoundary {
294 Sentence,
295 Paragraph,
296}
297
298impl ImmediateBoundary {
299 fn is_inner_start(&self, left: char, right: char, classifier: CharClassifier) -> bool {
300 match self {
301 Self::Word { ignore_punctuation } => {
302 let classifier = classifier.ignore_punctuation(*ignore_punctuation);
303 is_word_start(left, right, &classifier)
304 || (is_buffer_start(left) && classifier.kind(right) != CharKind::Whitespace)
305 }
306 Self::Subword { ignore_punctuation } => {
307 let classifier = classifier.ignore_punctuation(*ignore_punctuation);
308 movement::is_subword_start(left, right, &classifier)
309 || (is_buffer_start(left) && classifier.kind(right) != CharKind::Whitespace)
310 }
311 Self::AngleBrackets => left == '<',
312 Self::BackQuotes => left == '`',
313 Self::CurlyBrackets => left == '{',
314 Self::DoubleQuotes => left == '"',
315 Self::Parentheses => left == '(',
316 Self::SingleQuotes => left == '\'',
317 Self::SquareBrackets => left == '[',
318 Self::VerticalBars => left == '|',
319 }
320 }
321 fn is_inner_end(&self, left: char, right: char, classifier: CharClassifier) -> bool {
322 match self {
323 Self::Word { ignore_punctuation } => {
324 let classifier = classifier.ignore_punctuation(*ignore_punctuation);
325 is_word_end(left, right, &classifier)
326 || (is_buffer_end(right) && classifier.kind(left) != CharKind::Whitespace)
327 }
328 Self::Subword { ignore_punctuation } => {
329 let classifier = classifier.ignore_punctuation(*ignore_punctuation);
330 movement::is_subword_start(left, right, &classifier)
331 || (is_buffer_end(right) && classifier.kind(left) != CharKind::Whitespace)
332 }
333 Self::AngleBrackets => right == '>',
334 Self::BackQuotes => right == '`',
335 Self::CurlyBrackets => right == '}',
336 Self::DoubleQuotes => right == '"',
337 Self::Parentheses => right == ')',
338 Self::SingleQuotes => right == '\'',
339 Self::SquareBrackets => right == ']',
340 Self::VerticalBars => right == '|',
341 }
342 }
343 fn is_outer_start(&self, left: char, right: char, classifier: CharClassifier) -> bool {
344 match self {
345 word @ Self::Word { .. } => word.is_inner_end(left, right, classifier) || left == '\n',
346 subword @ Self::Subword { .. } => {
347 subword.is_inner_end(left, right, classifier) || left == '\n'
348 }
349 Self::AngleBrackets => right == '<',
350 Self::BackQuotes => right == '`',
351 Self::CurlyBrackets => right == '{',
352 Self::DoubleQuotes => right == '"',
353 Self::Parentheses => right == '(',
354 Self::SingleQuotes => right == '\'',
355 Self::SquareBrackets => right == '[',
356 Self::VerticalBars => right == '|',
357 }
358 }
359 fn is_outer_end(&self, left: char, right: char, classifier: CharClassifier) -> bool {
360 match self {
361 word @ Self::Word { .. } => {
362 word.is_inner_start(left, right, classifier) || right == '\n'
363 }
364 subword @ Self::Subword { .. } => {
365 subword.is_inner_start(left, right, classifier) || right == '\n'
366 }
367 Self::AngleBrackets => left == '>',
368 Self::BackQuotes => left == '`',
369 Self::CurlyBrackets => left == '}',
370 Self::DoubleQuotes => left == '"',
371 Self::Parentheses => left == ')',
372 Self::SingleQuotes => left == '\'',
373 Self::SquareBrackets => left == ']',
374 Self::VerticalBars => left == '|',
375 }
376 }
377}
378
379impl BoundedObject for ImmediateBoundary {
380 fn next_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
381 try_find_boundary(map, from, |left, right| {
382 let classifier = map.buffer_snapshot().char_classifier_at(from.0);
383 if outer {
384 self.is_outer_start(left, right, classifier)
385 } else {
386 self.is_inner_start(left, right, classifier)
387 }
388 })
389 }
390 fn next_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
391 try_find_boundary(map, from, |left, right| {
392 let classifier = map.buffer_snapshot().char_classifier_at(from.0);
393 if outer {
394 self.is_outer_end(left, right, classifier)
395 } else {
396 self.is_inner_end(left, right, classifier)
397 }
398 })
399 }
400 fn previous_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
401 try_find_preceding_boundary(map, from, |left, right| {
402 let classifier = map.buffer_snapshot().char_classifier_at(from.0);
403 if outer {
404 self.is_outer_start(left, right, classifier)
405 } else {
406 self.is_inner_start(left, right, classifier)
407 }
408 })
409 }
410 fn previous_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
411 try_find_preceding_boundary(map, from, |left, right| {
412 let classifier = map.buffer_snapshot().char_classifier_at(from.0);
413 if outer {
414 self.is_outer_end(left, right, classifier)
415 } else {
416 self.is_inner_end(left, right, classifier)
417 }
418 })
419 }
420 fn inner_range_can_be_zero_width(&self) -> bool {
421 match self {
422 Self::Subword { .. } | Self::Word { .. } => false,
423 _ => true,
424 }
425 }
426 fn surround_on_both_sides(&self) -> bool {
427 match self {
428 Self::Subword { .. } | Self::Word { .. } => false,
429 _ => true,
430 }
431 }
432 fn ambiguous_outer(&self) -> bool {
433 match self {
434 Self::BackQuotes
435 | Self::DoubleQuotes
436 | Self::SingleQuotes
437 | Self::VerticalBars
438 | Self::Subword { .. }
439 | Self::Word { .. } => true,
440 _ => false,
441 }
442 }
443}
444
445impl FuzzyBoundary {
446 /// When between two chars that form an easy-to-find identifier boundary,
447 /// what's the way to get to the actual start of the object, if any
448 fn is_near_potential_inner_start<'a>(
449 &self,
450 left: char,
451 right: char,
452 classifier: &CharClassifier,
453 ) -> Option<Box<dyn Fn(Offset, &'a DisplaySnapshot) -> Option<Offset>>> {
454 if is_buffer_start(left) {
455 return Some(Box::new(|identifier, _| Some(identifier)));
456 }
457 match self {
458 Self::Paragraph => {
459 if left != '\n' || right != '\n' {
460 return None;
461 }
462 Some(Box::new(|identifier, map| {
463 try_find_boundary(map, identifier, |left, right| left == '\n' && right != '\n')
464 }))
465 }
466 Self::Sentence => {
467 if let Some(find_paragraph_start) =
468 Self::Paragraph.is_near_potential_inner_start(left, right, classifier)
469 {
470 return Some(find_paragraph_start);
471 } else if !is_sentence_end(left, right, classifier) {
472 return None;
473 }
474 Some(Box::new(|identifier, map| {
475 let word = ImmediateBoundary::Word {
476 ignore_punctuation: false,
477 };
478 word.next_start(map, identifier, false)
479 }))
480 }
481 }
482 }
483 /// When between two chars that form an easy-to-find identifier boundary,
484 /// what's the way to get to the actual end of the object, if any
485 fn is_near_potential_inner_end<'a>(
486 &self,
487 left: char,
488 right: char,
489 classifier: &CharClassifier,
490 ) -> Option<Box<dyn Fn(Offset, &'a DisplaySnapshot) -> Option<Offset>>> {
491 if is_buffer_end(right) {
492 return Some(Box::new(|identifier, _| Some(identifier)));
493 }
494 match self {
495 Self::Paragraph => {
496 if left != '\n' || right != '\n' {
497 return None;
498 }
499 Some(Box::new(|identifier, map| {
500 try_find_preceding_boundary(map, identifier, |left, right| {
501 left != '\n' && right == '\n'
502 })
503 }))
504 }
505 Self::Sentence => {
506 if let Some(find_paragraph_end) =
507 Self::Paragraph.is_near_potential_inner_end(left, right, classifier)
508 {
509 return Some(find_paragraph_end);
510 } else if !is_sentence_end(left, right, classifier) {
511 return None;
512 }
513 Some(Box::new(|identifier, _| Some(identifier)))
514 }
515 }
516 }
517 /// When between two chars that form an easy-to-find identifier boundary,
518 /// what's the way to get to the actual end of the object, if any
519 fn is_near_potential_outer_start<'a>(
520 &self,
521 left: char,
522 right: char,
523 classifier: &CharClassifier,
524 ) -> Option<Box<dyn Fn(Offset, &'a DisplaySnapshot) -> Option<Offset>>> {
525 match self {
526 paragraph @ Self::Paragraph => {
527 paragraph.is_near_potential_inner_end(left, right, classifier)
528 }
529 sentence @ Self::Sentence => {
530 sentence.is_near_potential_inner_end(left, right, classifier)
531 }
532 }
533 }
534 /// When between two chars that form an easy-to-find identifier boundary,
535 /// what's the way to get to the actual end of the object, if any
536 fn is_near_potential_outer_end<'a>(
537 &self,
538 left: char,
539 right: char,
540 classifier: &CharClassifier,
541 ) -> Option<Box<dyn Fn(Offset, &'a DisplaySnapshot) -> Option<Offset>>> {
542 match self {
543 paragraph @ Self::Paragraph => {
544 paragraph.is_near_potential_inner_start(left, right, classifier)
545 }
546 sentence @ Self::Sentence => {
547 sentence.is_near_potential_inner_start(left, right, classifier)
548 }
549 }
550 }
551
552 // The boundary can be on the other side of `from` than the identifier, so the search needs to go both ways.
553 // Also, the distance (and direction) between identifier and boundary could vary, so a few ones need to be
554 // compared, even if one boundary was already found on the right side of `from`.
555 fn to_boundary(
556 &self,
557 map: &DisplaySnapshot,
558 from: Offset,
559 outer: bool,
560 backward: bool,
561 boundary_kind: Boundary,
562 ) -> Option<Offset> {
563 let generate_boundary_data = |left, right, point: Offset| {
564 let classifier = map.buffer_snapshot().char_classifier_at(from.0);
565 let reach_boundary = if outer && boundary_kind == Boundary::Start {
566 self.is_near_potential_outer_start(left, right, &classifier)
567 } else if !outer && boundary_kind == Boundary::Start {
568 self.is_near_potential_inner_start(left, right, &classifier)
569 } else if outer && boundary_kind == Boundary::End {
570 self.is_near_potential_outer_end(left, right, &classifier)
571 } else {
572 self.is_near_potential_inner_end(left, right, &classifier)
573 };
574
575 reach_boundary.map(|reach_start| (point, reach_start))
576 };
577
578 let forwards = try_find_boundary_data(map, from, generate_boundary_data);
579 let backwards = try_find_preceding_boundary_data(map, from, generate_boundary_data);
580 let boundaries = [forwards, backwards]
581 .into_iter()
582 .flatten()
583 .filter_map(|(identifier, reach_boundary)| reach_boundary(identifier, map))
584 .filter(|boundary| match boundary.cmp(&from) {
585 Ordering::Equal => true,
586 Ordering::Less => backward,
587 Ordering::Greater => !backward,
588 });
589 if backward {
590 boundaries.max_by_key(|boundary| *boundary)
591 } else {
592 boundaries.min_by_key(|boundary| *boundary)
593 }
594 }
595}
596
597#[derive(PartialEq)]
598enum Boundary {
599 Start,
600 End,
601}
602
603impl BoundedObject for FuzzyBoundary {
604 fn next_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
605 self.to_boundary(map, from, outer, false, Boundary::Start)
606 }
607 fn next_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
608 self.to_boundary(map, from, outer, false, Boundary::End)
609 }
610 fn previous_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
611 self.to_boundary(map, from, outer, true, Boundary::Start)
612 }
613 fn previous_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
614 self.to_boundary(map, from, outer, true, Boundary::End)
615 }
616 fn inner_range_can_be_zero_width(&self) -> bool {
617 false
618 }
619 fn surround_on_both_sides(&self) -> bool {
620 false
621 }
622 fn ambiguous_outer(&self) -> bool {
623 false
624 }
625}
626
627/// Returns the first boundary after or at `from` in text direction.
628/// The start and end of the file are the chars `'\0'`.
629fn try_find_boundary(
630 map: &DisplaySnapshot,
631 from: Offset,
632 is_boundary: impl Fn(char, char) -> bool,
633) -> Option<Offset> {
634 let boundary = try_find_boundary_data(map, from, |left, right, point| {
635 if is_boundary(left, right) {
636 Some(point)
637 } else {
638 None
639 }
640 })?;
641 Some(boundary)
642}
643
644/// Returns some information about it (of type `T`) as soon as
645/// there is a boundary after or at `from` in text direction
646/// The start and end of the file are the chars `'\0'`.
647fn try_find_boundary_data<T>(
648 map: &DisplaySnapshot,
649 mut from: Offset,
650 boundary_information: impl Fn(char, char, Offset) -> Option<T>,
651) -> Option<T> {
652 let mut prev_ch = map
653 .buffer_snapshot()
654 .reversed_chars_at(from.0)
655 .next()
656 .unwrap_or('\0');
657
658 for ch in map.buffer_snapshot().chars_at(from.0).chain(['\0']) {
659 if let Some(boundary_information) = boundary_information(prev_ch, ch, from) {
660 return Some(boundary_information);
661 }
662 from.0 += ch.len_utf8();
663 prev_ch = ch;
664 }
665
666 None
667}
668
669/// Returns the first boundary after or at `from` in text direction.
670/// The start and end of the file are the chars `'\0'`.
671fn try_find_preceding_boundary(
672 map: &DisplaySnapshot,
673 from: Offset,
674 is_boundary: impl Fn(char, char) -> bool,
675) -> Option<Offset> {
676 let boundary = try_find_preceding_boundary_data(map, from, |left, right, point| {
677 if is_boundary(left, right) {
678 Some(point)
679 } else {
680 None
681 }
682 })?;
683 Some(boundary)
684}
685
686/// Returns some information about it (of type `T`) as soon as
687/// there is a boundary before or at `from` in opposite text direction
688/// The start and end of the file are the chars `'\0'`.
689fn try_find_preceding_boundary_data<T>(
690 map: &DisplaySnapshot,
691 mut from: Offset,
692 is_boundary: impl Fn(char, char, Offset) -> Option<T>,
693) -> Option<T> {
694 let mut prev_ch = map
695 .buffer_snapshot()
696 .chars_at(from.0)
697 .next()
698 .unwrap_or('\0');
699
700 for ch in map
701 .buffer_snapshot()
702 .reversed_chars_at(from.0)
703 .chain(['\0'])
704 {
705 if let Some(boundary_information) = is_boundary(ch, prev_ch, from) {
706 return Some(boundary_information);
707 }
708 from.0.0 = from.0.0.saturating_sub(ch.len_utf8());
709 prev_ch = ch;
710 }
711
712 None
713}
714
715fn is_buffer_start(left: char) -> bool {
716 left == '\0'
717}
718
719fn is_buffer_end(right: char) -> bool {
720 right == '\0'
721}
722
723fn is_word_start(left: char, right: char, classifier: &CharClassifier) -> bool {
724 classifier.kind(left) != classifier.kind(right)
725 && classifier.kind(right) != CharKind::Whitespace
726}
727
728fn is_word_end(left: char, right: char, classifier: &CharClassifier) -> bool {
729 classifier.kind(left) != classifier.kind(right) && classifier.kind(left) != CharKind::Whitespace
730}
731
732fn is_sentence_end(left: char, right: char, classifier: &CharClassifier) -> bool {
733 const ENDS: [char; 1] = ['.'];
734
735 if classifier.kind(right) != CharKind::Whitespace {
736 return false;
737 }
738 ENDS.into_iter().any(|end| left == end)
739}