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