movement.rs

  1use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
  2use crate::ToPoint;
  3use anyhow::Result;
  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::{DisplayMap, MultiBuffer};
248
249    #[gpui::test]
250    fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
251        let tab_size = 4;
252        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
253        let font_id = cx
254            .font_cache()
255            .select_font(family_id, &Default::default())
256            .unwrap();
257        let font_size = 14.0;
258
259        let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx);
260        let display_map =
261            cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
262        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
263        assert_eq!(
264            prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)),
265            DisplayPoint::new(0, 7)
266        );
267        assert_eq!(
268            prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
269            DisplayPoint::new(0, 2)
270        );
271        assert_eq!(
272            prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
273            DisplayPoint::new(0, 2)
274        );
275        assert_eq!(
276            prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
277            DisplayPoint::new(0, 0)
278        );
279        assert_eq!(
280            prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
281            DisplayPoint::new(0, 0)
282        );
283
284        assert_eq!(
285            next_word_boundary(&snapshot, DisplayPoint::new(0, 0)),
286            DisplayPoint::new(0, 1)
287        );
288        assert_eq!(
289            next_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
290            DisplayPoint::new(0, 6)
291        );
292        assert_eq!(
293            next_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
294            DisplayPoint::new(0, 6)
295        );
296        assert_eq!(
297            next_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
298            DisplayPoint::new(0, 12)
299        );
300        assert_eq!(
301            next_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
302            DisplayPoint::new(0, 12)
303        );
304    }
305
306    #[gpui::test]
307    fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
308        let tab_size = 4;
309        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
310        let font_id = cx
311            .font_cache()
312            .select_font(family_id, &Default::default())
313            .unwrap();
314        let font_size = 14.0;
315        let buffer = MultiBuffer::build_simple("lorem ipsum   dolor\n    sit", cx);
316        let display_map =
317            cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
318        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
319
320        assert_eq!(
321            surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
322            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
323        );
324        assert_eq!(
325            surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
326            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
327        );
328        assert_eq!(
329            surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
330            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
331        );
332        assert_eq!(
333            surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
334            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
335        );
336        assert_eq!(
337            surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
338            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
339        );
340        assert_eq!(
341            surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
342            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
343        );
344        assert_eq!(
345            surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
346            DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14)
347        );
348        assert_eq!(
349            surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
350            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
351        );
352        assert_eq!(
353            surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
354            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
355        );
356        assert_eq!(
357            surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
358            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
359        );
360        assert_eq!(
361            surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
362            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4)
363        );
364        assert_eq!(
365            surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
366            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4)
367        );
368        assert_eq!(
369            surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
370            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7)
371        );
372        assert_eq!(
373            surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
374            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7)
375        );
376    }
377}