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}