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| 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}