movement.rs

  1use rope::point::Point;
  2
  3use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
  4use crate::{char_kind, CharKind, ToPoint};
  5use std::ops::Range;
  6
  7pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
  8    if point.column() > 0 {
  9        *point.column_mut() -= 1;
 10    } else if point.row() > 0 {
 11        *point.row_mut() -= 1;
 12        *point.column_mut() = map.line_len(point.row());
 13    }
 14    map.clip_point(point, Bias::Left)
 15}
 16
 17pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
 18    let max_column = map.line_len(point.row());
 19    if point.column() < max_column {
 20        *point.column_mut() += 1;
 21    } else if point.row() < map.max_point().row() {
 22        *point.row_mut() += 1;
 23        *point.column_mut() = 0;
 24    }
 25    map.clip_point(point, Bias::Right)
 26}
 27
 28pub fn up(
 29    map: &DisplaySnapshot,
 30    start: DisplayPoint,
 31    goal: SelectionGoal,
 32    preserve_column_at_start: bool,
 33) -> (DisplayPoint, SelectionGoal) {
 34    let mut goal_column = if let SelectionGoal::Column(column) = goal {
 35        column
 36    } else {
 37        map.column_to_chars(start.row(), start.column())
 38    };
 39
 40    let prev_row = start.row().saturating_sub(1);
 41    let mut point = map.clip_point(
 42        DisplayPoint::new(prev_row, map.line_len(prev_row)),
 43        Bias::Left,
 44    );
 45    if point.row() < start.row() {
 46        *point.column_mut() = map.column_from_chars(point.row(), goal_column);
 47    } else if preserve_column_at_start {
 48        return (start, goal);
 49    } else {
 50        point = DisplayPoint::new(0, 0);
 51        goal_column = 0;
 52    }
 53
 54    let clip_bias = if point.column() == map.line_len(point.row()) {
 55        Bias::Left
 56    } else {
 57        Bias::Right
 58    };
 59
 60    (
 61        map.clip_point(point, clip_bias),
 62        SelectionGoal::Column(goal_column),
 63    )
 64}
 65
 66pub fn down(
 67    map: &DisplaySnapshot,
 68    start: DisplayPoint,
 69    goal: SelectionGoal,
 70    preserve_column_at_end: bool,
 71) -> (DisplayPoint, SelectionGoal) {
 72    let mut goal_column = if let SelectionGoal::Column(column) = goal {
 73        column
 74    } else {
 75        map.column_to_chars(start.row(), start.column())
 76    };
 77
 78    let next_row = start.row() + 1;
 79    let mut point = map.clip_point(DisplayPoint::new(next_row, 0), Bias::Right);
 80    if point.row() > start.row() {
 81        *point.column_mut() = map.column_from_chars(point.row(), goal_column);
 82    } else if preserve_column_at_end {
 83        return (start, goal);
 84    } else {
 85        point = map.max_point();
 86        goal_column = map.column_to_chars(point.row(), point.column())
 87    }
 88
 89    let clip_bias = if point.column() == map.line_len(point.row()) {
 90        Bias::Left
 91    } else {
 92        Bias::Right
 93    };
 94
 95    (
 96        map.clip_point(point, clip_bias),
 97        SelectionGoal::Column(goal_column),
 98    )
 99}
100
101pub fn line_beginning(
102    map: &DisplaySnapshot,
103    display_point: DisplayPoint,
104    stop_at_soft_boundaries: bool,
105) -> DisplayPoint {
106    let point = display_point.to_point(map);
107    let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
108    let line_start = map.prev_line_boundary(point).1;
109
110    if stop_at_soft_boundaries && display_point != soft_line_start {
111        soft_line_start
112    } else {
113        line_start
114    }
115}
116
117pub fn indented_line_beginning(
118    map: &DisplaySnapshot,
119    display_point: DisplayPoint,
120    stop_at_soft_boundaries: bool,
121) -> DisplayPoint {
122    let point = display_point.to_point(map);
123    let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
124    let indent_start = Point::new(
125        point.row,
126        map.buffer_snapshot.indent_size_for_line(point.row).len,
127    )
128    .to_display_point(map);
129    let line_start = map.prev_line_boundary(point).1;
130
131    if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start
132    {
133        soft_line_start
134    } else if stop_at_soft_boundaries && display_point != indent_start {
135        indent_start
136    } else {
137        line_start
138    }
139}
140
141pub fn line_end(
142    map: &DisplaySnapshot,
143    display_point: DisplayPoint,
144    stop_at_soft_boundaries: bool,
145) -> DisplayPoint {
146    let soft_line_end = map.clip_point(
147        DisplayPoint::new(display_point.row(), map.line_len(display_point.row())),
148        Bias::Left,
149    );
150    if stop_at_soft_boundaries && display_point != soft_line_end {
151        soft_line_end
152    } else {
153        map.next_line_boundary(display_point.to_point(map)).1
154    }
155}
156
157pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
158    find_preceding_boundary(map, point, |left, right| {
159        (char_kind(left) != char_kind(right) && !right.is_whitespace()) || left == '\n'
160    })
161}
162
163pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
164    find_preceding_boundary(map, point, |left, right| {
165        let is_word_start = char_kind(left) != char_kind(right) && !right.is_whitespace();
166        let is_subword_start =
167            left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
168        is_word_start || is_subword_start || left == '\n'
169    })
170}
171
172pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
173    find_boundary(map, point, |left, right| {
174        (char_kind(left) != char_kind(right) && !left.is_whitespace()) || right == '\n'
175    })
176}
177
178pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
179    find_boundary(map, point, |left, right| {
180        let is_word_end = (char_kind(left) != char_kind(right)) && !left.is_whitespace();
181        let is_subword_end =
182            left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
183        is_word_end || is_subword_end || right == '\n'
184    })
185}
186
187/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
188/// given predicate returning true. The predicate is called with the character to the left and right
189/// of the candidate boundary location, and will be called with `\n` characters indicating the start
190/// or end of a line.
191pub fn find_preceding_boundary(
192    map: &DisplaySnapshot,
193    from: DisplayPoint,
194    mut is_boundary: impl FnMut(char, char) -> bool,
195) -> DisplayPoint {
196    let mut start_column = 0;
197    let mut soft_wrap_row = from.row() + 1;
198
199    let mut prev = None;
200    for (ch, point) in map.reverse_chars_at(from) {
201        // Recompute soft_wrap_indent if the row has changed
202        if point.row() != soft_wrap_row {
203            soft_wrap_row = point.row();
204
205            if point.row() == 0 {
206                start_column = 0;
207            } else if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
208                start_column = indent;
209            }
210        }
211
212        // If the current point is in the soft_wrap, skip comparing it
213        if point.column() < start_column {
214            continue;
215        }
216
217        if let Some((prev_ch, prev_point)) = prev {
218            if is_boundary(ch, prev_ch) {
219                return prev_point;
220            }
221        }
222
223        prev = Some((ch, point));
224    }
225    DisplayPoint::zero()
226}
227
228/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
229/// given predicate returning true. The predicate is called with the character to the left and right
230/// of the candidate boundary location, and will be called with `\n` characters indicating the start
231/// or end of a line. If no boundary is found, the start of the line is returned.
232pub fn find_preceding_boundary_in_line(
233    map: &DisplaySnapshot,
234    from: DisplayPoint,
235    mut is_boundary: impl FnMut(char, char) -> bool,
236) -> DisplayPoint {
237    let mut start_column = 0;
238    if from.row() > 0 {
239        if let Some(indent) = map.soft_wrap_indent(from.row() - 1) {
240            start_column = indent;
241        }
242    }
243
244    let mut prev = None;
245    for (ch, point) in map.reverse_chars_at(from) {
246        if let Some((prev_ch, prev_point)) = prev {
247            if is_boundary(ch, prev_ch) {
248                return prev_point;
249            }
250        }
251
252        if ch == '\n' || point.column() < start_column {
253            break;
254        }
255
256        prev = Some((ch, point));
257    }
258
259    prev.map(|(_, point)| point).unwrap_or(from)
260}
261
262/// Scans for a boundary following the given start point until a boundary is found, indicated by the
263/// given predicate returning true. The predicate is called with the character to the left and right
264/// of the candidate boundary location, and will be called with `\n` characters indicating the start
265/// or end of a line.
266pub fn find_boundary(
267    map: &DisplaySnapshot,
268    from: DisplayPoint,
269    mut is_boundary: impl FnMut(char, char) -> bool,
270) -> DisplayPoint {
271    let mut prev_ch = None;
272    for (ch, point) in map.chars_at(from) {
273        if let Some(prev_ch) = prev_ch {
274            if is_boundary(prev_ch, ch) {
275                return map.clip_point(point, Bias::Right);
276            }
277        }
278
279        prev_ch = Some(ch);
280    }
281    map.clip_point(map.max_point(), Bias::Right)
282}
283
284/// Scans for a boundary following the given start point until a boundary is found, indicated by the
285/// given predicate returning true. The predicate is called with the character to the left and right
286/// of the candidate boundary location, and will be called with `\n` characters indicating the start
287/// or end of a line. If no boundary is found, the end of the line is returned
288pub fn find_boundary_in_line(
289    map: &DisplaySnapshot,
290    from: DisplayPoint,
291    mut is_boundary: impl FnMut(char, char) -> bool,
292) -> DisplayPoint {
293    let mut prev = None;
294    for (ch, point) in map.chars_at(from) {
295        if let Some((prev_ch, _)) = prev {
296            if is_boundary(prev_ch, ch) {
297                return map.clip_point(point, Bias::Right);
298            }
299        }
300
301        prev = Some((ch, point));
302
303        if ch == '\n' {
304            break;
305        }
306    }
307
308    // Return the last position checked so that we give a point right before the newline or eof.
309    map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Right)
310}
311
312pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
313    let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
314    let text = &map.buffer_snapshot;
315    let next_char_kind = text.chars_at(ix).next().map(char_kind);
316    let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind);
317    prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
318}
319
320pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<DisplayPoint> {
321    let position = map
322        .clip_point(position, Bias::Left)
323        .to_offset(map, Bias::Left);
324    let (range, _) = map.buffer_snapshot.surrounding_word(position);
325    let start = range
326        .start
327        .to_point(&map.buffer_snapshot)
328        .to_display_point(map);
329    let end = range
330        .end
331        .to_point(&map.buffer_snapshot)
332        .to_display_point(map);
333    start..end
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339    use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer};
340    use rope::point::Point;
341    use settings::Settings;
342
343    #[gpui::test]
344    fn test_previous_word_start(cx: &mut gpui::MutableAppContext) {
345        cx.set_global(Settings::test(cx));
346        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
347            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
348            assert_eq!(
349                previous_word_start(&snapshot, display_points[1]),
350                display_points[0]
351            );
352        }
353
354        assert("\nˇ   ˇlorem", cx);
355        assert("ˇ\nˇ   lorem", cx);
356        assert("    ˇloremˇ", cx);
357        assert("ˇ    ˇlorem", cx);
358        assert("    ˇlorˇem", cx);
359        assert("\nlorem\nˇ   ˇipsum", cx);
360        assert("\n\nˇ\nˇ", cx);
361        assert("    ˇlorem  ˇipsum", cx);
362        assert("loremˇ-ˇipsum", cx);
363        assert("loremˇ-#$@ˇipsum", cx);
364        assert("ˇlorem_ˇipsum", cx);
365        assert(" ˇdefγˇ", cx);
366        assert(" ˇbcΔˇ", cx);
367        assert(" abˇ——ˇcd", cx);
368    }
369
370    #[gpui::test]
371    fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) {
372        cx.set_global(Settings::test(cx));
373        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
374            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
375            assert_eq!(
376                previous_subword_start(&snapshot, display_points[1]),
377                display_points[0]
378            );
379        }
380
381        // Subword boundaries are respected
382        assert("lorem_ˇipˇsum", cx);
383        assert("lorem_ˇipsumˇ", cx);
384        assert("ˇlorem_ˇipsum", cx);
385        assert("lorem_ˇipsum_ˇdolor", cx);
386        assert("loremˇIpˇsum", cx);
387        assert("loremˇIpsumˇ", cx);
388
389        // Word boundaries are still respected
390        assert("\nˇ   ˇlorem", cx);
391        assert("    ˇloremˇ", cx);
392        assert("    ˇlorˇem", cx);
393        assert("\nlorem\nˇ   ˇipsum", cx);
394        assert("\n\nˇ\nˇ", cx);
395        assert("    ˇlorem  ˇipsum", cx);
396        assert("loremˇ-ˇipsum", cx);
397        assert("loremˇ-#$@ˇipsum", cx);
398        assert(" ˇdefγˇ", cx);
399        assert(" bcˇΔˇ", cx);
400        assert(" ˇbcδˇ", cx);
401        assert(" abˇ——ˇcd", cx);
402    }
403
404    #[gpui::test]
405    fn test_find_preceding_boundary(cx: &mut gpui::MutableAppContext) {
406        cx.set_global(Settings::test(cx));
407        fn assert(
408            marked_text: &str,
409            cx: &mut gpui::MutableAppContext,
410            is_boundary: impl FnMut(char, char) -> bool,
411        ) {
412            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
413            assert_eq!(
414                find_preceding_boundary(&snapshot, display_points[1], is_boundary),
415                display_points[0]
416            );
417        }
418
419        assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
420            left == 'c' && right == 'd'
421        });
422        assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
423            left == '\n' && right == 'g'
424        });
425        let mut line_count = 0;
426        assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
427            if left == '\n' {
428                line_count += 1;
429                line_count == 2
430            } else {
431                false
432            }
433        });
434    }
435
436    #[gpui::test]
437    fn test_next_word_end(cx: &mut gpui::MutableAppContext) {
438        cx.set_global(Settings::test(cx));
439        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
440            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
441            assert_eq!(
442                next_word_end(&snapshot, display_points[0]),
443                display_points[1]
444            );
445        }
446
447        assert("\nˇ   loremˇ", cx);
448        assert("    ˇloremˇ", cx);
449        assert("    lorˇemˇ", cx);
450        assert("    loremˇ    ˇ\nipsum\n", cx);
451        assert("\nˇ\nˇ\n\n", cx);
452        assert("loremˇ    ipsumˇ   ", cx);
453        assert("loremˇ-ˇipsum", cx);
454        assert("loremˇ#$@-ˇipsum", cx);
455        assert("loremˇ_ipsumˇ", cx);
456        assert(" ˇbcΔˇ", cx);
457        assert(" abˇ——ˇcd", cx);
458    }
459
460    #[gpui::test]
461    fn test_next_subword_end(cx: &mut gpui::MutableAppContext) {
462        cx.set_global(Settings::test(cx));
463        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
464            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
465            assert_eq!(
466                next_subword_end(&snapshot, display_points[0]),
467                display_points[1]
468            );
469        }
470
471        // Subword boundaries are respected
472        assert("loˇremˇ_ipsum", cx);
473        assert("ˇloremˇ_ipsum", cx);
474        assert("loremˇ_ipsumˇ", cx);
475        assert("loremˇ_ipsumˇ_dolor", cx);
476        assert("loˇremˇIpsum", cx);
477        assert("loremˇIpsumˇDolor", cx);
478
479        // Word boundaries are still respected
480        assert("\nˇ   loremˇ", cx);
481        assert("    ˇloremˇ", cx);
482        assert("    lorˇemˇ", cx);
483        assert("    loremˇ    ˇ\nipsum\n", cx);
484        assert("\nˇ\nˇ\n\n", cx);
485        assert("loremˇ    ipsumˇ   ", cx);
486        assert("loremˇ-ˇipsum", cx);
487        assert("loremˇ#$@-ˇipsum", cx);
488        assert("loremˇ_ipsumˇ", cx);
489        assert(" ˇbcˇΔ", cx);
490        assert(" abˇ——ˇcd", cx);
491    }
492
493    #[gpui::test]
494    fn test_find_boundary(cx: &mut gpui::MutableAppContext) {
495        cx.set_global(Settings::test(cx));
496        fn assert(
497            marked_text: &str,
498            cx: &mut gpui::MutableAppContext,
499            is_boundary: impl FnMut(char, char) -> bool,
500        ) {
501            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
502            assert_eq!(
503                find_boundary(&snapshot, display_points[0], is_boundary),
504                display_points[1]
505            );
506        }
507
508        assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
509            left == 'j' && right == 'k'
510        });
511        assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
512            left == '\n' && right == 'i'
513        });
514        let mut line_count = 0;
515        assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
516            if left == '\n' {
517                line_count += 1;
518                line_count == 2
519            } else {
520                false
521            }
522        });
523    }
524
525    #[gpui::test]
526    fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
527        cx.set_global(Settings::test(cx));
528        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
529            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
530            assert_eq!(
531                surrounding_word(&snapshot, display_points[1]),
532                display_points[0]..display_points[2]
533            );
534        }
535
536        assert("ˇˇloremˇ  ipsum", cx);
537        assert("ˇloˇremˇ  ipsum", cx);
538        assert("ˇloremˇˇ  ipsum", cx);
539        assert("loremˇ ˇ  ˇipsum", cx);
540        assert("lorem\nˇˇˇ\nipsum", cx);
541        assert("lorem\nˇˇipsumˇ", cx);
542        assert("lorem,ˇˇ ˇipsum", cx);
543        assert("ˇloremˇˇ, ipsum", cx);
544    }
545
546    #[gpui::test]
547    fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
548        cx.set_global(Settings::test(cx));
549        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
550        let font_id = cx
551            .font_cache()
552            .select_font(family_id, &Default::default())
553            .unwrap();
554
555        let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx));
556        let multibuffer = cx.add_model(|cx| {
557            let mut multibuffer = MultiBuffer::new(0);
558            multibuffer.push_excerpts(
559                buffer.clone(),
560                [
561                    ExcerptRange {
562                        context: Point::new(0, 0)..Point::new(1, 4),
563                        primary: None,
564                    },
565                    ExcerptRange {
566                        context: Point::new(2, 0)..Point::new(3, 2),
567                        primary: None,
568                    },
569                ],
570                cx,
571            );
572            multibuffer
573        });
574        let display_map =
575            cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
576        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
577
578        assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
579
580        // Can't move up into the first excerpt's header
581        assert_eq!(
582            up(
583                &snapshot,
584                DisplayPoint::new(2, 2),
585                SelectionGoal::Column(2),
586                false
587            ),
588            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
589        );
590        assert_eq!(
591            up(
592                &snapshot,
593                DisplayPoint::new(2, 0),
594                SelectionGoal::None,
595                false
596            ),
597            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
598        );
599
600        // Move up and down within first excerpt
601        assert_eq!(
602            up(
603                &snapshot,
604                DisplayPoint::new(3, 4),
605                SelectionGoal::Column(4),
606                false
607            ),
608            (DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
609        );
610        assert_eq!(
611            down(
612                &snapshot,
613                DisplayPoint::new(2, 3),
614                SelectionGoal::Column(4),
615                false
616            ),
617            (DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
618        );
619
620        // Move up and down across second excerpt's header
621        assert_eq!(
622            up(
623                &snapshot,
624                DisplayPoint::new(6, 5),
625                SelectionGoal::Column(5),
626                false
627            ),
628            (DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
629        );
630        assert_eq!(
631            down(
632                &snapshot,
633                DisplayPoint::new(3, 4),
634                SelectionGoal::Column(5),
635                false
636            ),
637            (DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
638        );
639
640        // Can't move down off the end
641        assert_eq!(
642            down(
643                &snapshot,
644                DisplayPoint::new(7, 0),
645                SelectionGoal::Column(0),
646                false
647            ),
648            (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
649        );
650        assert_eq!(
651            down(
652                &snapshot,
653                DisplayPoint::new(7, 2),
654                SelectionGoal::Column(2),
655                false
656            ),
657            (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
658        );
659    }
660}