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 indent_start = Point::new(
109        point.row,
110        map.buffer_snapshot.indent_size_for_line(point.row).len,
111    )
112    .to_display_point(map);
113    let line_start = map.prev_line_boundary(point).1;
114
115    if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start
116    {
117        soft_line_start
118    } else if stop_at_soft_boundaries && display_point != indent_start {
119        indent_start
120    } else {
121        line_start
122    }
123}
124
125pub fn line_end(
126    map: &DisplaySnapshot,
127    display_point: DisplayPoint,
128    stop_at_soft_boundaries: bool,
129) -> DisplayPoint {
130    let soft_line_end = map.clip_point(
131        DisplayPoint::new(display_point.row(), map.line_len(display_point.row())),
132        Bias::Left,
133    );
134    if stop_at_soft_boundaries && display_point != soft_line_end {
135        soft_line_end
136    } else {
137        map.next_line_boundary(display_point.to_point(map)).1
138    }
139}
140
141pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
142    find_preceding_boundary(map, point, |left, right| {
143        (char_kind(left) != char_kind(right) && !right.is_whitespace()) || left == '\n'
144    })
145}
146
147pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
148    find_preceding_boundary(map, point, |left, right| {
149        let is_word_start = char_kind(left) != char_kind(right) && !right.is_whitespace();
150        let is_subword_start =
151            left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
152        is_word_start || is_subword_start || left == '\n'
153    })
154}
155
156pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
157    find_boundary(map, point, |left, right| {
158        (char_kind(left) != char_kind(right) && !left.is_whitespace()) || right == '\n'
159    })
160}
161
162pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
163    find_boundary(map, point, |left, right| {
164        let is_word_end = (char_kind(left) != char_kind(right)) && !left.is_whitespace();
165        let is_subword_end =
166            left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
167        is_word_end || is_subword_end || right == '\n'
168    })
169}
170
171/// Scans for a boundary from the start of each line preceding the given end point until a boundary
172/// is found, indicated by the given predicate returning true. The predicate is called with the
173/// character to the left and right of the candidate boundary location, and will be called with `\n`
174/// characters indicating the start or end of a line. If the predicate returns true multiple times
175/// on a line, the *rightmost* boundary is returned.
176pub fn find_preceding_boundary(
177    map: &DisplaySnapshot,
178    end: DisplayPoint,
179    mut is_boundary: impl FnMut(char, char) -> bool,
180) -> DisplayPoint {
181    let mut point = end;
182    loop {
183        *point.column_mut() = 0;
184        if point.row() > 0 {
185            if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
186                *point.column_mut() = indent;
187            }
188        }
189
190        let mut boundary = None;
191        let mut prev_ch = if point.is_zero() { None } else { Some('\n') };
192        for ch in map.chars_at(point) {
193            if point >= end {
194                break;
195            }
196
197            if let Some(prev_ch) = prev_ch {
198                if is_boundary(prev_ch, ch) {
199                    boundary = Some(point);
200                }
201            }
202
203            if ch == '\n' {
204                break;
205            }
206
207            prev_ch = Some(ch);
208            *point.column_mut() += ch.len_utf8() as u32;
209        }
210
211        if let Some(boundary) = boundary {
212            return boundary;
213        } else if point.row() == 0 {
214            return DisplayPoint::zero();
215        } else {
216            *point.row_mut() -= 1;
217        }
218    }
219}
220
221/// Scans for a boundary following the given start point until a boundary is found, indicated by the
222/// given predicate returning true. The predicate is called with the character to the left and right
223/// of the candidate boundary location, and will be called with `\n` characters indicating the start
224/// or end of a line.
225pub fn find_boundary(
226    map: &DisplaySnapshot,
227    mut point: DisplayPoint,
228    mut is_boundary: impl FnMut(char, char) -> bool,
229) -> DisplayPoint {
230    let mut prev_ch = None;
231    for ch in map.chars_at(point) {
232        if let Some(prev_ch) = prev_ch {
233            if is_boundary(prev_ch, ch) {
234                break;
235            }
236        }
237
238        if ch == '\n' {
239            *point.row_mut() += 1;
240            *point.column_mut() = 0;
241        } else {
242            *point.column_mut() += ch.len_utf8() as u32;
243        }
244        prev_ch = Some(ch);
245    }
246    map.clip_point(point, Bias::Right)
247}
248
249pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
250    let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
251    let text = &map.buffer_snapshot;
252    let next_char_kind = text.chars_at(ix).next().map(char_kind);
253    let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind);
254    prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
255}
256
257pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<DisplayPoint> {
258    let position = map
259        .clip_point(position, Bias::Left)
260        .to_offset(map, Bias::Left);
261    let (range, _) = map.buffer_snapshot.surrounding_word(position);
262    let start = range
263        .start
264        .to_point(&map.buffer_snapshot)
265        .to_display_point(map);
266    let end = range
267        .end
268        .to_point(&map.buffer_snapshot)
269        .to_display_point(map);
270    start..end
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer};
277    use rope::point::Point;
278    use settings::Settings;
279
280    #[gpui::test]
281    fn test_previous_word_start(cx: &mut gpui::MutableAppContext) {
282        cx.set_global(Settings::test(cx));
283        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
284            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
285            assert_eq!(
286                previous_word_start(&snapshot, display_points[1]),
287                display_points[0]
288            );
289        }
290
291        assert("\nˇ   ˇlorem", cx);
292        assert("ˇ\nˇ   lorem", cx);
293        assert("    ˇloremˇ", cx);
294        assert("ˇ    ˇlorem", cx);
295        assert("    ˇlorˇem", cx);
296        assert("\nlorem\nˇ   ˇipsum", cx);
297        assert("\n\nˇ\nˇ", cx);
298        assert("    ˇlorem  ˇipsum", cx);
299        assert("loremˇ-ˇipsum", cx);
300        assert("loremˇ-#$@ˇipsum", cx);
301        assert("ˇlorem_ˇipsum", cx);
302        assert(" ˇdefγˇ", cx);
303        assert(" ˇbcΔˇ", cx);
304        assert(" abˇ——ˇcd", cx);
305    }
306
307    #[gpui::test]
308    fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) {
309        cx.set_global(Settings::test(cx));
310        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
311            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
312            assert_eq!(
313                previous_subword_start(&snapshot, display_points[1]),
314                display_points[0]
315            );
316        }
317
318        // Subword boundaries are respected
319        assert("lorem_ˇipˇsum", cx);
320        assert("lorem_ˇipsumˇ", cx);
321        assert("ˇlorem_ˇipsum", cx);
322        assert("lorem_ˇipsum_ˇdolor", cx);
323        assert("loremˇIpˇsum", cx);
324        assert("loremˇIpsumˇ", cx);
325
326        // Word boundaries are still respected
327        assert("\nˇ   ˇlorem", cx);
328        assert("    ˇloremˇ", cx);
329        assert("    ˇlorˇem", cx);
330        assert("\nlorem\nˇ   ˇipsum", cx);
331        assert("\n\nˇ\nˇ", cx);
332        assert("    ˇlorem  ˇipsum", cx);
333        assert("loremˇ-ˇipsum", cx);
334        assert("loremˇ-#$@ˇipsum", cx);
335        assert(" ˇdefγˇ", cx);
336        assert(" bcˇΔˇ", cx);
337        assert(" ˇbcδˇ", cx);
338        assert(" abˇ——ˇcd", cx);
339    }
340
341    #[gpui::test]
342    fn test_find_preceding_boundary(cx: &mut gpui::MutableAppContext) {
343        cx.set_global(Settings::test(cx));
344        fn assert(
345            marked_text: &str,
346            cx: &mut gpui::MutableAppContext,
347            is_boundary: impl FnMut(char, char) -> bool,
348        ) {
349            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
350            assert_eq!(
351                find_preceding_boundary(&snapshot, display_points[1], is_boundary),
352                display_points[0]
353            );
354        }
355
356        assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
357            left == 'c' && right == 'd'
358        });
359        assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
360            left == '\n' && right == 'g'
361        });
362        let mut line_count = 0;
363        assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
364            if left == '\n' {
365                line_count += 1;
366                line_count == 2
367            } else {
368                false
369            }
370        });
371    }
372
373    #[gpui::test]
374    fn test_next_word_end(cx: &mut gpui::MutableAppContext) {
375        cx.set_global(Settings::test(cx));
376        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
377            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
378            assert_eq!(
379                next_word_end(&snapshot, display_points[0]),
380                display_points[1]
381            );
382        }
383
384        assert("\nˇ   loremˇ", cx);
385        assert("    ˇloremˇ", cx);
386        assert("    lorˇemˇ", cx);
387        assert("    loremˇ    ˇ\nipsum\n", cx);
388        assert("\nˇ\nˇ\n\n", cx);
389        assert("loremˇ    ipsumˇ   ", cx);
390        assert("loremˇ-ˇipsum", cx);
391        assert("loremˇ#$@-ˇipsum", cx);
392        assert("loremˇ_ipsumˇ", cx);
393        assert(" ˇbcΔˇ", cx);
394        assert(" abˇ——ˇcd", cx);
395    }
396
397    #[gpui::test]
398    fn test_next_subword_end(cx: &mut gpui::MutableAppContext) {
399        cx.set_global(Settings::test(cx));
400        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
401            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
402            assert_eq!(
403                next_subword_end(&snapshot, display_points[0]),
404                display_points[1]
405            );
406        }
407
408        // Subword boundaries are respected
409        assert("loˇremˇ_ipsum", cx);
410        assert("ˇloremˇ_ipsum", cx);
411        assert("loremˇ_ipsumˇ", cx);
412        assert("loremˇ_ipsumˇ_dolor", cx);
413        assert("loˇremˇIpsum", cx);
414        assert("loremˇIpsumˇDolor", cx);
415
416        // Word boundaries are still respected
417        assert("\nˇ   loremˇ", cx);
418        assert("    ˇloremˇ", cx);
419        assert("    lorˇemˇ", cx);
420        assert("    loremˇ    ˇ\nipsum\n", cx);
421        assert("\nˇ\nˇ\n\n", cx);
422        assert("loremˇ    ipsumˇ   ", cx);
423        assert("loremˇ-ˇipsum", cx);
424        assert("loremˇ#$@-ˇipsum", cx);
425        assert("loremˇ_ipsumˇ", cx);
426        assert(" ˇbcˇΔ", cx);
427        assert(" abˇ——ˇcd", cx);
428    }
429
430    #[gpui::test]
431    fn test_find_boundary(cx: &mut gpui::MutableAppContext) {
432        cx.set_global(Settings::test(cx));
433        fn assert(
434            marked_text: &str,
435            cx: &mut gpui::MutableAppContext,
436            is_boundary: impl FnMut(char, char) -> bool,
437        ) {
438            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
439            assert_eq!(
440                find_boundary(&snapshot, display_points[0], is_boundary),
441                display_points[1]
442            );
443        }
444
445        assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
446            left == 'j' && right == 'k'
447        });
448        assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
449            left == '\n' && right == 'i'
450        });
451        let mut line_count = 0;
452        assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
453            if left == '\n' {
454                line_count += 1;
455                line_count == 2
456            } else {
457                false
458            }
459        });
460    }
461
462    #[gpui::test]
463    fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
464        cx.set_global(Settings::test(cx));
465        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
466            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
467            assert_eq!(
468                surrounding_word(&snapshot, display_points[1]),
469                display_points[0]..display_points[2]
470            );
471        }
472
473        assert("ˇˇloremˇ  ipsum", cx);
474        assert("ˇloˇremˇ  ipsum", cx);
475        assert("ˇloremˇˇ  ipsum", cx);
476        assert("loremˇ ˇ  ˇipsum", cx);
477        assert("lorem\nˇˇˇ\nipsum", cx);
478        assert("lorem\nˇˇipsumˇ", cx);
479        assert("lorem,ˇˇ ˇipsum", cx);
480        assert("ˇloremˇˇ, ipsum", cx);
481    }
482
483    #[gpui::test]
484    fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
485        cx.set_global(Settings::test(cx));
486        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
487        let font_id = cx
488            .font_cache()
489            .select_font(family_id, &Default::default())
490            .unwrap();
491
492        let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx));
493        let multibuffer = cx.add_model(|cx| {
494            let mut multibuffer = MultiBuffer::new(0);
495            multibuffer.push_excerpts(
496                buffer.clone(),
497                [
498                    ExcerptRange {
499                        context: Point::new(0, 0)..Point::new(1, 4),
500                        primary: None,
501                    },
502                    ExcerptRange {
503                        context: Point::new(2, 0)..Point::new(3, 2),
504                        primary: None,
505                    },
506                ],
507                cx,
508            );
509            multibuffer
510        });
511        let display_map =
512            cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
513        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
514
515        assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
516
517        // Can't move up into the first excerpt's header
518        assert_eq!(
519            up(
520                &snapshot,
521                DisplayPoint::new(2, 2),
522                SelectionGoal::Column(2),
523                false
524            ),
525            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
526        );
527        assert_eq!(
528            up(
529                &snapshot,
530                DisplayPoint::new(2, 0),
531                SelectionGoal::None,
532                false
533            ),
534            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
535        );
536
537        // Move up and down within first excerpt
538        assert_eq!(
539            up(
540                &snapshot,
541                DisplayPoint::new(3, 4),
542                SelectionGoal::Column(4),
543                false
544            ),
545            (DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
546        );
547        assert_eq!(
548            down(
549                &snapshot,
550                DisplayPoint::new(2, 3),
551                SelectionGoal::Column(4),
552                false
553            ),
554            (DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
555        );
556
557        // Move up and down across second excerpt's header
558        assert_eq!(
559            up(
560                &snapshot,
561                DisplayPoint::new(6, 5),
562                SelectionGoal::Column(5),
563                false
564            ),
565            (DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
566        );
567        assert_eq!(
568            down(
569                &snapshot,
570                DisplayPoint::new(3, 4),
571                SelectionGoal::Column(5),
572                false
573            ),
574            (DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
575        );
576
577        // Can't move down off the end
578        assert_eq!(
579            down(
580                &snapshot,
581                DisplayPoint::new(7, 0),
582                SelectionGoal::Column(0),
583                false
584            ),
585            (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
586        );
587        assert_eq!(
588            down(
589                &snapshot,
590                DisplayPoint::new(7, 2),
591                SelectionGoal::Column(2),
592                false
593            ),
594            (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
595        );
596    }
597}