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
272    #[gpui::test]
273    fn test_previous_word_start(cx: &mut gpui::MutableAppContext) {
274        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
275            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
276            assert_eq!(
277                previous_word_start(&snapshot, display_points[1]),
278                display_points[0]
279            );
280        }
281
282        assert("\n|   |lorem", cx);
283        assert("|\n|   lorem", cx);
284        assert("    |lorem|", cx);
285        assert("|    |lorem", cx);
286        assert("    |lor|em", cx);
287        assert("\nlorem\n|   |ipsum", cx);
288        assert("\n\n|\n|", cx);
289        assert("    |lorem  |ipsum", cx);
290        assert("lorem|-|ipsum", cx);
291        assert("lorem|-#$@|ipsum", cx);
292        assert("|lorem_|ipsum", cx);
293        assert(" |defγ|", cx);
294        assert(" |bcΔ|", cx);
295        assert(" ab|——|cd", cx);
296    }
297
298    #[gpui::test]
299    fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) {
300        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
301            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
302            assert_eq!(
303                previous_subword_start(&snapshot, display_points[1]),
304                display_points[0]
305            );
306        }
307
308        // Subword boundaries are respected
309        assert("lorem_|ip|sum", cx);
310        assert("lorem_|ipsum|", cx);
311        assert("|lorem_|ipsum", cx);
312        assert("lorem_|ipsum_|dolor", cx);
313        assert("lorem|Ip|sum", cx);
314        assert("lorem|Ipsum|", cx);
315
316        // Word boundaries are still respected
317        assert("\n|   |lorem", cx);
318        assert("    |lorem|", cx);
319        assert("    |lor|em", cx);
320        assert("\nlorem\n|   |ipsum", cx);
321        assert("\n\n|\n|", cx);
322        assert("    |lorem  |ipsum", cx);
323        assert("lorem|-|ipsum", cx);
324        assert("lorem|-#$@|ipsum", cx);
325        assert(" |defγ|", cx);
326        assert(" bc|Δ|", cx);
327        assert(" |bcδ|", cx);
328        assert(" ab|——|cd", cx);
329    }
330
331    #[gpui::test]
332    fn test_find_preceding_boundary(cx: &mut gpui::MutableAppContext) {
333        fn assert(
334            marked_text: &str,
335            cx: &mut gpui::MutableAppContext,
336            is_boundary: impl FnMut(char, char) -> bool,
337        ) {
338            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
339            assert_eq!(
340                find_preceding_boundary(&snapshot, display_points[1], is_boundary),
341                display_points[0]
342            );
343        }
344
345        assert("abc|def\ngh\nij|k", cx, |left, right| {
346            left == 'c' && right == 'd'
347        });
348        assert("abcdef\n|gh\nij|k", cx, |left, right| {
349            left == '\n' && right == 'g'
350        });
351        let mut line_count = 0;
352        assert("abcdef\n|gh\nij|k", cx, |left, _| {
353            if left == '\n' {
354                line_count += 1;
355                line_count == 2
356            } else {
357                false
358            }
359        });
360    }
361
362    #[gpui::test]
363    fn test_next_word_end(cx: &mut gpui::MutableAppContext) {
364        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
365            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
366            assert_eq!(
367                next_word_end(&snapshot, display_points[0]),
368                display_points[1]
369            );
370        }
371
372        assert("\n|   lorem|", cx);
373        assert("    |lorem|", cx);
374        assert("    lor|em|", cx);
375        assert("    lorem|    |\nipsum\n", cx);
376        assert("\n|\n|\n\n", cx);
377        assert("lorem|    ipsum|   ", cx);
378        assert("lorem|-|ipsum", cx);
379        assert("lorem|#$@-|ipsum", cx);
380        assert("lorem|_ipsum|", cx);
381        assert(" |bcΔ|", cx);
382        assert(" ab|——|cd", cx);
383    }
384
385    #[gpui::test]
386    fn test_next_subword_end(cx: &mut gpui::MutableAppContext) {
387        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
388            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
389            assert_eq!(
390                next_subword_end(&snapshot, display_points[0]),
391                display_points[1]
392            );
393        }
394
395        // Subword boundaries are respected
396        assert("lo|rem|_ipsum", cx);
397        assert("|lorem|_ipsum", cx);
398        assert("lorem|_ipsum|", cx);
399        assert("lorem|_ipsum|_dolor", cx);
400        assert("lo|rem|Ipsum", cx);
401        assert("lorem|Ipsum|Dolor", cx);
402
403        // Word boundaries are still respected
404        assert("\n|   lorem|", cx);
405        assert("    |lorem|", cx);
406        assert("    lor|em|", cx);
407        assert("    lorem|    |\nipsum\n", cx);
408        assert("\n|\n|\n\n", cx);
409        assert("lorem|    ipsum|   ", cx);
410        assert("lorem|-|ipsum", cx);
411        assert("lorem|#$@-|ipsum", cx);
412        assert("lorem|_ipsum|", cx);
413        assert(" |bc|Δ", cx);
414        assert(" ab|——|cd", cx);
415    }
416
417    #[gpui::test]
418    fn test_find_boundary(cx: &mut gpui::MutableAppContext) {
419        fn assert(
420            marked_text: &str,
421            cx: &mut gpui::MutableAppContext,
422            is_boundary: impl FnMut(char, char) -> bool,
423        ) {
424            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
425            assert_eq!(
426                find_boundary(&snapshot, display_points[0], is_boundary),
427                display_points[1]
428            );
429        }
430
431        assert("abc|def\ngh\nij|k", cx, |left, right| {
432            left == 'j' && right == 'k'
433        });
434        assert("ab|cdef\ngh\n|ijk", cx, |left, right| {
435            left == '\n' && right == 'i'
436        });
437        let mut line_count = 0;
438        assert("abc|def\ngh\n|ijk", cx, |left, _| {
439            if left == '\n' {
440                line_count += 1;
441                line_count == 2
442            } else {
443                false
444            }
445        });
446    }
447
448    #[gpui::test]
449    fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
450        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
451            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
452            assert_eq!(
453                surrounding_word(&snapshot, display_points[1]),
454                display_points[0]..display_points[2]
455            );
456        }
457
458        assert("||lorem|  ipsum", cx);
459        assert("|lo|rem|  ipsum", cx);
460        assert("|lorem||  ipsum", cx);
461        assert("lorem| |  |ipsum", cx);
462        assert("lorem\n|||\nipsum", cx);
463        assert("lorem\n||ipsum|", cx);
464        assert("lorem,|| |ipsum", cx);
465        assert("|lorem||, ipsum", cx);
466    }
467
468    #[gpui::test]
469    fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
470        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
471        let font_id = cx
472            .font_cache()
473            .select_font(family_id, &Default::default())
474            .unwrap();
475
476        let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx));
477        let multibuffer = cx.add_model(|cx| {
478            let mut multibuffer = MultiBuffer::new(0);
479            multibuffer.push_excerpts(
480                buffer.clone(),
481                [
482                    Point::new(0, 0)..Point::new(1, 4),
483                    Point::new(2, 0)..Point::new(3, 2),
484                ],
485                cx,
486            );
487            multibuffer
488        });
489        let display_map =
490            cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, 2, 2, cx));
491        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
492
493        assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
494
495        // Can't move up into the first excerpt's header
496        assert_eq!(
497            up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)),
498            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
499        );
500        assert_eq!(
501            up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None),
502            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
503        );
504
505        // Move up and down within first excerpt
506        assert_eq!(
507            up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
508            (DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
509        );
510        assert_eq!(
511            down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
512            (DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
513        );
514
515        // Move up and down across second excerpt's header
516        assert_eq!(
517            up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
518            (DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
519        );
520        assert_eq!(
521            down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
522            (DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
523        );
524
525        // Can't move down off the end
526        assert_eq!(
527            down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)),
528            (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
529        );
530        assert_eq!(
531            down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
532            (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
533        );
534    }
535}