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_excerpts(
243                buffer.clone(),
244                [
245                    Point::new(0, 0)..Point::new(1, 4),
246                    Point::new(2, 0)..Point::new(3, 2),
247                ],
248                cx,
249            );
250            multibuffer
251        });
252
253        let display_map =
254            cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, 2, 2, cx));
255
256        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
257        assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
258
259        // Can't move up into the first excerpt's header
260        assert_eq!(
261            up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)).unwrap(),
262            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
263        );
264        assert_eq!(
265            up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None).unwrap(),
266            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
267        );
268
269        // Move up and down within first excerpt
270        assert_eq!(
271            up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)).unwrap(),
272            (DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
273        );
274        assert_eq!(
275            down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)).unwrap(),
276            (DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
277        );
278
279        // Move up and down across second excerpt's header
280        assert_eq!(
281            up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)).unwrap(),
282            (DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
283        );
284        assert_eq!(
285            down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)).unwrap(),
286            (DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
287        );
288
289        // Can't move down off the end
290        assert_eq!(
291            down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)).unwrap(),
292            (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
293        );
294        assert_eq!(
295            down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)).unwrap(),
296            (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
297        );
298    }
299
300    #[gpui::test]
301    fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
302        let tab_size = 4;
303        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
304        let font_id = cx
305            .font_cache()
306            .select_font(family_id, &Default::default())
307            .unwrap();
308        let font_size = 14.0;
309
310        let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx);
311        let display_map = cx
312            .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
313        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
314        assert_eq!(
315            prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)),
316            DisplayPoint::new(0, 7)
317        );
318        assert_eq!(
319            prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
320            DisplayPoint::new(0, 2)
321        );
322        assert_eq!(
323            prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
324            DisplayPoint::new(0, 2)
325        );
326        assert_eq!(
327            prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
328            DisplayPoint::new(0, 0)
329        );
330        assert_eq!(
331            prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
332            DisplayPoint::new(0, 0)
333        );
334
335        assert_eq!(
336            next_word_boundary(&snapshot, DisplayPoint::new(0, 0)),
337            DisplayPoint::new(0, 1)
338        );
339        assert_eq!(
340            next_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
341            DisplayPoint::new(0, 6)
342        );
343        assert_eq!(
344            next_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
345            DisplayPoint::new(0, 6)
346        );
347        assert_eq!(
348            next_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
349            DisplayPoint::new(0, 12)
350        );
351        assert_eq!(
352            next_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
353            DisplayPoint::new(0, 12)
354        );
355    }
356
357    #[gpui::test]
358    fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
359        let tab_size = 4;
360        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
361        let font_id = cx
362            .font_cache()
363            .select_font(family_id, &Default::default())
364            .unwrap();
365        let font_size = 14.0;
366        let buffer = MultiBuffer::build_simple("lorem ipsum   dolor\n    sit", cx);
367        let display_map = cx
368            .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
369        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
370
371        assert_eq!(
372            surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
373            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
374        );
375        assert_eq!(
376            surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
377            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
378        );
379        assert_eq!(
380            surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
381            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
382        );
383        assert_eq!(
384            surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
385            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
386        );
387        assert_eq!(
388            surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
389            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
390        );
391        assert_eq!(
392            surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
393            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
394        );
395        assert_eq!(
396            surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
397            DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14),
398        );
399        assert_eq!(
400            surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
401            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
402        );
403        assert_eq!(
404            surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
405            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
406        );
407        assert_eq!(
408            surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
409            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
410        );
411        assert_eq!(
412            surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
413            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
414        );
415        assert_eq!(
416            surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
417            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
418        );
419        assert_eq!(
420            surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
421            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
422        );
423        assert_eq!(
424            surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
425            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
426        );
427    }
428}