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 saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
17 if point.column() > 0 {
18 *point.column_mut() -= 1;
19 }
20 map.clip_point(point, Bias::Left)
21}
22
23pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
24 let max_column = map.line_len(point.row());
25 if point.column() < max_column {
26 *point.column_mut() += 1;
27 } else if point.row() < map.max_point().row() {
28 *point.row_mut() += 1;
29 *point.column_mut() = 0;
30 }
31 map.clip_point(point, Bias::Right)
32}
33
34pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
35 *point.column_mut() += 1;
36 map.clip_point(point, Bias::Right)
37}
38
39pub fn up(
40 map: &DisplaySnapshot,
41 start: DisplayPoint,
42 goal: SelectionGoal,
43 preserve_column_at_start: bool,
44) -> (DisplayPoint, SelectionGoal) {
45 up_by_rows(map, start, 1, goal, preserve_column_at_start)
46}
47
48pub fn down(
49 map: &DisplaySnapshot,
50 start: DisplayPoint,
51 goal: SelectionGoal,
52 preserve_column_at_end: bool,
53) -> (DisplayPoint, SelectionGoal) {
54 down_by_rows(map, start, 1, goal, preserve_column_at_end)
55}
56
57pub fn up_by_rows(
58 map: &DisplaySnapshot,
59 start: DisplayPoint,
60 row_count: u32,
61 goal: SelectionGoal,
62 preserve_column_at_start: bool,
63) -> (DisplayPoint, SelectionGoal) {
64 let mut goal_column = if let SelectionGoal::Column(column) = goal {
65 column
66 } else {
67 map.column_to_chars(start.row(), start.column())
68 };
69
70 let prev_row = start.row().saturating_sub(row_count);
71 let mut point = map.clip_point(
72 DisplayPoint::new(prev_row, map.line_len(prev_row)),
73 Bias::Left,
74 );
75 if point.row() < start.row() {
76 *point.column_mut() = map.column_from_chars(point.row(), goal_column);
77 } else if preserve_column_at_start {
78 return (start, goal);
79 } else {
80 point = DisplayPoint::new(0, 0);
81 goal_column = 0;
82 }
83
84 let mut clipped_point = map.clip_point(point, Bias::Left);
85 if clipped_point.row() < point.row() {
86 clipped_point = map.clip_point(point, Bias::Right);
87 }
88 (clipped_point, SelectionGoal::Column(goal_column))
89}
90
91pub fn down_by_rows(
92 map: &DisplaySnapshot,
93 start: DisplayPoint,
94 row_count: u32,
95 goal: SelectionGoal,
96 preserve_column_at_end: bool,
97) -> (DisplayPoint, SelectionGoal) {
98 let mut goal_column = if let SelectionGoal::Column(column) = goal {
99 column
100 } else {
101 map.column_to_chars(start.row(), start.column())
102 };
103
104 let new_row = start.row() + row_count;
105 let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
106 if point.row() > start.row() {
107 *point.column_mut() = map.column_from_chars(point.row(), goal_column);
108 } else if preserve_column_at_end {
109 return (start, goal);
110 } else {
111 point = map.max_point();
112 goal_column = map.column_to_chars(point.row(), point.column())
113 }
114
115 let mut clipped_point = map.clip_point(point, Bias::Right);
116 if clipped_point.row() > point.row() {
117 clipped_point = map.clip_point(point, Bias::Left);
118 }
119 (clipped_point, SelectionGoal::Column(goal_column))
120}
121
122pub fn line_beginning(
123 map: &DisplaySnapshot,
124 display_point: DisplayPoint,
125 stop_at_soft_boundaries: bool,
126) -> DisplayPoint {
127 let point = display_point.to_point(map);
128 let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
129 let line_start = map.prev_line_boundary(point).1;
130
131 if stop_at_soft_boundaries && display_point != soft_line_start {
132 soft_line_start
133 } else {
134 line_start
135 }
136}
137
138pub fn indented_line_beginning(
139 map: &DisplaySnapshot,
140 display_point: DisplayPoint,
141 stop_at_soft_boundaries: bool,
142) -> DisplayPoint {
143 let point = display_point.to_point(map);
144 let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
145 let indent_start = Point::new(
146 point.row,
147 map.buffer_snapshot.indent_size_for_line(point.row).len,
148 )
149 .to_display_point(map);
150 let line_start = map.prev_line_boundary(point).1;
151
152 if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start
153 {
154 soft_line_start
155 } else if stop_at_soft_boundaries && display_point != indent_start {
156 indent_start
157 } else {
158 line_start
159 }
160}
161
162pub fn line_end(
163 map: &DisplaySnapshot,
164 display_point: DisplayPoint,
165 stop_at_soft_boundaries: bool,
166) -> DisplayPoint {
167 let soft_line_end = map.clip_point(
168 DisplayPoint::new(display_point.row(), map.line_len(display_point.row())),
169 Bias::Left,
170 );
171 if stop_at_soft_boundaries && display_point != soft_line_end {
172 soft_line_end
173 } else {
174 map.next_line_boundary(display_point.to_point(map)).1
175 }
176}
177
178pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
179 find_preceding_boundary(map, point, |left, right| {
180 (char_kind(left) != char_kind(right) && !right.is_whitespace()) || left == '\n'
181 })
182}
183
184pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
185 find_preceding_boundary(map, point, |left, right| {
186 let is_word_start = char_kind(left) != char_kind(right) && !right.is_whitespace();
187 let is_subword_start =
188 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
189 is_word_start || is_subword_start || left == '\n'
190 })
191}
192
193pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
194 find_boundary(map, point, |left, right| {
195 (char_kind(left) != char_kind(right) && !left.is_whitespace()) || right == '\n'
196 })
197}
198
199pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
200 find_boundary(map, point, |left, right| {
201 let is_word_end = (char_kind(left) != char_kind(right)) && !left.is_whitespace();
202 let is_subword_end =
203 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
204 is_word_end || is_subword_end || right == '\n'
205 })
206}
207
208pub fn start_of_paragraph(
209 map: &DisplaySnapshot,
210 display_point: DisplayPoint,
211 mut count: usize,
212) -> DisplayPoint {
213 let point = display_point.to_point(map);
214 if point.row == 0 {
215 return map.max_point();
216 }
217
218 let mut found_non_blank_line = false;
219 for row in (0..point.row + 1).rev() {
220 let blank = map.buffer_snapshot.is_line_blank(row);
221 if found_non_blank_line && blank {
222 if count <= 1 {
223 return Point::new(row, 0).to_display_point(map);
224 }
225 count -= 1;
226 found_non_blank_line = false;
227 }
228
229 found_non_blank_line |= !blank;
230 }
231
232 DisplayPoint::zero()
233}
234
235pub fn end_of_paragraph(
236 map: &DisplaySnapshot,
237 display_point: DisplayPoint,
238 mut count: usize,
239) -> DisplayPoint {
240 let point = display_point.to_point(map);
241 if point.row == map.max_buffer_row() {
242 return DisplayPoint::zero();
243 }
244
245 let mut found_non_blank_line = false;
246 for row in point.row..map.max_buffer_row() + 1 {
247 let blank = map.buffer_snapshot.is_line_blank(row);
248 if found_non_blank_line && blank {
249 if count <= 1 {
250 return Point::new(row, 0).to_display_point(map);
251 }
252 count -= 1;
253 found_non_blank_line = false;
254 }
255
256 found_non_blank_line |= !blank;
257 }
258
259 map.max_point()
260}
261
262/// Scans for a boundary preceding the given start point `from` 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_preceding_boundary(
267 map: &DisplaySnapshot,
268 from: DisplayPoint,
269 mut is_boundary: impl FnMut(char, char) -> bool,
270) -> DisplayPoint {
271 let mut start_column = 0;
272 let mut soft_wrap_row = from.row() + 1;
273
274 let mut prev = None;
275 for (ch, point) in map.reverse_chars_at(from) {
276 // Recompute soft_wrap_indent if the row has changed
277 if point.row() != soft_wrap_row {
278 soft_wrap_row = point.row();
279
280 if point.row() == 0 {
281 start_column = 0;
282 } else if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
283 start_column = indent;
284 }
285 }
286
287 // If the current point is in the soft_wrap, skip comparing it
288 if point.column() < start_column {
289 continue;
290 }
291
292 if let Some((prev_ch, prev_point)) = prev {
293 if is_boundary(ch, prev_ch) {
294 return map.clip_point(prev_point, Bias::Left);
295 }
296 }
297
298 prev = Some((ch, point));
299 }
300 map.clip_point(DisplayPoint::zero(), Bias::Left)
301}
302
303/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
304/// given predicate returning true. The predicate is called with the character to the left and right
305/// of the candidate boundary location, and will be called with `\n` characters indicating the start
306/// or end of a line. If no boundary is found, the start of the line is returned.
307pub fn find_preceding_boundary_in_line(
308 map: &DisplaySnapshot,
309 from: DisplayPoint,
310 mut is_boundary: impl FnMut(char, char) -> bool,
311) -> DisplayPoint {
312 let mut start_column = 0;
313 if from.row() > 0 {
314 if let Some(indent) = map.soft_wrap_indent(from.row() - 1) {
315 start_column = indent;
316 }
317 }
318
319 let mut prev = None;
320 for (ch, point) in map.reverse_chars_at(from) {
321 if let Some((prev_ch, prev_point)) = prev {
322 if is_boundary(ch, prev_ch) {
323 return map.clip_point(prev_point, Bias::Left);
324 }
325 }
326
327 if ch == '\n' || point.column() < start_column {
328 break;
329 }
330
331 prev = Some((ch, point));
332 }
333
334 map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Left)
335}
336
337/// Scans for a boundary following the given start point until a boundary is found, indicated by the
338/// given predicate returning true. The predicate is called with the character to the left and right
339/// of the candidate boundary location, and will be called with `\n` characters indicating the start
340/// or end of a line.
341pub fn find_boundary(
342 map: &DisplaySnapshot,
343 from: DisplayPoint,
344 mut is_boundary: impl FnMut(char, char) -> bool,
345) -> DisplayPoint {
346 let mut prev_ch = None;
347 for (ch, point) in map.chars_at(from) {
348 if let Some(prev_ch) = prev_ch {
349 if is_boundary(prev_ch, ch) {
350 return map.clip_point(point, Bias::Right);
351 }
352 }
353
354 prev_ch = Some(ch);
355 }
356 map.clip_point(map.max_point(), Bias::Right)
357}
358
359/// Scans for a boundary following the given start point until a boundary is found, indicated by the
360/// given predicate returning true. The predicate is called with the character to the left and right
361/// of the candidate boundary location, and will be called with `\n` characters indicating the start
362/// or end of a line. If no boundary is found, the end of the line is returned
363pub fn find_boundary_in_line(
364 map: &DisplaySnapshot,
365 from: DisplayPoint,
366 mut is_boundary: impl FnMut(char, char) -> bool,
367) -> DisplayPoint {
368 let mut prev = None;
369 for (ch, point) in map.chars_at(from) {
370 if let Some((prev_ch, _)) = prev {
371 if is_boundary(prev_ch, ch) {
372 return map.clip_point(point, Bias::Right);
373 }
374 }
375
376 prev = Some((ch, point));
377
378 if ch == '\n' {
379 break;
380 }
381 }
382
383 // Return the last position checked so that we give a point right before the newline or eof.
384 map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Right)
385}
386
387pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
388 let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
389 let text = &map.buffer_snapshot;
390 let next_char_kind = text.chars_at(ix).next().map(char_kind);
391 let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind);
392 prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
393}
394
395pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<DisplayPoint> {
396 let position = map
397 .clip_point(position, Bias::Left)
398 .to_offset(map, Bias::Left);
399 let (range, _) = map.buffer_snapshot.surrounding_word(position);
400 let start = range
401 .start
402 .to_point(&map.buffer_snapshot)
403 .to_display_point(map);
404 let end = range
405 .end
406 .to_point(&map.buffer_snapshot)
407 .to_display_point(map);
408 start..end
409}
410
411pub fn split_display_range_by_lines(
412 map: &DisplaySnapshot,
413 range: Range<DisplayPoint>,
414) -> Vec<Range<DisplayPoint>> {
415 let mut result = Vec::new();
416
417 let mut start = range.start;
418 // Loop over all the covered rows until the one containing the range end
419 for row in range.start.row()..range.end.row() {
420 let row_end_column = map.line_len(row);
421 let end = map.clip_point(DisplayPoint::new(row, row_end_column), Bias::Left);
422 if start != end {
423 result.push(start..end);
424 }
425 start = map.clip_point(DisplayPoint::new(row + 1, 0), Bias::Left);
426 }
427
428 // Add the final range from the start of the last end to the original range end.
429 result.push(start..range.end);
430
431 result
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use crate::{
438 display_map::Inlay, test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange,
439 InlayId, MultiBuffer,
440 };
441 use settings::SettingsStore;
442 use util::post_inc;
443
444 #[gpui::test]
445 fn test_previous_word_start(cx: &mut gpui::AppContext) {
446 init_test(cx);
447
448 fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
449 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
450 assert_eq!(
451 previous_word_start(&snapshot, display_points[1]),
452 display_points[0]
453 );
454 }
455
456 assert("\nˇ ˇlorem", cx);
457 assert("ˇ\nˇ lorem", cx);
458 assert(" ˇloremˇ", cx);
459 assert("ˇ ˇlorem", cx);
460 assert(" ˇlorˇem", cx);
461 assert("\nlorem\nˇ ˇipsum", cx);
462 assert("\n\nˇ\nˇ", cx);
463 assert(" ˇlorem ˇipsum", cx);
464 assert("loremˇ-ˇipsum", cx);
465 assert("loremˇ-#$@ˇipsum", cx);
466 assert("ˇlorem_ˇipsum", cx);
467 assert(" ˇdefγˇ", cx);
468 assert(" ˇbcΔˇ", cx);
469 assert(" abˇ——ˇcd", cx);
470 }
471
472 #[gpui::test]
473 fn test_previous_subword_start(cx: &mut gpui::AppContext) {
474 init_test(cx);
475
476 fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
477 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
478 assert_eq!(
479 previous_subword_start(&snapshot, display_points[1]),
480 display_points[0]
481 );
482 }
483
484 // Subword boundaries are respected
485 assert("lorem_ˇipˇsum", cx);
486 assert("lorem_ˇipsumˇ", cx);
487 assert("ˇlorem_ˇipsum", cx);
488 assert("lorem_ˇipsum_ˇdolor", cx);
489 assert("loremˇIpˇsum", cx);
490 assert("loremˇIpsumˇ", cx);
491
492 // Word boundaries are still respected
493 assert("\nˇ ˇlorem", cx);
494 assert(" ˇloremˇ", cx);
495 assert(" ˇlorˇem", cx);
496 assert("\nlorem\nˇ ˇipsum", cx);
497 assert("\n\nˇ\nˇ", cx);
498 assert(" ˇlorem ˇipsum", cx);
499 assert("loremˇ-ˇipsum", cx);
500 assert("loremˇ-#$@ˇipsum", cx);
501 assert(" ˇdefγˇ", cx);
502 assert(" bcˇΔˇ", cx);
503 assert(" ˇbcδˇ", cx);
504 assert(" abˇ——ˇcd", cx);
505 }
506
507 #[gpui::test]
508 fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
509 init_test(cx);
510
511 fn assert(
512 marked_text: &str,
513 cx: &mut gpui::AppContext,
514 is_boundary: impl FnMut(char, char) -> bool,
515 ) {
516 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
517 assert_eq!(
518 find_preceding_boundary(&snapshot, display_points[1], is_boundary),
519 display_points[0]
520 );
521 }
522
523 assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
524 left == 'c' && right == 'd'
525 });
526 assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
527 left == '\n' && right == 'g'
528 });
529 let mut line_count = 0;
530 assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
531 if left == '\n' {
532 line_count += 1;
533 line_count == 2
534 } else {
535 false
536 }
537 });
538 }
539
540 #[gpui::test]
541 fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
542 init_test(cx);
543
544 let input_text = "abcdefghijklmnopqrstuvwxys";
545 let family_id = cx
546 .font_cache()
547 .load_family(&["Helvetica"], &Default::default())
548 .unwrap();
549 let font_id = cx
550 .font_cache()
551 .select_font(family_id, &Default::default())
552 .unwrap();
553 let font_size = 14.0;
554 let buffer = MultiBuffer::build_simple(input_text, cx);
555 let buffer_snapshot = buffer.read(cx).snapshot(cx);
556 let display_map =
557 cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
558
559 // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
560 let mut id = 0;
561 let inlays = (0..buffer_snapshot.len())
562 .map(|offset| {
563 [
564 Inlay {
565 id: InlayId::Suggestion(post_inc(&mut id)),
566 position: buffer_snapshot.anchor_at(offset, Bias::Left),
567 text: format!("test").into(),
568 },
569 Inlay {
570 id: InlayId::Suggestion(post_inc(&mut id)),
571 position: buffer_snapshot.anchor_at(offset, Bias::Right),
572 text: format!("test").into(),
573 },
574 Inlay {
575 id: InlayId::Hint(post_inc(&mut id)),
576 position: buffer_snapshot.anchor_at(offset, Bias::Left),
577 text: format!("test").into(),
578 },
579 Inlay {
580 id: InlayId::Hint(post_inc(&mut id)),
581 position: buffer_snapshot.anchor_at(offset, Bias::Right),
582 text: format!("test").into(),
583 },
584 ]
585 })
586 .flatten()
587 .collect();
588 let snapshot = display_map.update(cx, |map, cx| {
589 map.splice_inlays(Vec::new(), inlays, cx);
590 map.snapshot(cx)
591 });
592
593 assert_eq!(
594 find_preceding_boundary(
595 &snapshot,
596 buffer_snapshot.len().to_display_point(&snapshot),
597 |left, _| left == 'a',
598 ),
599 0.to_display_point(&snapshot),
600 "Should not stop at inlays when looking for boundaries"
601 );
602
603 assert_eq!(
604 find_preceding_boundary_in_line(
605 &snapshot,
606 buffer_snapshot.len().to_display_point(&snapshot),
607 |left, _| left == 'a',
608 ),
609 0.to_display_point(&snapshot),
610 "Should not stop at inlays when looking for boundaries in line"
611 );
612 }
613
614 #[gpui::test]
615 fn test_next_word_end(cx: &mut gpui::AppContext) {
616 init_test(cx);
617
618 fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
619 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
620 assert_eq!(
621 next_word_end(&snapshot, display_points[0]),
622 display_points[1]
623 );
624 }
625
626 assert("\nˇ loremˇ", cx);
627 assert(" ˇloremˇ", cx);
628 assert(" lorˇemˇ", cx);
629 assert(" loremˇ ˇ\nipsum\n", cx);
630 assert("\nˇ\nˇ\n\n", cx);
631 assert("loremˇ ipsumˇ ", cx);
632 assert("loremˇ-ˇipsum", cx);
633 assert("loremˇ#$@-ˇipsum", cx);
634 assert("loremˇ_ipsumˇ", cx);
635 assert(" ˇbcΔˇ", cx);
636 assert(" abˇ——ˇcd", cx);
637 }
638
639 #[gpui::test]
640 fn test_next_subword_end(cx: &mut gpui::AppContext) {
641 init_test(cx);
642
643 fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
644 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
645 assert_eq!(
646 next_subword_end(&snapshot, display_points[0]),
647 display_points[1]
648 );
649 }
650
651 // Subword boundaries are respected
652 assert("loˇremˇ_ipsum", cx);
653 assert("ˇloremˇ_ipsum", cx);
654 assert("loremˇ_ipsumˇ", cx);
655 assert("loremˇ_ipsumˇ_dolor", cx);
656 assert("loˇremˇIpsum", cx);
657 assert("loremˇIpsumˇDolor", cx);
658
659 // Word boundaries are still respected
660 assert("\nˇ loremˇ", cx);
661 assert(" ˇloremˇ", cx);
662 assert(" lorˇemˇ", cx);
663 assert(" loremˇ ˇ\nipsum\n", cx);
664 assert("\nˇ\nˇ\n\n", cx);
665 assert("loremˇ ipsumˇ ", cx);
666 assert("loremˇ-ˇipsum", cx);
667 assert("loremˇ#$@-ˇipsum", cx);
668 assert("loremˇ_ipsumˇ", cx);
669 assert(" ˇbcˇΔ", cx);
670 assert(" abˇ——ˇcd", cx);
671 }
672
673 #[gpui::test]
674 fn test_find_boundary(cx: &mut gpui::AppContext) {
675 init_test(cx);
676
677 fn assert(
678 marked_text: &str,
679 cx: &mut gpui::AppContext,
680 is_boundary: impl FnMut(char, char) -> bool,
681 ) {
682 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
683 assert_eq!(
684 find_boundary(&snapshot, display_points[0], is_boundary),
685 display_points[1]
686 );
687 }
688
689 assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
690 left == 'j' && right == 'k'
691 });
692 assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
693 left == '\n' && right == 'i'
694 });
695 let mut line_count = 0;
696 assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
697 if left == '\n' {
698 line_count += 1;
699 line_count == 2
700 } else {
701 false
702 }
703 });
704 }
705
706 #[gpui::test]
707 fn test_surrounding_word(cx: &mut gpui::AppContext) {
708 init_test(cx);
709
710 fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
711 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
712 assert_eq!(
713 surrounding_word(&snapshot, display_points[1]),
714 display_points[0]..display_points[2]
715 );
716 }
717
718 assert("ˇˇloremˇ ipsum", cx);
719 assert("ˇloˇremˇ ipsum", cx);
720 assert("ˇloremˇˇ ipsum", cx);
721 assert("loremˇ ˇ ˇipsum", cx);
722 assert("lorem\nˇˇˇ\nipsum", cx);
723 assert("lorem\nˇˇipsumˇ", cx);
724 assert("lorem,ˇˇ ˇipsum", cx);
725 assert("ˇloremˇˇ, ipsum", cx);
726 }
727
728 #[gpui::test]
729 fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) {
730 init_test(cx);
731
732 let family_id = cx
733 .font_cache()
734 .load_family(&["Helvetica"], &Default::default())
735 .unwrap();
736 let font_id = cx
737 .font_cache()
738 .select_font(family_id, &Default::default())
739 .unwrap();
740
741 let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx));
742 let multibuffer = cx.add_model(|cx| {
743 let mut multibuffer = MultiBuffer::new(0);
744 multibuffer.push_excerpts(
745 buffer.clone(),
746 [
747 ExcerptRange {
748 context: Point::new(0, 0)..Point::new(1, 4),
749 primary: None,
750 },
751 ExcerptRange {
752 context: Point::new(2, 0)..Point::new(3, 2),
753 primary: None,
754 },
755 ],
756 cx,
757 );
758 multibuffer
759 });
760 let display_map =
761 cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
762 let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
763
764 assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
765
766 // Can't move up into the first excerpt's header
767 assert_eq!(
768 up(
769 &snapshot,
770 DisplayPoint::new(2, 2),
771 SelectionGoal::Column(2),
772 false
773 ),
774 (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
775 );
776 assert_eq!(
777 up(
778 &snapshot,
779 DisplayPoint::new(2, 0),
780 SelectionGoal::None,
781 false
782 ),
783 (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
784 );
785
786 // Move up and down within first excerpt
787 assert_eq!(
788 up(
789 &snapshot,
790 DisplayPoint::new(3, 4),
791 SelectionGoal::Column(4),
792 false
793 ),
794 (DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
795 );
796 assert_eq!(
797 down(
798 &snapshot,
799 DisplayPoint::new(2, 3),
800 SelectionGoal::Column(4),
801 false
802 ),
803 (DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
804 );
805
806 // Move up and down across second excerpt's header
807 assert_eq!(
808 up(
809 &snapshot,
810 DisplayPoint::new(6, 5),
811 SelectionGoal::Column(5),
812 false
813 ),
814 (DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
815 );
816 assert_eq!(
817 down(
818 &snapshot,
819 DisplayPoint::new(3, 4),
820 SelectionGoal::Column(5),
821 false
822 ),
823 (DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
824 );
825
826 // Can't move down off the end
827 assert_eq!(
828 down(
829 &snapshot,
830 DisplayPoint::new(7, 0),
831 SelectionGoal::Column(0),
832 false
833 ),
834 (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
835 );
836 assert_eq!(
837 down(
838 &snapshot,
839 DisplayPoint::new(7, 2),
840 SelectionGoal::Column(2),
841 false
842 ),
843 (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
844 );
845 }
846
847 fn init_test(cx: &mut gpui::AppContext) {
848 cx.set_global(SettingsStore::test(cx));
849 theme::init((), cx);
850 language::init(cx);
851 crate::init(cx);
852 }
853}