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::{
229        display_map::{BlockDisposition, BlockProperties},
230        Buffer, DisplayMap, ExcerptProperties, MultiBuffer,
231    };
232    use gpui::{elements::Empty, Element};
233    use language::Point;
234    use std::sync::Arc;
235
236    #[gpui::test]
237    fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
238        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
239        let font_id = cx
240            .font_cache()
241            .select_font(family_id, &Default::default())
242            .unwrap();
243
244        let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx));
245        let mut excerpt1_header_position = None;
246        let mut excerpt2_header_position = None;
247        let multibuffer = cx.add_model(|cx| {
248            let mut multibuffer = MultiBuffer::new(0);
249            let excerpt1_id = multibuffer.push_excerpt(
250                ExcerptProperties {
251                    buffer: &buffer,
252                    range: Point::new(0, 0)..Point::new(1, 4),
253                },
254                cx,
255            );
256            let excerpt2_id = multibuffer.push_excerpt(
257                ExcerptProperties {
258                    buffer: &buffer,
259                    range: Point::new(2, 0)..Point::new(3, 2),
260                },
261                cx,
262            );
263
264            excerpt1_header_position = Some(
265                multibuffer
266                    .read(cx)
267                    .anchor_in_excerpt(excerpt1_id, language::Anchor::min()),
268            );
269            excerpt2_header_position = Some(
270                multibuffer
271                    .read(cx)
272                    .anchor_in_excerpt(excerpt2_id, language::Anchor::min()),
273            );
274            multibuffer
275        });
276
277        let display_map =
278            cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, cx));
279        display_map.update(cx, |display_map, cx| {
280            display_map.insert_blocks(
281                [
282                    BlockProperties {
283                        position: excerpt1_header_position.unwrap(),
284                        height: 2,
285                        render: Arc::new(|_| Empty::new().boxed()),
286                        disposition: BlockDisposition::Above,
287                    },
288                    BlockProperties {
289                        position: excerpt2_header_position.unwrap(),
290                        height: 3,
291                        render: Arc::new(|_| Empty::new().boxed()),
292                        disposition: BlockDisposition::Above,
293                    },
294                ],
295                cx,
296            )
297        });
298
299        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
300        assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\n\nhijkl\nmn");
301
302        // Can't move up into the first excerpt's header
303        assert_eq!(
304            up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)).unwrap(),
305            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
306        );
307        assert_eq!(
308            up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None).unwrap(),
309            (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
310        );
311
312        // Move up and down within first excerpt
313        assert_eq!(
314            up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)).unwrap(),
315            (DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
316        );
317        assert_eq!(
318            down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)).unwrap(),
319            (DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
320        );
321
322        // Move up and down across second excerpt's header
323        assert_eq!(
324            up(&snapshot, DisplayPoint::new(7, 5), SelectionGoal::Column(5)).unwrap(),
325            (DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
326        );
327        assert_eq!(
328            down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)).unwrap(),
329            (DisplayPoint::new(7, 5), SelectionGoal::Column(5)),
330        );
331
332        // Can't move down off the end
333        assert_eq!(
334            down(&snapshot, DisplayPoint::new(8, 0), SelectionGoal::Column(0)).unwrap(),
335            (DisplayPoint::new(8, 2), SelectionGoal::Column(2)),
336        );
337        assert_eq!(
338            down(&snapshot, DisplayPoint::new(8, 2), SelectionGoal::Column(2)).unwrap(),
339            (DisplayPoint::new(8, 2), SelectionGoal::Column(2)),
340        );
341    }
342
343    #[gpui::test]
344    fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
345        let tab_size = 4;
346        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
347        let font_id = cx
348            .font_cache()
349            .select_font(family_id, &Default::default())
350            .unwrap();
351        let font_size = 14.0;
352
353        let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx);
354        let display_map =
355            cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
356        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
357        assert_eq!(
358            prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)),
359            DisplayPoint::new(0, 7)
360        );
361        assert_eq!(
362            prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
363            DisplayPoint::new(0, 2)
364        );
365        assert_eq!(
366            prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
367            DisplayPoint::new(0, 2)
368        );
369        assert_eq!(
370            prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
371            DisplayPoint::new(0, 0)
372        );
373        assert_eq!(
374            prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
375            DisplayPoint::new(0, 0)
376        );
377
378        assert_eq!(
379            next_word_boundary(&snapshot, DisplayPoint::new(0, 0)),
380            DisplayPoint::new(0, 1)
381        );
382        assert_eq!(
383            next_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
384            DisplayPoint::new(0, 6)
385        );
386        assert_eq!(
387            next_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
388            DisplayPoint::new(0, 6)
389        );
390        assert_eq!(
391            next_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
392            DisplayPoint::new(0, 12)
393        );
394        assert_eq!(
395            next_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
396            DisplayPoint::new(0, 12)
397        );
398    }
399
400    #[gpui::test]
401    fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
402        let tab_size = 4;
403        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
404        let font_id = cx
405            .font_cache()
406            .select_font(family_id, &Default::default())
407            .unwrap();
408        let font_size = 14.0;
409        let buffer = MultiBuffer::build_simple("lorem ipsum   dolor\n    sit", cx);
410        let display_map =
411            cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
412        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
413
414        assert_eq!(
415            surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
416            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
417        );
418        assert_eq!(
419            surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
420            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
421        );
422        assert_eq!(
423            surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
424            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
425        );
426        assert_eq!(
427            surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
428            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
429        );
430        assert_eq!(
431            surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
432            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
433        );
434        assert_eq!(
435            surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
436            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
437        );
438        assert_eq!(
439            surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
440            DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14),
441        );
442        assert_eq!(
443            surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
444            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
445        );
446        assert_eq!(
447            surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
448            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
449        );
450        assert_eq!(
451            surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
452            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
453        );
454        assert_eq!(
455            surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
456            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
457        );
458        assert_eq!(
459            surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
460            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
461        );
462        assert_eq!(
463            surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
464            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
465        );
466        assert_eq!(
467            surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
468            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
469        );
470    }
471}