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