boundary.rs

  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| {
464                        left == '\n' && right != '\n'
465                    })
466                }))
467            }
468            Self::Sentence => {
469                if let Some(find_paragraph_start) =
470                    Self::Paragraph.is_near_potential_inner_start(left, right, classifier)
471                {
472                    return Some(find_paragraph_start);
473                } else if !is_sentence_end(left, right, classifier) {
474                    return None;
475                }
476                Some(Box::new(|identifier, map| {
477                    let word = ImmediateBoundary::Word {
478                        ignore_punctuation: false,
479                    };
480                    word.next_start(map, identifier, false)
481                }))
482            }
483        }
484    }
485    /// When between two chars that form an easy-to-find identifier boundary,
486    /// what's the way to get to the actual end of the object, if any
487    fn is_near_potential_inner_end<'a>(
488        &self,
489        left: char,
490        right: char,
491        classifier: &CharClassifier,
492    ) -> Option<Box<dyn Fn(Offset, &'a DisplaySnapshot) -> Option<Offset>>> {
493        if is_buffer_end(right) {
494            return Some(Box::new(|identifier, _| Some(identifier)));
495        }
496        match self {
497            Self::Paragraph => {
498                if left != '\n' || right != '\n' {
499                    return None;
500                }
501                Some(Box::new(|identifier, map| {
502                    try_find_preceding_boundary(map, identifier, &|left, right| {
503                        left != '\n' && right == '\n'
504                    })
505                }))
506            }
507            Self::Sentence => {
508                if let Some(find_paragraph_end) =
509                    Self::Paragraph.is_near_potential_inner_end(left, right, classifier)
510                {
511                    return Some(find_paragraph_end);
512                } else if !is_sentence_end(left, right, classifier) {
513                    return None;
514                }
515                Some(Box::new(|identifier, _| Some(identifier)))
516            }
517        }
518    }
519    /// When between two chars that form an easy-to-find identifier boundary,
520    /// what's the way to get to the actual end of the object, if any
521    fn is_near_potential_outer_start<'a>(
522        &self,
523        left: char,
524        right: char,
525        classifier: &CharClassifier,
526    ) -> Option<Box<dyn Fn(Offset, &'a DisplaySnapshot) -> Option<Offset>>> {
527        match self {
528            paragraph @ Self::Paragraph => {
529                paragraph.is_near_potential_inner_end(left, right, classifier)
530            }
531            sentence @ Self::Sentence => {
532                sentence.is_near_potential_inner_end(left, right, classifier)
533            }
534        }
535    }
536    /// When between two chars that form an easy-to-find identifier boundary,
537    /// what's the way to get to the actual end of the object, if any
538    fn is_near_potential_outer_end<'a>(
539        &self,
540        left: char,
541        right: char,
542        classifier: &CharClassifier,
543    ) -> Option<Box<dyn Fn(Offset, &'a DisplaySnapshot) -> Option<Offset>>> {
544        match self {
545            paragraph @ Self::Paragraph => {
546                paragraph.is_near_potential_inner_start(left, right, classifier)
547            }
548            sentence @ Self::Sentence => {
549                sentence.is_near_potential_inner_start(left, right, classifier)
550            }
551        }
552    }
553
554    // The boundary can be on the other side of `from` than the identifier, so the search needs to go both ways.
555    // Also, the distance (and direction) between identifier and boundary could vary, so a few ones need to be
556    // compared, even if one boundary was already found on the right side of `from`.
557    fn to_boundary(
558        &self,
559        map: &DisplaySnapshot,
560        from: Offset,
561        outer: bool,
562        backward: bool,
563        boundary_kind: Boundary,
564    ) -> Option<Offset> {
565        let generate_boundary_data = |left, right, point: Offset| {
566            let classifier = map.buffer_snapshot().char_classifier_at(from.0);
567            let reach_boundary = if outer && boundary_kind == Boundary::Start {
568                self.is_near_potential_outer_start(left, right, &classifier)
569            } else if !outer && boundary_kind == Boundary::Start {
570                self.is_near_potential_inner_start(left, right, &classifier)
571            } else if outer && boundary_kind == Boundary::End {
572                self.is_near_potential_outer_end(left, right, &classifier)
573            } else {
574                self.is_near_potential_inner_end(left, right, &classifier)
575            };
576
577            reach_boundary.map(|reach_start| (point, reach_start))
578        };
579
580        let forwards = try_find_boundary_data(map, from, generate_boundary_data);
581        let backwards = try_find_preceding_boundary_data(map, from, generate_boundary_data);
582        let boundaries = [forwards, backwards]
583            .into_iter()
584            .flatten()
585            .filter_map(|(identifier, reach_boundary)| reach_boundary(identifier, map))
586            .filter(|boundary| match boundary.cmp(&from) {
587                Ordering::Equal => true,
588                Ordering::Less => backward,
589                Ordering::Greater => !backward,
590            });
591        if backward {
592            boundaries.max_by_key(|boundary| *boundary)
593        } else {
594            boundaries.min_by_key(|boundary| *boundary)
595        }
596    }
597}
598
599#[derive(PartialEq)]
600enum Boundary {
601    Start,
602    End,
603}
604
605impl BoundedObject for FuzzyBoundary {
606    fn next_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
607        self.to_boundary(map, from, outer, false, Boundary::Start)
608    }
609    fn next_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
610        self.to_boundary(map, from, outer, false, Boundary::End)
611    }
612    fn previous_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
613        self.to_boundary(map, from, outer, true, Boundary::Start)
614    }
615    fn previous_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
616        self.to_boundary(map, from, outer, true, Boundary::End)
617    }
618    fn inner_range_can_be_zero_width(&self) -> bool {
619        false
620    }
621    fn surround_on_both_sides(&self) -> bool {
622        false
623    }
624    fn ambiguous_outer(&self) -> bool {
625        false
626    }
627}
628
629/// Returns the first boundary after or at `from` in text direction.
630/// The start and end of the file are the chars `'\0'`.
631fn try_find_boundary(
632    map: &DisplaySnapshot,
633    from: Offset,
634    is_boundary: &dyn Fn(char, char) -> bool,
635) -> Option<Offset> {
636    let boundary = try_find_boundary_data(map, from, |left, right, point| {
637        if is_boundary(left, right) {
638            Some(point)
639        } else {
640            None
641        }
642    })?;
643    Some(boundary)
644}
645
646/// Returns some information about it (of type `T`) as soon as
647/// there is a boundary after or at `from` in text direction
648/// The start and end of the file are the chars `'\0'`.
649fn try_find_boundary_data<T>(
650    map: &DisplaySnapshot,
651    mut from: Offset,
652    boundary_information: impl Fn(char, char, Offset) -> Option<T>,
653) -> Option<T> {
654    let mut prev_ch = map
655        .buffer_snapshot()
656        .reversed_chars_at(from.0)
657        .next()
658        .unwrap_or('\0');
659
660    for ch in map.buffer_snapshot().chars_at(from.0).chain(['\0']) {
661        if let Some(boundary_information) = boundary_information(prev_ch, ch, from) {
662            return Some(boundary_information);
663        }
664        from.0 += ch.len_utf8();
665        prev_ch = ch;
666    }
667
668    None
669}
670
671/// Returns the first boundary after or at `from` in text direction.
672/// The start and end of the file are the chars `'\0'`.
673fn try_find_preceding_boundary(
674    map: &DisplaySnapshot,
675    from: Offset,
676    is_boundary: &dyn Fn(char, char) -> bool,
677) -> Option<Offset> {
678    let boundary = try_find_preceding_boundary_data(map, from, |left, right, point| {
679        if is_boundary(left, right) {
680            Some(point)
681        } else {
682            None
683        }
684    })?;
685    Some(boundary)
686}
687
688/// Returns some information about it (of type `T`) as soon as
689/// there is a boundary before or at `from` in opposite text direction
690/// The start and end of the file are the chars `'\0'`.
691fn try_find_preceding_boundary_data<T>(
692    map: &DisplaySnapshot,
693    mut from: Offset,
694    is_boundary: impl Fn(char, char, Offset) -> Option<T>,
695) -> Option<T> {
696    let mut prev_ch = map
697        .buffer_snapshot()
698        .chars_at(from.0)
699        .next()
700        .unwrap_or('\0');
701
702    for ch in map
703        .buffer_snapshot()
704        .reversed_chars_at(from.0)
705        .chain(['\0'])
706    {
707        if let Some(boundary_information) = is_boundary(ch, prev_ch, from) {
708            return Some(boundary_information);
709        }
710        from.0.0 = from.0.0.saturating_sub(ch.len_utf8());
711        prev_ch = ch;
712    }
713
714    None
715}
716
717fn is_buffer_start(left: char) -> bool {
718    left == '\0'
719}
720
721fn is_buffer_end(right: char) -> bool {
722    right == '\0'
723}
724
725fn is_word_start(left: char, right: char, classifier: &CharClassifier) -> bool {
726    classifier.kind(left) != classifier.kind(right)
727        && classifier.kind(right) != CharKind::Whitespace
728}
729
730fn is_word_end(left: char, right: char, classifier: &CharClassifier) -> bool {
731    classifier.kind(left) != classifier.kind(right) && classifier.kind(left) != CharKind::Whitespace
732}
733
734fn is_sentence_end(left: char, right: char, classifier: &CharClassifier) -> bool {
735    const ENDS: [char; 1] = ['.'];
736
737    if classifier.kind(right) != CharKind::Whitespace {
738        return false;
739    }
740    ENDS.into_iter().any(|end| left == end)
741}