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