movement.rs

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