movement.rs

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