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