boundary.rs

  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(map.buffer_snapshot.clip_offset(*self - 1, Bias::Left)))
177    }
178    fn range(
179        start: (DisplayPoint, Bias),
180        end: (DisplayPoint, Bias),
181        map: &DisplaySnapshot,
182    ) -> Range<Self> {
183        Self(start.0.to_offset(map, start.1))..Self(end.0.to_offset(map, end.1))
184    }
185}
186
187impl<B: BoundedObject> HelixTextObject for B {
188    fn range(
189        &self,
190        map: &DisplaySnapshot,
191        relative_to: Range<DisplayPoint>,
192        around: bool,
193    ) -> Option<Range<DisplayPoint>> {
194        let relative_to = Offset::range(
195            (relative_to.start, Bias::Left),
196            (relative_to.end, Bias::Left),
197            map,
198        );
199
200        relative_range(self, around, map, |find_outer| {
201            let search_start = if self.can_be_zero_width(find_outer) {
202                relative_to.end
203            } else {
204                // If the objects can be directly next to each other an object end the
205                // cursor (relative_to) end would not count for close_at_end, so the search
206                // needs to start one character to the left.
207                relative_to.end.previous(map)?
208            };
209            let max_end = self.close_at_end(search_start, map, find_outer)?;
210            let min_start = self.close_at_start(max_end, map, find_outer)?;
211
212            (*min_start <= *relative_to.start).then(|| min_start..max_end)
213        })
214    }
215
216    fn next_range(
217        &self,
218        map: &DisplaySnapshot,
219        relative_to: Range<DisplayPoint>,
220        around: bool,
221    ) -> Option<Range<DisplayPoint>> {
222        let relative_to = Offset::range(
223            (relative_to.start, Bias::Left),
224            (relative_to.end, Bias::Left),
225            map,
226        );
227
228        relative_range(self, around, map, |find_outer| {
229            let min_start = self.next_start(map, relative_to.end, find_outer)?;
230            let max_end = self.close_at_end(min_start, map, find_outer)?;
231
232            Some(min_start..max_end)
233        })
234    }
235
236    fn previous_range(
237        &self,
238        map: &DisplaySnapshot,
239        relative_to: Range<DisplayPoint>,
240        around: bool,
241    ) -> Option<Range<DisplayPoint>> {
242        let relative_to = Offset::range(
243            (relative_to.start, Bias::Left),
244            (relative_to.end, Bias::Left),
245            map,
246        );
247
248        relative_range(self, around, map, |find_outer| {
249            let max_end = self.previous_end(map, relative_to.start, find_outer)?;
250            let min_start = self.close_at_start(max_end, map, find_outer)?;
251
252            Some(min_start..max_end)
253        })
254    }
255}
256
257fn relative_range<B: BoundedObject>(
258    object: &B,
259    outer: bool,
260    map: &DisplaySnapshot,
261    find_range: impl Fn(bool) -> Option<Range<Offset>>,
262) -> Option<Range<DisplayPoint>> {
263    // The cursor could be inside the outer range, but not the inner range.
264    // Whether that should count as found.
265    let find_outer = object.surround_on_both_sides() && !object.ambiguous_outer();
266    let range = find_range(find_outer)?;
267    let min_start = range.start;
268    let max_end = range.end;
269
270    let wanted_range = if outer && !find_outer {
271        // max_end is not yet the outer end
272        object.around(map, min_start..max_end)
273    } else if !outer && find_outer {
274        // max_end is the outer end, but the final result should have the inner end
275        object.inside(map, min_start..max_end)
276    } else {
277        min_start..max_end
278    };
279
280    let start = wanted_range.start.clone().to_display_point(map);
281    let end = wanted_range.end.clone().to_display_point(map);
282
283    Some(start..end)
284}
285
286/// A textobject whose boundaries can easily be found between two chars
287pub enum ImmediateBoundary {
288    Word { ignore_punctuation: bool },
289    Subword { ignore_punctuation: bool },
290    AngleBrackets,
291    BackQuotes,
292    CurlyBrackets,
293    DoubleQuotes,
294    Parentheses,
295    SingleQuotes,
296    SquareBrackets,
297    VerticalBars,
298}
299
300/// A textobject whose start and end can be found from an easy-to-find
301/// boundary between two chars by following a simple path from there
302pub enum FuzzyBoundary {
303    Sentence,
304    Paragraph,
305}
306
307impl ImmediateBoundary {
308    fn is_inner_start(&self, left: char, right: char, classifier: CharClassifier) -> bool {
309        match self {
310            Self::Word { ignore_punctuation } => {
311                let classifier = classifier.ignore_punctuation(*ignore_punctuation);
312                is_word_start(left, right, &classifier)
313                    || (is_buffer_start(left) && classifier.kind(right) != CharKind::Whitespace)
314            }
315            Self::Subword { ignore_punctuation } => {
316                let classifier = classifier.ignore_punctuation(*ignore_punctuation);
317                movement::is_subword_start(left, right, &classifier)
318                    || (is_buffer_start(left) && classifier.kind(right) != CharKind::Whitespace)
319            }
320            Self::AngleBrackets => left == '<',
321            Self::BackQuotes => left == '`',
322            Self::CurlyBrackets => left == '{',
323            Self::DoubleQuotes => left == '"',
324            Self::Parentheses => left == '(',
325            Self::SingleQuotes => left == '\'',
326            Self::SquareBrackets => left == '[',
327            Self::VerticalBars => left == '|',
328        }
329    }
330    fn is_inner_end(&self, left: char, right: char, classifier: CharClassifier) -> bool {
331        match self {
332            Self::Word { ignore_punctuation } => {
333                let classifier = classifier.ignore_punctuation(*ignore_punctuation);
334                is_word_end(left, right, &classifier)
335                    || (is_buffer_end(right) && classifier.kind(left) != CharKind::Whitespace)
336            }
337            Self::Subword { ignore_punctuation } => {
338                let classifier = classifier.ignore_punctuation(*ignore_punctuation);
339                movement::is_subword_start(left, right, &classifier)
340                    || (is_buffer_end(right) && classifier.kind(left) != CharKind::Whitespace)
341            }
342            Self::AngleBrackets => right == '>',
343            Self::BackQuotes => right == '`',
344            Self::CurlyBrackets => right == '}',
345            Self::DoubleQuotes => right == '"',
346            Self::Parentheses => right == ')',
347            Self::SingleQuotes => right == '\'',
348            Self::SquareBrackets => right == ']',
349            Self::VerticalBars => right == '|',
350        }
351    }
352    fn is_outer_start(&self, left: char, right: char, classifier: CharClassifier) -> bool {
353        match self {
354            word @ Self::Word { .. } => word.is_inner_end(left, right, classifier) || left == '\n',
355            subword @ Self::Subword { .. } => {
356                subword.is_inner_end(left, right, classifier) || left == '\n'
357            }
358            Self::AngleBrackets => right == '<',
359            Self::BackQuotes => right == '`',
360            Self::CurlyBrackets => right == '{',
361            Self::DoubleQuotes => right == '"',
362            Self::Parentheses => right == '(',
363            Self::SingleQuotes => right == '\'',
364            Self::SquareBrackets => right == '[',
365            Self::VerticalBars => right == '|',
366        }
367    }
368    fn is_outer_end(&self, left: char, right: char, classifier: CharClassifier) -> bool {
369        match self {
370            word @ Self::Word { .. } => {
371                word.is_inner_start(left, right, classifier) || right == '\n'
372            }
373            subword @ Self::Subword { .. } => {
374                subword.is_inner_start(left, right, classifier) || right == '\n'
375            }
376            Self::AngleBrackets => left == '>',
377            Self::BackQuotes => left == '`',
378            Self::CurlyBrackets => left == '}',
379            Self::DoubleQuotes => left == '"',
380            Self::Parentheses => left == ')',
381            Self::SingleQuotes => left == '\'',
382            Self::SquareBrackets => left == ']',
383            Self::VerticalBars => left == '|',
384        }
385    }
386}
387
388impl BoundedObject for ImmediateBoundary {
389    fn next_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
390        try_find_boundary(map, from, |left, right| {
391            let classifier = map.buffer_snapshot.char_classifier_at(*from);
392            if outer {
393                self.is_outer_start(left, right, classifier)
394            } else {
395                self.is_inner_start(left, right, classifier)
396            }
397        })
398    }
399    fn next_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
400        try_find_boundary(map, from, |left, right| {
401            let classifier = map.buffer_snapshot.char_classifier_at(*from);
402            if outer {
403                self.is_outer_end(left, right, classifier)
404            } else {
405                self.is_inner_end(left, right, classifier)
406            }
407        })
408    }
409    fn previous_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
410        try_find_preceding_boundary(map, from, |left, right| {
411            let classifier = map.buffer_snapshot.char_classifier_at(*from);
412            if outer {
413                self.is_outer_start(left, right, classifier)
414            } else {
415                self.is_inner_start(left, right, classifier)
416            }
417        })
418    }
419    fn previous_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
420        try_find_preceding_boundary(map, from, |left, right| {
421            let classifier = map.buffer_snapshot.char_classifier_at(*from);
422            if outer {
423                self.is_outer_end(left, right, classifier)
424            } else {
425                self.is_inner_end(left, right, classifier)
426            }
427        })
428    }
429    fn inner_range_can_be_zero_width(&self) -> bool {
430        match self {
431            Self::Subword { .. } | Self::Word { .. } => false,
432            _ => true,
433        }
434    }
435    fn surround_on_both_sides(&self) -> bool {
436        match self {
437            Self::Subword { .. } | Self::Word { .. } => false,
438            _ => true,
439        }
440    }
441    fn ambiguous_outer(&self) -> bool {
442        match self {
443            Self::BackQuotes
444            | Self::DoubleQuotes
445            | Self::SingleQuotes
446            | Self::VerticalBars
447            | Self::Subword { .. }
448            | Self::Word { .. } => true,
449            _ => false,
450        }
451    }
452}
453
454impl FuzzyBoundary {
455    /// When between two chars that form an easy-to-find identifier boundary,
456    /// what's the way to get to the actual start of the object, if any
457    fn is_near_potential_inner_start<'a>(
458        &self,
459        left: char,
460        right: char,
461        classifier: &CharClassifier,
462    ) -> Option<Box<dyn Fn(Offset, &'a DisplaySnapshot) -> Option<Offset>>> {
463        if is_buffer_start(left) {
464            return Some(Box::new(|identifier, _| Some(identifier)));
465        }
466        match self {
467            Self::Paragraph => {
468                if left != '\n' || right != '\n' {
469                    return None;
470                }
471                Some(Box::new(|identifier, map| {
472                    try_find_boundary(map, identifier, |left, right| left == '\n' && right != '\n')
473                }))
474            }
475            Self::Sentence => {
476                if let Some(find_paragraph_start) =
477                    Self::Paragraph.is_near_potential_inner_start(left, right, classifier)
478                {
479                    return Some(find_paragraph_start);
480                } else if !is_sentence_end(left, right, classifier) {
481                    return None;
482                }
483                Some(Box::new(|identifier, map| {
484                    let word = ImmediateBoundary::Word {
485                        ignore_punctuation: false,
486                    };
487                    word.next_start(map, identifier, false)
488                }))
489            }
490        }
491    }
492    /// When between two chars that form an easy-to-find identifier boundary,
493    /// what's the way to get to the actual end of the object, if any
494    fn is_near_potential_inner_end<'a>(
495        &self,
496        left: char,
497        right: char,
498        classifier: &CharClassifier,
499    ) -> Option<Box<dyn Fn(Offset, &'a DisplaySnapshot) -> Option<Offset>>> {
500        if is_buffer_end(right) {
501            return Some(Box::new(|identifier, _| Some(identifier)));
502        }
503        match self {
504            Self::Paragraph => {
505                if left != '\n' || right != '\n' {
506                    return None;
507                }
508                Some(Box::new(|identifier, map| {
509                    try_find_preceding_boundary(map, identifier, |left, right| {
510                        left != '\n' && right == '\n'
511                    })
512                }))
513            }
514            Self::Sentence => {
515                if let Some(find_paragraph_end) =
516                    Self::Paragraph.is_near_potential_inner_end(left, right, classifier)
517                {
518                    return Some(find_paragraph_end);
519                } else if !is_sentence_end(left, right, classifier) {
520                    return None;
521                }
522                Some(Box::new(|identifier, _| Some(identifier)))
523            }
524        }
525    }
526    /// When between two chars that form an easy-to-find identifier boundary,
527    /// what's the way to get to the actual end of the object, if any
528    fn is_near_potential_outer_start<'a>(
529        &self,
530        left: char,
531        right: char,
532        classifier: &CharClassifier,
533    ) -> Option<Box<dyn Fn(Offset, &'a DisplaySnapshot) -> Option<Offset>>> {
534        match self {
535            paragraph @ Self::Paragraph => {
536                paragraph.is_near_potential_inner_end(left, right, classifier)
537            }
538            sentence @ Self::Sentence => {
539                sentence.is_near_potential_inner_end(left, right, classifier)
540            }
541        }
542    }
543    /// When between two chars that form an easy-to-find identifier boundary,
544    /// what's the way to get to the actual end of the object, if any
545    fn is_near_potential_outer_end<'a>(
546        &self,
547        left: char,
548        right: char,
549        classifier: &CharClassifier,
550    ) -> Option<Box<dyn Fn(Offset, &'a DisplaySnapshot) -> Option<Offset>>> {
551        match self {
552            paragraph @ Self::Paragraph => {
553                paragraph.is_near_potential_inner_start(left, right, classifier)
554            }
555            sentence @ Self::Sentence => {
556                sentence.is_near_potential_inner_start(left, right, classifier)
557            }
558        }
559    }
560
561    // The boundary can be on the other side of `from` than the identifier, so the search needs to go both ways.
562    // Also, the distance (and direction) between identifier and boundary could vary, so a few ones need to be
563    // compared, even if one boundary was already found on the right side of `from`.
564    fn to_boundary(
565        &self,
566        map: &DisplaySnapshot,
567        from: Offset,
568        outer: bool,
569        backward: bool,
570        boundary_kind: Boundary,
571    ) -> Option<Offset> {
572        let generate_boundary_data = |left, right, point: Offset| {
573            let classifier = map.buffer_snapshot.char_classifier_at(*from);
574            let reach_boundary = if outer && boundary_kind == Boundary::Start {
575                self.is_near_potential_outer_start(left, right, &classifier)
576            } else if !outer && boundary_kind == Boundary::Start {
577                self.is_near_potential_inner_start(left, right, &classifier)
578            } else if outer && boundary_kind == Boundary::End {
579                self.is_near_potential_outer_end(left, right, &classifier)
580            } else {
581                self.is_near_potential_inner_end(left, right, &classifier)
582            };
583
584            reach_boundary.map(|reach_start| (point, reach_start))
585        };
586
587        let forwards = try_find_boundary_data(map, from, generate_boundary_data);
588        let backwards = try_find_preceding_boundary_data(map, from, generate_boundary_data);
589        let boundaries = [forwards, backwards]
590            .into_iter()
591            .flatten()
592            .filter_map(|(identifier, reach_boundary)| reach_boundary(identifier, map))
593            .filter(|boundary| match boundary.cmp(&from) {
594                Ordering::Equal => true,
595                Ordering::Less => backward,
596                Ordering::Greater => !backward,
597            });
598        if backward {
599            boundaries.max_by_key(|boundary| **boundary)
600        } else {
601            boundaries.min_by_key(|boundary| **boundary)
602        }
603    }
604}
605
606#[derive(PartialEq)]
607enum Boundary {
608    Start,
609    End,
610}
611
612impl BoundedObject for FuzzyBoundary {
613    fn next_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
614        self.to_boundary(map, from, outer, false, Boundary::Start)
615    }
616    fn next_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
617        self.to_boundary(map, from, outer, false, Boundary::End)
618    }
619    fn previous_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
620        self.to_boundary(map, from, outer, true, Boundary::Start)
621    }
622    fn previous_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
623        self.to_boundary(map, from, outer, true, Boundary::End)
624    }
625    fn inner_range_can_be_zero_width(&self) -> bool {
626        false
627    }
628    fn surround_on_both_sides(&self) -> bool {
629        false
630    }
631    fn ambiguous_outer(&self) -> bool {
632        false
633    }
634}
635
636/// Returns the first boundary after or at `from` in text direction.
637/// The start and end of the file are the chars `'\0'`.
638fn try_find_boundary(
639    map: &DisplaySnapshot,
640    from: Offset,
641    is_boundary: impl Fn(char, char) -> bool,
642) -> Option<Offset> {
643    let boundary = try_find_boundary_data(map, from, |left, right, point| {
644        if is_boundary(left, right) {
645            Some(point)
646        } else {
647            None
648        }
649    })?;
650    Some(boundary)
651}
652
653/// Returns some information about it (of type `T`) as soon as
654/// there is a boundary after or at `from` in text direction
655/// The start and end of the file are the chars `'\0'`.
656fn try_find_boundary_data<T>(
657    map: &DisplaySnapshot,
658    mut from: Offset,
659    boundary_information: impl Fn(char, char, Offset) -> Option<T>,
660) -> Option<T> {
661    let mut prev_ch = map
662        .buffer_snapshot
663        .reversed_chars_at(*from)
664        .next()
665        .unwrap_or('\0');
666
667    for ch in map.buffer_snapshot.chars_at(*from).chain(['\0']) {
668        if let Some(boundary_information) = boundary_information(prev_ch, ch, from) {
669            return Some(boundary_information);
670        }
671        *from += ch.len_utf8();
672        prev_ch = ch;
673    }
674
675    None
676}
677
678/// Returns the first boundary after or at `from` in text direction.
679/// The start and end of the file are the chars `'\0'`.
680fn try_find_preceding_boundary(
681    map: &DisplaySnapshot,
682    from: Offset,
683    is_boundary: impl Fn(char, char) -> bool,
684) -> Option<Offset> {
685    let boundary = try_find_preceding_boundary_data(map, from, |left, right, point| {
686        if is_boundary(left, right) {
687            Some(point)
688        } else {
689            None
690        }
691    })?;
692    Some(boundary)
693}
694
695/// Returns some information about it (of type `T`) as soon as
696/// there is a boundary before or at `from` in opposite text direction
697/// The start and end of the file are the chars `'\0'`.
698fn try_find_preceding_boundary_data<T>(
699    map: &DisplaySnapshot,
700    mut from: Offset,
701    is_boundary: impl Fn(char, char, Offset) -> Option<T>,
702) -> Option<T> {
703    let mut prev_ch = map.buffer_snapshot.chars_at(*from).next().unwrap_or('\0');
704
705    for ch in map.buffer_snapshot.reversed_chars_at(*from).chain(['\0']) {
706        if let Some(boundary_information) = is_boundary(ch, prev_ch, from) {
707            return Some(boundary_information);
708        }
709        from.0 = from.0.saturating_sub(ch.len_utf8());
710        prev_ch = ch;
711    }
712
713    None
714}
715
716fn is_buffer_start(left: char) -> bool {
717    left == '\0'
718}
719
720fn is_buffer_end(right: char) -> bool {
721    right == '\0'
722}
723
724fn is_word_start(left: char, right: char, classifier: &CharClassifier) -> bool {
725    classifier.kind(left) != classifier.kind(right)
726        && classifier.kind(right) != CharKind::Whitespace
727}
728
729fn is_word_end(left: char, right: char, classifier: &CharClassifier) -> bool {
730    classifier.kind(left) != classifier.kind(right) && classifier.kind(left) != CharKind::Whitespace
731}
732
733fn is_sentence_end(left: char, right: char, classifier: &CharClassifier) -> bool {
734    const ENDS: [char; 1] = ['.'];
735
736    if classifier.kind(right) != CharKind::Whitespace {
737        return false;
738    }
739    ENDS.into_iter().any(|end| left == end)
740}