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, mut point: DisplayPoint) -> DisplayPoint {
136    let mut line_start = 0;
137    if point.row() > 0 {
138        if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
139            line_start = indent;
140        }
141    }
142
143    if point.column() == line_start {
144        if point.row() == 0 {
145            return DisplayPoint::new(0, 0);
146        } else {
147            let row = point.row() - 1;
148            point = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left);
149        }
150    }
151
152    let mut boundary = DisplayPoint::new(point.row(), 0);
153    let mut column = 0;
154    let mut prev_char_kind = CharKind::Whitespace;
155    for c in map.chars_at(DisplayPoint::new(point.row(), 0)) {
156        if column >= point.column() {
157            break;
158        }
159
160        let char_kind = char_kind(c);
161        if char_kind != prev_char_kind && char_kind != CharKind::Whitespace && c != '\n' {
162            *boundary.column_mut() = column;
163        }
164
165        prev_char_kind = char_kind;
166        column += c.len_utf8() as u32;
167    }
168    boundary
169}
170
171pub fn next_word_end(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
172    let mut prev_char_kind = None;
173    for c in map.chars_at(point) {
174        let char_kind = char_kind(c);
175        if let Some(prev_char_kind) = prev_char_kind {
176            if c == '\n' {
177                break;
178            }
179            if prev_char_kind != char_kind && prev_char_kind != CharKind::Whitespace {
180                break;
181            }
182        }
183
184        if c == '\n' {
185            *point.row_mut() += 1;
186            *point.column_mut() = 0;
187        } else {
188            *point.column_mut() += c.len_utf8() as u32;
189        }
190        prev_char_kind = Some(char_kind);
191    }
192    map.clip_point(point, Bias::Right)
193}
194
195pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
196    let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
197    let text = &map.buffer_snapshot;
198    let next_char_kind = text.chars_at(ix).next().map(char_kind);
199    let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind);
200    prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
201}
202
203pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<DisplayPoint> {
204    let position = map
205        .clip_point(position, Bias::Left)
206        .to_offset(map, Bias::Left);
207    let (range, _) = map.buffer_snapshot.surrounding_word(position);
208    let start = range
209        .start
210        .to_point(&map.buffer_snapshot)
211        .to_display_point(map);
212    let end = range
213        .end
214        .to_point(&map.buffer_snapshot)
215        .to_display_point(map);
216    start..end
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use crate::{Buffer, DisplayMap, MultiBuffer};
223    use language::Point;
224
225    #[gpui::test]
226    fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
227        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
228        let font_id = cx
229            .font_cache()
230            .select_font(family_id, &Default::default())
231            .unwrap();
232
233        let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx));
234        let multibuffer = cx.add_model(|cx| {
235            let mut multibuffer = MultiBuffer::new(0);
236            multibuffer.push_excerpts(
237                buffer.clone(),
238                [
239                    Point::new(0, 0)..Point::new(1, 4),
240                    Point::new(2, 0)..Point::new(3, 2),
241                ],
242                cx,
243            );
244            multibuffer
245        });
246
247        let display_map =
248            cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, 2, 2, cx));
249
250        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
251        assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
252
253        // Can't move up into the first excerpt's header
254        assert_eq!(
255            up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)).unwrap(),
256            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
257        );
258        assert_eq!(
259            up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None).unwrap(),
260            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
261        );
262
263        // Move up and down within first excerpt
264        assert_eq!(
265            up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)).unwrap(),
266            (DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
267        );
268        assert_eq!(
269            down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)).unwrap(),
270            (DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
271        );
272
273        // Move up and down across second excerpt's header
274        assert_eq!(
275            up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)).unwrap(),
276            (DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
277        );
278        assert_eq!(
279            down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)).unwrap(),
280            (DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
281        );
282
283        // Can't move down off the end
284        assert_eq!(
285            down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)).unwrap(),
286            (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
287        );
288        assert_eq!(
289            down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)).unwrap(),
290            (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
291        );
292    }
293
294    #[gpui::test]
295    fn test_previous_word_start(cx: &mut gpui::MutableAppContext) {
296        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
297            let (snapshot, display_points) = marked_snapshot(marked_text, cx);
298            dbg!(&display_points);
299            assert_eq!(
300                previous_word_start(&snapshot, display_points[1]),
301                display_points[0]
302            );
303        }
304
305        assert("\n|   |lorem", cx);
306        assert("    |lorem|", cx);
307        assert("    |lor|em", cx);
308        assert("\nlorem\n|   |ipsum", cx);
309        assert("\n\n|\n|", cx);
310        assert("    |lorem  |ipsum", cx);
311        assert("lorem|-|ipsum", cx);
312        assert("lorem|-#$@|ipsum", cx);
313        assert("|lorem_|ipsum", cx);
314    }
315
316    #[gpui::test]
317    fn test_next_word_end(cx: &mut gpui::MutableAppContext) {
318        fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
319            let (snapshot, display_points) = marked_snapshot(marked_text, cx);
320            assert_eq!(
321                next_word_end(&snapshot, display_points[0]),
322                display_points[1]
323            );
324        }
325
326        assert("\n|   lorem|", cx);
327        assert("    |lorem|", cx);
328        assert("    lor|em|", cx);
329        assert("    lorem|    |\nipsum\n", cx);
330        assert("\n|\n|\n\n", cx);
331        assert("lorem|    ipsum|   ", cx);
332        assert("lorem|-|ipsum", cx);
333        assert("lorem|#$@-|ipsum", cx);
334        assert("lorem|_ipsum|", cx);
335    }
336
337    // Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
338    fn marked_snapshot(
339        text: &str,
340        cx: &mut gpui::MutableAppContext,
341    ) -> (DisplaySnapshot, Vec<DisplayPoint>) {
342        let mut marked_offsets = Vec::new();
343        let chunks = text.split('|');
344        let mut text = String::new();
345
346        for chunk in chunks {
347            text.push_str(chunk);
348            marked_offsets.push(text.len());
349        }
350        marked_offsets.pop();
351
352        let tab_size = 4;
353        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
354        let font_id = cx
355            .font_cache()
356            .select_font(family_id, &Default::default())
357            .unwrap();
358        let font_size = 14.0;
359
360        let buffer = MultiBuffer::build_simple(&text, cx);
361        let display_map = cx
362            .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
363        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
364        let marked_display_points = marked_offsets
365            .into_iter()
366            .map(|offset| offset.to_display_point(&snapshot))
367            .collect();
368
369        (snapshot, marked_display_points)
370    }
371
372    #[gpui::test]
373    fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
374        let tab_size = 4;
375        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
376        let font_id = cx
377            .font_cache()
378            .select_font(family_id, &Default::default())
379            .unwrap();
380        let font_size = 14.0;
381
382        let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx);
383        let display_map = cx
384            .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
385        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
386        assert_eq!(
387            previous_word_start(&snapshot, DisplayPoint::new(0, 12)),
388            DisplayPoint::new(0, 7)
389        );
390        assert_eq!(
391            previous_word_start(&snapshot, DisplayPoint::new(0, 7)),
392            DisplayPoint::new(0, 2)
393        );
394        assert_eq!(
395            previous_word_start(&snapshot, DisplayPoint::new(0, 6)),
396            DisplayPoint::new(0, 2)
397        );
398        assert_eq!(
399            previous_word_start(&snapshot, DisplayPoint::new(0, 2)),
400            DisplayPoint::new(0, 0)
401        );
402        assert_eq!(
403            previous_word_start(&snapshot, DisplayPoint::new(0, 1)),
404            DisplayPoint::new(0, 0)
405        );
406
407        assert_eq!(
408            next_word_end(&snapshot, DisplayPoint::new(0, 0)),
409            DisplayPoint::new(0, 1)
410        );
411        assert_eq!(
412            next_word_end(&snapshot, DisplayPoint::new(0, 1)),
413            DisplayPoint::new(0, 6)
414        );
415        assert_eq!(
416            next_word_end(&snapshot, DisplayPoint::new(0, 2)),
417            DisplayPoint::new(0, 6)
418        );
419        assert_eq!(
420            next_word_end(&snapshot, DisplayPoint::new(0, 6)),
421            DisplayPoint::new(0, 12)
422        );
423        assert_eq!(
424            next_word_end(&snapshot, DisplayPoint::new(0, 7)),
425            DisplayPoint::new(0, 12)
426        );
427    }
428
429    #[gpui::test]
430    fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
431        let tab_size = 4;
432        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
433        let font_id = cx
434            .font_cache()
435            .select_font(family_id, &Default::default())
436            .unwrap();
437        let font_size = 14.0;
438        let buffer = MultiBuffer::build_simple("lorem ipsum   dolor\n    sit\n\n\n\n", cx);
439        let display_map = cx
440            .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
441        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
442
443        assert_eq!(
444            surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
445            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
446        );
447        assert_eq!(
448            surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
449            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
450        );
451        assert_eq!(
452            surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
453            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
454        );
455        assert_eq!(
456            surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
457            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
458        );
459        assert_eq!(
460            surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
461            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
462        );
463        assert_eq!(
464            surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
465            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
466        );
467        assert_eq!(
468            surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
469            DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14),
470        );
471        assert_eq!(
472            surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
473            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
474        );
475        assert_eq!(
476            surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
477            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
478        );
479        assert_eq!(
480            surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
481            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
482        );
483        assert_eq!(
484            surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
485            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
486        );
487        assert_eq!(
488            surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
489            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
490        );
491        assert_eq!(
492            surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
493            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
494        );
495        assert_eq!(
496            surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
497            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
498        );
499
500        // Don't consider runs of multiple newlines to be a "word"
501        assert_eq!(
502            surrounding_word(&snapshot, DisplayPoint::new(3, 0)),
503            DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
504        );
505    }
506}