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}