movement.rs

  1use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
  2use anyhow::Result;
  3use language::multi_buffer::ToPoint;
  4use std::{cmp, ops::Range};
  5
  6pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<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    Ok(map.clip_point(point, Bias::Left))
 14}
 15
 16pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<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    Ok(map.clip_point(point, Bias::Right))
 25}
 26
 27pub fn up(
 28    map: &DisplaySnapshot,
 29    mut point: DisplayPoint,
 30    goal: SelectionGoal,
 31) -> Result<(DisplayPoint, SelectionGoal)> {
 32    let goal_column = if let SelectionGoal::Column(column) = goal {
 33        column
 34    } else {
 35        map.column_to_chars(point.row(), point.column())
 36    };
 37
 38    loop {
 39        if point.row() > 0 {
 40            *point.row_mut() -= 1;
 41            *point.column_mut() = map.column_from_chars(point.row(), goal_column);
 42            if !map.is_block_line(point.row()) {
 43                break;
 44            }
 45        } else {
 46            point = DisplayPoint::new(0, 0);
 47            break;
 48        }
 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    mut point: DisplayPoint,
 66    goal: SelectionGoal,
 67) -> Result<(DisplayPoint, SelectionGoal)> {
 68    let max_point = map.max_point();
 69    let goal_column = if let SelectionGoal::Column(column) = goal {
 70        column
 71    } else {
 72        map.column_to_chars(point.row(), point.column())
 73    };
 74
 75    loop {
 76        if point.row() < max_point.row() {
 77            *point.row_mut() += 1;
 78            *point.column_mut() = map.column_from_chars(point.row(), goal_column);
 79            if !map.is_block_line(point.row()) {
 80                break;
 81            }
 82        } else {
 83            point = max_point;
 84            break;
 85        }
 86    }
 87
 88    let clip_bias = if point.column() == map.line_len(point.row()) {
 89        Bias::Left
 90    } else {
 91        Bias::Right
 92    };
 93
 94    Ok((
 95        map.clip_point(point, clip_bias),
 96        SelectionGoal::Column(goal_column),
 97    ))
 98}
 99
100pub fn line_beginning(
101    map: &DisplaySnapshot,
102    point: DisplayPoint,
103    toggle_indent: bool,
104) -> DisplayPoint {
105    let (indent, is_blank) = map.line_indent(point.row());
106    if toggle_indent && !is_blank && point.column() != indent {
107        DisplayPoint::new(point.row(), indent)
108    } else {
109        DisplayPoint::new(point.row(), 0)
110    }
111}
112
113pub fn line_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
114    let line_end = DisplayPoint::new(point.row(), map.line_len(point.row()));
115    map.clip_point(line_end, Bias::Left)
116}
117
118pub fn prev_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
119    let mut line_start = 0;
120    if point.row() > 0 {
121        if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
122            line_start = indent;
123        }
124    }
125
126    if point.column() == line_start {
127        if point.row() == 0 {
128            return DisplayPoint::new(0, 0);
129        } else {
130            let row = point.row() - 1;
131            point = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left);
132        }
133    }
134
135    let mut boundary = DisplayPoint::new(point.row(), 0);
136    let mut column = 0;
137    let mut prev_char_kind = CharKind::Newline;
138    for c in map.chars_at(DisplayPoint::new(point.row(), 0)) {
139        if column >= point.column() {
140            break;
141        }
142
143        let char_kind = char_kind(c);
144        if char_kind != prev_char_kind
145            && char_kind != CharKind::Whitespace
146            && char_kind != CharKind::Newline
147        {
148            *boundary.column_mut() = column;
149        }
150
151        prev_char_kind = char_kind;
152        column += c.len_utf8() as u32;
153    }
154    boundary
155}
156
157pub fn next_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
158    let mut prev_char_kind = None;
159    for c in map.chars_at(point) {
160        let char_kind = char_kind(c);
161        if let Some(prev_char_kind) = prev_char_kind {
162            if c == '\n' {
163                break;
164            }
165            if prev_char_kind != char_kind
166                && prev_char_kind != CharKind::Whitespace
167                && prev_char_kind != CharKind::Newline
168            {
169                break;
170            }
171        }
172
173        if c == '\n' {
174            *point.row_mut() += 1;
175            *point.column_mut() = 0;
176        } else {
177            *point.column_mut() += c.len_utf8() as u32;
178        }
179        prev_char_kind = Some(char_kind);
180    }
181    point
182}
183
184pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
185    let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
186    let text = &map.buffer_snapshot;
187    let next_char_kind = text.chars_at(ix).next().map(char_kind);
188    let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind);
189    prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
190}
191
192pub fn surrounding_word(map: &DisplaySnapshot, point: DisplayPoint) -> Range<DisplayPoint> {
193    let mut start = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
194    let mut end = start;
195
196    let text = &map.buffer_snapshot;
197    let mut next_chars = text.chars_at(start).peekable();
198    let mut prev_chars = text.reversed_chars_at(start).peekable();
199    let word_kind = cmp::max(
200        prev_chars.peek().copied().map(char_kind),
201        next_chars.peek().copied().map(char_kind),
202    );
203
204    for ch in prev_chars {
205        if Some(char_kind(ch)) == word_kind {
206            start -= ch.len_utf8();
207        } else {
208            break;
209        }
210    }
211
212    for ch in next_chars {
213        if Some(char_kind(ch)) == word_kind {
214            end += ch.len_utf8();
215        } else {
216            break;
217        }
218    }
219
220    start.to_point(&map.buffer_snapshot).to_display_point(map)
221        ..end.to_point(&map.buffer_snapshot).to_display_point(map)
222}
223
224#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
225enum CharKind {
226    Newline,
227    Punctuation,
228    Whitespace,
229    Word,
230}
231
232fn char_kind(c: char) -> CharKind {
233    if c == '\n' {
234        CharKind::Newline
235    } else if c.is_whitespace() {
236        CharKind::Whitespace
237    } else if c.is_alphanumeric() || c == '_' {
238        CharKind::Word
239    } else {
240        CharKind::Punctuation
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use crate::display_map::DisplayMap;
248    use language::MultiBuffer;
249
250    #[gpui::test]
251    fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
252        let tab_size = 4;
253        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
254        let font_id = cx
255            .font_cache()
256            .select_font(family_id, &Default::default())
257            .unwrap();
258        let font_size = 14.0;
259
260        let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx);
261        let display_map =
262            cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
263        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
264        assert_eq!(
265            prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)),
266            DisplayPoint::new(0, 7)
267        );
268        assert_eq!(
269            prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
270            DisplayPoint::new(0, 2)
271        );
272        assert_eq!(
273            prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
274            DisplayPoint::new(0, 2)
275        );
276        assert_eq!(
277            prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
278            DisplayPoint::new(0, 0)
279        );
280        assert_eq!(
281            prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
282            DisplayPoint::new(0, 0)
283        );
284
285        assert_eq!(
286            next_word_boundary(&snapshot, DisplayPoint::new(0, 0)),
287            DisplayPoint::new(0, 1)
288        );
289        assert_eq!(
290            next_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
291            DisplayPoint::new(0, 6)
292        );
293        assert_eq!(
294            next_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
295            DisplayPoint::new(0, 6)
296        );
297        assert_eq!(
298            next_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
299            DisplayPoint::new(0, 12)
300        );
301        assert_eq!(
302            next_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
303            DisplayPoint::new(0, 12)
304        );
305    }
306
307    #[gpui::test]
308    fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
309        let tab_size = 4;
310        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
311        let font_id = cx
312            .font_cache()
313            .select_font(family_id, &Default::default())
314            .unwrap();
315        let font_size = 14.0;
316        let buffer = MultiBuffer::build_simple("lorem ipsum   dolor\n    sit", cx);
317        let display_map =
318            cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
319        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
320
321        assert_eq!(
322            surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
323            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
324        );
325        assert_eq!(
326            surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
327            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
328        );
329        assert_eq!(
330            surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
331            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
332        );
333        assert_eq!(
334            surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
335            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
336        );
337        assert_eq!(
338            surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
339            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
340        );
341        assert_eq!(
342            surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
343            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
344        );
345        assert_eq!(
346            surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
347            DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14)
348        );
349        assert_eq!(
350            surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
351            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
352        );
353        assert_eq!(
354            surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
355            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
356        );
357        assert_eq!(
358            surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
359            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
360        );
361        assert_eq!(
362            surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
363            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4)
364        );
365        assert_eq!(
366            surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
367            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4)
368        );
369        assert_eq!(
370            surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
371            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7)
372        );
373        assert_eq!(
374            surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
375            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7)
376        );
377    }
378}