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