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 prev_word_boundary(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::Newline;
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
162            && char_kind != CharKind::Whitespace
163            && char_kind != CharKind::Newline
164        {
165            *boundary.column_mut() = column;
166        }
167
168        prev_char_kind = char_kind;
169        column += c.len_utf8() as u32;
170    }
171    boundary
172}
173
174pub fn next_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
175    let mut prev_char_kind = None;
176    for c in map.chars_at(point) {
177        let char_kind = char_kind(c);
178        if let Some(prev_char_kind) = prev_char_kind {
179            if c == '\n' {
180                break;
181            }
182            if prev_char_kind != char_kind
183                && prev_char_kind != CharKind::Whitespace
184                && prev_char_kind != CharKind::Newline
185            {
186                break;
187            }
188        }
189
190        if c == '\n' {
191            *point.row_mut() += 1;
192            *point.column_mut() = 0;
193        } else {
194            *point.column_mut() += c.len_utf8() as u32;
195        }
196        prev_char_kind = Some(char_kind);
197    }
198    map.clip_point(point, Bias::Right)
199}
200
201pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
202    let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
203    let text = &map.buffer_snapshot;
204    let next_char_kind = text.chars_at(ix).next().map(char_kind);
205    let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind);
206    prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
207}
208
209pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<DisplayPoint> {
210    let position = map
211        .clip_point(position, Bias::Left)
212        .to_offset(map, Bias::Left);
213    let (range, _) = map.buffer_snapshot.surrounding_word(position);
214    let start = range
215        .start
216        .to_point(&map.buffer_snapshot)
217        .to_display_point(map);
218    let end = range
219        .end
220        .to_point(&map.buffer_snapshot)
221        .to_display_point(map);
222    start..end
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use crate::{Buffer, DisplayMap, MultiBuffer};
229    use language::Point;
230
231    #[gpui::test]
232    fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
233        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
234        let font_id = cx
235            .font_cache()
236            .select_font(family_id, &Default::default())
237            .unwrap();
238
239        let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx));
240        let multibuffer = cx.add_model(|cx| {
241            let mut multibuffer = MultiBuffer::new(0);
242            multibuffer.push_excerpt(buffer.clone(), Point::new(0, 0)..Point::new(1, 4), cx);
243            multibuffer.push_excerpt(buffer.clone(), Point::new(2, 0)..Point::new(3, 2), cx);
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_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
296        let tab_size = 4;
297        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
298        let font_id = cx
299            .font_cache()
300            .select_font(family_id, &Default::default())
301            .unwrap();
302        let font_size = 14.0;
303
304        let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx);
305        let display_map = cx
306            .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
307        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
308        assert_eq!(
309            prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)),
310            DisplayPoint::new(0, 7)
311        );
312        assert_eq!(
313            prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
314            DisplayPoint::new(0, 2)
315        );
316        assert_eq!(
317            prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
318            DisplayPoint::new(0, 2)
319        );
320        assert_eq!(
321            prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
322            DisplayPoint::new(0, 0)
323        );
324        assert_eq!(
325            prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
326            DisplayPoint::new(0, 0)
327        );
328
329        assert_eq!(
330            next_word_boundary(&snapshot, DisplayPoint::new(0, 0)),
331            DisplayPoint::new(0, 1)
332        );
333        assert_eq!(
334            next_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
335            DisplayPoint::new(0, 6)
336        );
337        assert_eq!(
338            next_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
339            DisplayPoint::new(0, 6)
340        );
341        assert_eq!(
342            next_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
343            DisplayPoint::new(0, 12)
344        );
345        assert_eq!(
346            next_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
347            DisplayPoint::new(0, 12)
348        );
349    }
350
351    #[gpui::test]
352    fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
353        let tab_size = 4;
354        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
355        let font_id = cx
356            .font_cache()
357            .select_font(family_id, &Default::default())
358            .unwrap();
359        let font_size = 14.0;
360        let buffer = MultiBuffer::build_simple("lorem ipsum   dolor\n    sit", 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
365        assert_eq!(
366            surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
367            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
368        );
369        assert_eq!(
370            surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
371            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
372        );
373        assert_eq!(
374            surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
375            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
376        );
377        assert_eq!(
378            surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
379            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
380        );
381        assert_eq!(
382            surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
383            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
384        );
385        assert_eq!(
386            surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
387            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
388        );
389        assert_eq!(
390            surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
391            DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14),
392        );
393        assert_eq!(
394            surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
395            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
396        );
397        assert_eq!(
398            surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
399            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
400        );
401        assert_eq!(
402            surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
403            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
404        );
405        assert_eq!(
406            surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
407            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
408        );
409        assert_eq!(
410            surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
411            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
412        );
413        assert_eq!(
414            surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
415            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
416        );
417        assert_eq!(
418            surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
419            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
420        );
421    }
422}