movement.rs

  1use super::{Bias, DisplayMapSnapshot, DisplayPoint, SelectionGoal};
  2use anyhow::Result;
  3
  4pub fn left(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
  5    if point.column() > 0 {
  6        *point.column_mut() -= 1;
  7    } else if point.row() > 0 {
  8        *point.row_mut() -= 1;
  9        *point.column_mut() = map.line_len(point.row());
 10    }
 11    Ok(map.clip_point(point, Bias::Left))
 12}
 13
 14pub fn right(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
 15    let max_column = map.line_len(point.row());
 16    if point.column() < max_column {
 17        *point.column_mut() += 1;
 18    } else if point.row() < map.max_point().row() {
 19        *point.row_mut() += 1;
 20        *point.column_mut() = 0;
 21    }
 22    Ok(map.clip_point(point, Bias::Right))
 23}
 24
 25pub fn up(
 26    map: &DisplayMapSnapshot,
 27    mut point: DisplayPoint,
 28    goal: SelectionGoal,
 29) -> Result<(DisplayPoint, SelectionGoal)> {
 30    let goal_column = if let SelectionGoal::Column(column) = goal {
 31        column
 32    } else {
 33        map.column_to_chars(point.row(), point.column())
 34    };
 35
 36    loop {
 37        if point.row() > 0 {
 38            *point.row_mut() -= 1;
 39            *point.column_mut() = map.column_from_chars(point.row(), goal_column);
 40            if !map.is_block_line(point.row()) {
 41                break;
 42            }
 43        } else {
 44            point = DisplayPoint::new(0, 0);
 45            break;
 46        }
 47    }
 48
 49    let clip_bias = if point.column() == map.line_len(point.row()) {
 50        Bias::Left
 51    } else {
 52        Bias::Right
 53    };
 54
 55    Ok((
 56        map.clip_point(point, clip_bias),
 57        SelectionGoal::Column(goal_column),
 58    ))
 59}
 60
 61pub fn down(
 62    map: &DisplayMapSnapshot,
 63    mut point: DisplayPoint,
 64    goal: SelectionGoal,
 65) -> Result<(DisplayPoint, SelectionGoal)> {
 66    let max_point = map.max_point();
 67    let goal_column = if let SelectionGoal::Column(column) = goal {
 68        column
 69    } else {
 70        map.column_to_chars(point.row(), point.column())
 71    };
 72
 73    loop {
 74        if point.row() < max_point.row() {
 75            *point.row_mut() += 1;
 76            *point.column_mut() = map.column_from_chars(point.row(), goal_column);
 77            if !map.is_block_line(point.row()) {
 78                break;
 79            }
 80        } else {
 81            point = max_point;
 82            break;
 83        }
 84    }
 85
 86    let clip_bias = if point.column() == map.line_len(point.row()) {
 87        Bias::Left
 88    } else {
 89        Bias::Right
 90    };
 91
 92    Ok((
 93        map.clip_point(point, clip_bias),
 94        SelectionGoal::Column(goal_column),
 95    ))
 96}
 97
 98pub fn line_beginning(
 99    map: &DisplayMapSnapshot,
100    point: DisplayPoint,
101    toggle_indent: bool,
102) -> Result<DisplayPoint> {
103    let (indent, is_blank) = map.line_indent(point.row());
104    if toggle_indent && !is_blank && point.column() != indent {
105        Ok(DisplayPoint::new(point.row(), indent))
106    } else {
107        Ok(DisplayPoint::new(point.row(), 0))
108    }
109}
110
111pub fn line_end(map: &DisplayMapSnapshot, point: DisplayPoint) -> Result<DisplayPoint> {
112    let line_end = DisplayPoint::new(point.row(), map.line_len(point.row()));
113    Ok(map.clip_point(line_end, Bias::Left))
114}
115
116pub fn prev_word_boundary(
117    map: &DisplayMapSnapshot,
118    mut point: DisplayPoint,
119) -> Result<DisplayPoint> {
120    let mut line_start = 0;
121    if point.row() > 0 {
122        if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
123            line_start = indent;
124        }
125    }
126
127    if point.column() == line_start {
128        if point.row() == 0 {
129            return Ok(DisplayPoint::new(0, 0));
130        } else {
131            let row = point.row() - 1;
132            point = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left);
133        }
134    }
135
136    let mut boundary = DisplayPoint::new(point.row(), 0);
137    let mut column = 0;
138    let mut prev_char_kind = CharKind::Newline;
139    for c in map.chars_at(DisplayPoint::new(point.row(), 0)) {
140        if column >= point.column() {
141            break;
142        }
143
144        let char_kind = char_kind(c);
145        if char_kind != prev_char_kind
146            && char_kind != CharKind::Whitespace
147            && char_kind != CharKind::Newline
148        {
149            *boundary.column_mut() = column;
150        }
151
152        prev_char_kind = char_kind;
153        column += c.len_utf8() as u32;
154    }
155    Ok(boundary)
156}
157
158pub fn next_word_boundary(
159    map: &DisplayMapSnapshot,
160    mut point: DisplayPoint,
161) -> Result<DisplayPoint> {
162    let mut prev_char_kind = None;
163    for c in map.chars_at(point) {
164        let char_kind = char_kind(c);
165        if let Some(prev_char_kind) = prev_char_kind {
166            if c == '\n' {
167                break;
168            }
169            if prev_char_kind != char_kind
170                && prev_char_kind != CharKind::Whitespace
171                && prev_char_kind != CharKind::Newline
172            {
173                break;
174            }
175        }
176
177        if c == '\n' {
178            *point.row_mut() += 1;
179            *point.column_mut() = 0;
180        } else {
181            *point.column_mut() += c.len_utf8() as u32;
182        }
183        prev_char_kind = Some(char_kind);
184    }
185    Ok(point)
186}
187
188#[derive(Copy, Clone, Eq, PartialEq)]
189enum CharKind {
190    Newline,
191    Whitespace,
192    Punctuation,
193    Word,
194}
195
196fn char_kind(c: char) -> CharKind {
197    if c == '\n' {
198        CharKind::Newline
199    } else if c.is_whitespace() {
200        CharKind::Whitespace
201    } else if c.is_alphanumeric() || c == '_' {
202        CharKind::Word
203    } else {
204        CharKind::Punctuation
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::{display_map::DisplayMap, Buffer};
212
213    #[gpui::test]
214    fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
215        let tab_size = 4;
216        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
217        let font_id = cx
218            .font_cache()
219            .select_font(family_id, &Default::default())
220            .unwrap();
221        let font_size = 14.0;
222
223        let buffer = cx.add_model(|cx| Buffer::new(0, "a bcΔ defγ hi—jk", cx));
224        let display_map =
225            cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
226        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
227        assert_eq!(
228            prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)).unwrap(),
229            DisplayPoint::new(0, 7)
230        );
231        assert_eq!(
232            prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
233            DisplayPoint::new(0, 2)
234        );
235        assert_eq!(
236            prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)).unwrap(),
237            DisplayPoint::new(0, 2)
238        );
239        assert_eq!(
240            prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)).unwrap(),
241            DisplayPoint::new(0, 0)
242        );
243        assert_eq!(
244            prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)).unwrap(),
245            DisplayPoint::new(0, 0)
246        );
247
248        assert_eq!(
249            next_word_boundary(&snapshot, DisplayPoint::new(0, 0)).unwrap(),
250            DisplayPoint::new(0, 1)
251        );
252        assert_eq!(
253            next_word_boundary(&snapshot, DisplayPoint::new(0, 1)).unwrap(),
254            DisplayPoint::new(0, 6)
255        );
256        assert_eq!(
257            next_word_boundary(&snapshot, DisplayPoint::new(0, 2)).unwrap(),
258            DisplayPoint::new(0, 6)
259        );
260        assert_eq!(
261            next_word_boundary(&snapshot, DisplayPoint::new(0, 6)).unwrap(),
262            DisplayPoint::new(0, 12)
263        );
264        assert_eq!(
265            next_word_boundary(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
266            DisplayPoint::new(0, 12)
267        );
268    }
269}