1use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
2use crate::{char_kind, CharKind, ToPoint};
3use language::Point;
4use std::ops::Range;
5
6pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> 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 map.clip_point(point, Bias::Left)
14}
15
16pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> 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 map.clip_point(point, Bias::Right)
25}
26
27pub fn up(
28 map: &DisplaySnapshot,
29 start: DisplayPoint,
30 goal: SelectionGoal,
31 preserve_column_at_start: bool,
32) -> (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 if preserve_column_at_start {
47 return (start, goal);
48 } else {
49 point = DisplayPoint::new(0, 0);
50 goal_column = 0;
51 }
52
53 let clip_bias = if point.column() == map.line_len(point.row()) {
54 Bias::Left
55 } else {
56 Bias::Right
57 };
58
59 (
60 map.clip_point(point, clip_bias),
61 SelectionGoal::Column(goal_column),
62 )
63}
64
65pub fn down(
66 map: &DisplaySnapshot,
67 start: DisplayPoint,
68 goal: SelectionGoal,
69 preserve_column_at_end: bool,
70) -> (DisplayPoint, SelectionGoal) {
71 let mut goal_column = if let SelectionGoal::Column(column) = goal {
72 column
73 } else {
74 map.column_to_chars(start.row(), start.column())
75 };
76
77 let next_row = start.row() + 1;
78 let mut point = map.clip_point(DisplayPoint::new(next_row, 0), Bias::Right);
79 if point.row() > start.row() {
80 *point.column_mut() = map.column_from_chars(point.row(), goal_column);
81 } else if preserve_column_at_end {
82 return (start, goal);
83 } else {
84 point = map.max_point();
85 goal_column = map.column_to_chars(point.row(), point.column())
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 (
95 map.clip_point(point, clip_bias),
96 SelectionGoal::Column(goal_column),
97 )
98}
99
100pub fn line_beginning(
101 map: &DisplaySnapshot,
102 display_point: DisplayPoint,
103 stop_at_soft_boundaries: bool,
104) -> DisplayPoint {
105 let point = display_point.to_point(map);
106 let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
107 let indent_start = Point::new(
108 point.row,
109 map.buffer_snapshot.indent_size_for_line(point.row).len,
110 )
111 .to_display_point(map);
112 let line_start = map.prev_line_boundary(point).1;
113
114 if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start
115 {
116 soft_line_start
117 } else if stop_at_soft_boundaries && display_point != indent_start {
118 indent_start
119 } else {
120 line_start
121 }
122}
123
124pub fn line_end(
125 map: &DisplaySnapshot,
126 display_point: DisplayPoint,
127 stop_at_soft_boundaries: bool,
128) -> DisplayPoint {
129 let soft_line_end = map.clip_point(
130 DisplayPoint::new(display_point.row(), map.line_len(display_point.row())),
131 Bias::Left,
132 );
133 if stop_at_soft_boundaries && display_point != soft_line_end {
134 soft_line_end
135 } else {
136 map.next_line_boundary(display_point.to_point(map)).1
137 }
138}
139
140pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
141 find_preceding_boundary(map, point, |left, right| {
142 (char_kind(left) != char_kind(right) && !right.is_whitespace()) || left == '\n'
143 })
144}
145
146pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
147 find_preceding_boundary(map, point, |left, right| {
148 let is_word_start = char_kind(left) != char_kind(right) && !right.is_whitespace();
149 let is_subword_start =
150 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
151 is_word_start || is_subword_start || left == '\n'
152 })
153}
154
155pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
156 find_boundary(map, point, |left, right| {
157 (char_kind(left) != char_kind(right) && !left.is_whitespace()) || right == '\n'
158 })
159}
160
161pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
162 find_boundary(map, point, |left, right| {
163 let is_word_end = (char_kind(left) != char_kind(right)) && !left.is_whitespace();
164 let is_subword_end =
165 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
166 is_word_end || is_subword_end || right == '\n'
167 })
168}
169
170/// Scans for a boundary from the start of each line preceding the given end point until a boundary
171/// is found, indicated by the given predicate returning true. The predicate is called with the
172/// character to the left and right of the candidate boundary location, and will be called with `\n`
173/// characters indicating the start or end of a line. If the predicate returns true multiple times
174/// on a line, the *rightmost* boundary is returned.
175pub fn find_preceding_boundary(
176 map: &DisplaySnapshot,
177 end: DisplayPoint,
178 mut is_boundary: impl FnMut(char, char) -> bool,
179) -> DisplayPoint {
180 let mut point = end;
181 loop {
182 *point.column_mut() = 0;
183 if point.row() > 0 {
184 if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
185 *point.column_mut() = indent;
186 }
187 }
188
189 let mut boundary = None;
190 let mut prev_ch = if point.is_zero() { None } else { Some('\n') };
191 for ch in map.chars_at(point) {
192 if point >= end {
193 break;
194 }
195
196 if let Some(prev_ch) = prev_ch {
197 if is_boundary(prev_ch, ch) {
198 boundary = Some(point);
199 }
200 }
201
202 if ch == '\n' {
203 break;
204 }
205
206 prev_ch = Some(ch);
207 *point.column_mut() += ch.len_utf8() as u32;
208 }
209
210 if let Some(boundary) = boundary {
211 return boundary;
212 } else if point.row() == 0 {
213 return DisplayPoint::zero();
214 } else {
215 *point.row_mut() -= 1;
216 }
217 }
218}
219
220/// Scans for a boundary following the given start point until a boundary is found, indicated by the
221/// given predicate returning true. The predicate is called with the character to the left and right
222/// of the candidate boundary location, and will be called with `\n` characters indicating the start
223/// or end of a line.
224pub fn find_boundary(
225 map: &DisplaySnapshot,
226 mut point: DisplayPoint,
227 mut is_boundary: impl FnMut(char, char) -> bool,
228) -> DisplayPoint {
229 let mut prev_ch = None;
230 for ch in map.chars_at(point) {
231 if let Some(prev_ch) = prev_ch {
232 if is_boundary(prev_ch, ch) {
233 break;
234 }
235 }
236
237 if ch == '\n' {
238 *point.row_mut() += 1;
239 *point.column_mut() = 0;
240 } else {
241 *point.column_mut() += ch.len_utf8() as u32;
242 }
243 prev_ch = Some(ch);
244 }
245 map.clip_point(point, Bias::Right)
246}
247
248pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
249 let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
250 let text = &map.buffer_snapshot;
251 let next_char_kind = text.chars_at(ix).next().map(char_kind);
252 let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind);
253 prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
254}
255
256pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<DisplayPoint> {
257 let position = map
258 .clip_point(position, Bias::Left)
259 .to_offset(map, Bias::Left);
260 let (range, _) = map.buffer_snapshot.surrounding_word(position);
261 let start = range
262 .start
263 .to_point(&map.buffer_snapshot)
264 .to_display_point(map);
265 let end = range
266 .end
267 .to_point(&map.buffer_snapshot)
268 .to_display_point(map);
269 start..end
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer};
276 use language::Point;
277 use settings::Settings;
278
279 #[gpui::test]
280 fn test_previous_word_start(cx: &mut gpui::MutableAppContext) {
281 cx.set_global(Settings::test(cx));
282 fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
283 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
284 assert_eq!(
285 previous_word_start(&snapshot, display_points[1]),
286 display_points[0]
287 );
288 }
289
290 assert("\nˇ ˇlorem", cx);
291 assert("ˇ\nˇ lorem", cx);
292 assert(" ˇloremˇ", cx);
293 assert("ˇ ˇlorem", cx);
294 assert(" ˇlorˇem", cx);
295 assert("\nlorem\nˇ ˇipsum", cx);
296 assert("\n\nˇ\nˇ", cx);
297 assert(" ˇlorem ˇipsum", cx);
298 assert("loremˇ-ˇipsum", cx);
299 assert("loremˇ-#$@ˇipsum", cx);
300 assert("ˇlorem_ˇipsum", cx);
301 assert(" ˇdefγˇ", cx);
302 assert(" ˇbcΔˇ", cx);
303 assert(" abˇ——ˇcd", cx);
304 }
305
306 #[gpui::test]
307 fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) {
308 cx.set_global(Settings::test(cx));
309 fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
310 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
311 assert_eq!(
312 previous_subword_start(&snapshot, display_points[1]),
313 display_points[0]
314 );
315 }
316
317 // Subword boundaries are respected
318 assert("lorem_ˇipˇsum", cx);
319 assert("lorem_ˇipsumˇ", cx);
320 assert("ˇlorem_ˇipsum", cx);
321 assert("lorem_ˇipsum_ˇdolor", cx);
322 assert("loremˇIpˇsum", cx);
323 assert("loremˇIpsumˇ", cx);
324
325 // Word boundaries are still respected
326 assert("\nˇ ˇlorem", cx);
327 assert(" ˇloremˇ", cx);
328 assert(" ˇlorˇem", cx);
329 assert("\nlorem\nˇ ˇipsum", cx);
330 assert("\n\nˇ\nˇ", cx);
331 assert(" ˇlorem ˇipsum", cx);
332 assert("loremˇ-ˇipsum", cx);
333 assert("loremˇ-#$@ˇipsum", cx);
334 assert(" ˇdefγˇ", cx);
335 assert(" bcˇΔˇ", cx);
336 assert(" ˇbcδˇ", cx);
337 assert(" abˇ——ˇcd", cx);
338 }
339
340 #[gpui::test]
341 fn test_find_preceding_boundary(cx: &mut gpui::MutableAppContext) {
342 cx.set_global(Settings::test(cx));
343 fn assert(
344 marked_text: &str,
345 cx: &mut gpui::MutableAppContext,
346 is_boundary: impl FnMut(char, char) -> bool,
347 ) {
348 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
349 assert_eq!(
350 find_preceding_boundary(&snapshot, display_points[1], is_boundary),
351 display_points[0]
352 );
353 }
354
355 assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
356 left == 'c' && right == 'd'
357 });
358 assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
359 left == '\n' && right == 'g'
360 });
361 let mut line_count = 0;
362 assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
363 if left == '\n' {
364 line_count += 1;
365 line_count == 2
366 } else {
367 false
368 }
369 });
370 }
371
372 #[gpui::test]
373 fn test_next_word_end(cx: &mut gpui::MutableAppContext) {
374 cx.set_global(Settings::test(cx));
375 fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
376 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
377 assert_eq!(
378 next_word_end(&snapshot, display_points[0]),
379 display_points[1]
380 );
381 }
382
383 assert("\nˇ loremˇ", cx);
384 assert(" ˇloremˇ", cx);
385 assert(" lorˇemˇ", cx);
386 assert(" loremˇ ˇ\nipsum\n", cx);
387 assert("\nˇ\nˇ\n\n", cx);
388 assert("loremˇ ipsumˇ ", cx);
389 assert("loremˇ-ˇipsum", cx);
390 assert("loremˇ#$@-ˇipsum", cx);
391 assert("loremˇ_ipsumˇ", cx);
392 assert(" ˇbcΔˇ", cx);
393 assert(" abˇ——ˇcd", cx);
394 }
395
396 #[gpui::test]
397 fn test_next_subword_end(cx: &mut gpui::MutableAppContext) {
398 cx.set_global(Settings::test(cx));
399 fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
400 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
401 assert_eq!(
402 next_subword_end(&snapshot, display_points[0]),
403 display_points[1]
404 );
405 }
406
407 // Subword boundaries are respected
408 assert("loˇremˇ_ipsum", cx);
409 assert("ˇloremˇ_ipsum", cx);
410 assert("loremˇ_ipsumˇ", cx);
411 assert("loremˇ_ipsumˇ_dolor", cx);
412 assert("loˇremˇIpsum", cx);
413 assert("loremˇIpsumˇDolor", cx);
414
415 // Word boundaries are still respected
416 assert("\nˇ loremˇ", cx);
417 assert(" ˇloremˇ", cx);
418 assert(" lorˇemˇ", cx);
419 assert(" loremˇ ˇ\nipsum\n", cx);
420 assert("\nˇ\nˇ\n\n", cx);
421 assert("loremˇ ipsumˇ ", cx);
422 assert("loremˇ-ˇipsum", cx);
423 assert("loremˇ#$@-ˇipsum", cx);
424 assert("loremˇ_ipsumˇ", cx);
425 assert(" ˇbcˇΔ", cx);
426 assert(" abˇ——ˇcd", cx);
427 }
428
429 #[gpui::test]
430 fn test_find_boundary(cx: &mut gpui::MutableAppContext) {
431 cx.set_global(Settings::test(cx));
432 fn assert(
433 marked_text: &str,
434 cx: &mut gpui::MutableAppContext,
435 is_boundary: impl FnMut(char, char) -> bool,
436 ) {
437 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
438 assert_eq!(
439 find_boundary(&snapshot, display_points[0], is_boundary),
440 display_points[1]
441 );
442 }
443
444 assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
445 left == 'j' && right == 'k'
446 });
447 assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
448 left == '\n' && right == 'i'
449 });
450 let mut line_count = 0;
451 assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
452 if left == '\n' {
453 line_count += 1;
454 line_count == 2
455 } else {
456 false
457 }
458 });
459 }
460
461 #[gpui::test]
462 fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
463 cx.set_global(Settings::test(cx));
464 fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
465 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
466 assert_eq!(
467 surrounding_word(&snapshot, display_points[1]),
468 display_points[0]..display_points[2]
469 );
470 }
471
472 assert("ˇˇloremˇ ipsum", cx);
473 assert("ˇloˇremˇ ipsum", cx);
474 assert("ˇloremˇˇ ipsum", cx);
475 assert("loremˇ ˇ ˇipsum", cx);
476 assert("lorem\nˇˇˇ\nipsum", cx);
477 assert("lorem\nˇˇipsumˇ", cx);
478 assert("lorem,ˇˇ ˇipsum", cx);
479 assert("ˇloremˇˇ, ipsum", cx);
480 }
481
482 #[gpui::test]
483 fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
484 cx.set_global(Settings::test(cx));
485 let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
486 let font_id = cx
487 .font_cache()
488 .select_font(family_id, &Default::default())
489 .unwrap();
490
491 let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx));
492 let multibuffer = cx.add_model(|cx| {
493 let mut multibuffer = MultiBuffer::new(0);
494 multibuffer.push_excerpts(
495 buffer.clone(),
496 [
497 ExcerptRange {
498 context: Point::new(0, 0)..Point::new(1, 4),
499 primary: None,
500 },
501 ExcerptRange {
502 context: Point::new(2, 0)..Point::new(3, 2),
503 primary: None,
504 },
505 ],
506 cx,
507 );
508 multibuffer
509 });
510 let display_map =
511 cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
512 let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
513
514 assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
515
516 // Can't move up into the first excerpt's header
517 assert_eq!(
518 up(
519 &snapshot,
520 DisplayPoint::new(2, 2),
521 SelectionGoal::Column(2),
522 false
523 ),
524 (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
525 );
526 assert_eq!(
527 up(
528 &snapshot,
529 DisplayPoint::new(2, 0),
530 SelectionGoal::None,
531 false
532 ),
533 (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
534 );
535
536 // Move up and down within first excerpt
537 assert_eq!(
538 up(
539 &snapshot,
540 DisplayPoint::new(3, 4),
541 SelectionGoal::Column(4),
542 false
543 ),
544 (DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
545 );
546 assert_eq!(
547 down(
548 &snapshot,
549 DisplayPoint::new(2, 3),
550 SelectionGoal::Column(4),
551 false
552 ),
553 (DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
554 );
555
556 // Move up and down across second excerpt's header
557 assert_eq!(
558 up(
559 &snapshot,
560 DisplayPoint::new(6, 5),
561 SelectionGoal::Column(5),
562 false
563 ),
564 (DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
565 );
566 assert_eq!(
567 down(
568 &snapshot,
569 DisplayPoint::new(3, 4),
570 SelectionGoal::Column(5),
571 false
572 ),
573 (DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
574 );
575
576 // Can't move down off the end
577 assert_eq!(
578 down(
579 &snapshot,
580 DisplayPoint::new(7, 0),
581 SelectionGoal::Column(0),
582 false
583 ),
584 (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
585 );
586 assert_eq!(
587 down(
588 &snapshot,
589 DisplayPoint::new(7, 2),
590 SelectionGoal::Column(2),
591 false
592 ),
593 (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
594 );
595 }
596}