1//! Movement module contains helper functions for calculating intended position
2//! in editor given a given motion (e.g. it handles converting a "move left" command into coordinates in editor). It is exposed mostly for use by vim crate.
3
4use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
5use crate::{scroll::ScrollAnchor, CharKind, DisplayRow, EditorStyle, RowExt, ToOffset, ToPoint};
6use gpui::{Pixels, WindowTextSystem};
7use language::Point;
8use multi_buffer::{MultiBufferRow, MultiBufferSnapshot};
9use serde::Deserialize;
10
11use std::{ops::Range, sync::Arc};
12
13/// Defines search strategy for items in `movement` module.
14/// `FindRange::SingeLine` only looks for a match on a single line at a time, whereas
15/// `FindRange::MultiLine` keeps going until the end of a string.
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
17pub enum FindRange {
18 SingleLine,
19 MultiLine,
20}
21
22/// TextLayoutDetails encompasses everything we need to move vertically
23/// taking into account variable width characters.
24pub struct TextLayoutDetails {
25 pub(crate) text_system: Arc<WindowTextSystem>,
26 pub(crate) editor_style: EditorStyle,
27 pub(crate) rem_size: Pixels,
28 pub scroll_anchor: ScrollAnchor,
29 pub visible_rows: Option<f32>,
30 pub vertical_scroll_margin: f32,
31}
32
33/// Returns a column to the left of the current point, wrapping
34/// to the previous line if that point is at the start of line.
35pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
36 if point.column() > 0 {
37 *point.column_mut() -= 1;
38 } else if point.row().0 > 0 {
39 *point.row_mut() -= 1;
40 *point.column_mut() = map.line_len(point.row());
41 }
42 map.clip_point(point, Bias::Left)
43}
44
45/// Returns a column to the left of the current point, doing nothing if
46/// that point is already at the start of line.
47pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
48 if point.column() > 0 {
49 *point.column_mut() -= 1;
50 } else if point.column() == 0 {
51 // If the current sofr_wrap mode is used, the column corresponding to the display is 0,
52 // which does not necessarily mean that the actual beginning of a paragraph
53 if map.display_point_to_fold_point(point, Bias::Left).column() > 0 {
54 return left(map, point);
55 }
56 }
57 map.clip_point(point, Bias::Left)
58}
59
60/// Returns a column to the right of the current point, doing nothing
61// if that point is at the end of the line.
62pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
63 if point.column() < map.line_len(point.row()) {
64 *point.column_mut() += 1;
65 } else if point.row() < map.max_point().row() {
66 *point.row_mut() += 1;
67 *point.column_mut() = 0;
68 }
69 map.clip_point(point, Bias::Right)
70}
71
72/// Returns a column to the right of the current point, not performing any wrapping
73/// if that point is already at the end of line.
74pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
75 *point.column_mut() += 1;
76 map.clip_point(point, Bias::Right)
77}
78
79/// Returns a display point for the preceding displayed line (which might be a soft-wrapped line).
80pub fn up(
81 map: &DisplaySnapshot,
82 start: DisplayPoint,
83 goal: SelectionGoal,
84 preserve_column_at_start: bool,
85 text_layout_details: &TextLayoutDetails,
86) -> (DisplayPoint, SelectionGoal) {
87 up_by_rows(
88 map,
89 start,
90 1,
91 goal,
92 preserve_column_at_start,
93 text_layout_details,
94 )
95}
96
97/// Returns a display point for the next displayed line (which might be a soft-wrapped line).
98pub fn down(
99 map: &DisplaySnapshot,
100 start: DisplayPoint,
101 goal: SelectionGoal,
102 preserve_column_at_end: bool,
103 text_layout_details: &TextLayoutDetails,
104) -> (DisplayPoint, SelectionGoal) {
105 down_by_rows(
106 map,
107 start,
108 1,
109 goal,
110 preserve_column_at_end,
111 text_layout_details,
112 )
113}
114
115pub(crate) fn up_by_rows(
116 map: &DisplaySnapshot,
117 start: DisplayPoint,
118 row_count: u32,
119 goal: SelectionGoal,
120 preserve_column_at_start: bool,
121 text_layout_details: &TextLayoutDetails,
122) -> (DisplayPoint, SelectionGoal) {
123 let goal_x = match goal {
124 SelectionGoal::HorizontalPosition(x) => x.into(),
125 SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
126 SelectionGoal::HorizontalRange { end, .. } => end.into(),
127 _ => map.x_for_display_point(start, text_layout_details),
128 };
129
130 let prev_row = DisplayRow(start.row().0.saturating_sub(row_count));
131 let mut point = map.clip_point(
132 DisplayPoint::new(prev_row, map.line_len(prev_row)),
133 Bias::Left,
134 );
135 if point.row() < start.row() {
136 *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
137 } else if preserve_column_at_start {
138 return (start, goal);
139 } else {
140 point = DisplayPoint::new(DisplayRow(0), 0);
141 }
142
143 let mut clipped_point = map.clip_point(point, Bias::Left);
144 if clipped_point.row() < point.row() {
145 clipped_point = map.clip_point(point, Bias::Right);
146 }
147 (
148 clipped_point,
149 SelectionGoal::HorizontalPosition(goal_x.into()),
150 )
151}
152
153pub(crate) fn down_by_rows(
154 map: &DisplaySnapshot,
155 start: DisplayPoint,
156 row_count: u32,
157 goal: SelectionGoal,
158 preserve_column_at_end: bool,
159 text_layout_details: &TextLayoutDetails,
160) -> (DisplayPoint, SelectionGoal) {
161 let goal_x = match goal {
162 SelectionGoal::HorizontalPosition(x) => x.into(),
163 SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
164 SelectionGoal::HorizontalRange { end, .. } => end.into(),
165 _ => map.x_for_display_point(start, text_layout_details),
166 };
167
168 let new_row = DisplayRow(start.row().0 + row_count);
169 let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
170 if point.row() > start.row() {
171 *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
172 } else if preserve_column_at_end {
173 return (start, goal);
174 } else {
175 point = map.max_point();
176 }
177
178 let mut clipped_point = map.clip_point(point, Bias::Right);
179 if clipped_point.row() > point.row() {
180 clipped_point = map.clip_point(point, Bias::Left);
181 }
182 (
183 clipped_point,
184 SelectionGoal::HorizontalPosition(goal_x.into()),
185 )
186}
187
188/// Returns a position of the start of line.
189/// If `stop_at_soft_boundaries` is true, the returned position is that of the
190/// displayed line (e.g. it could actually be in the middle of a text line if that line is soft-wrapped).
191/// Otherwise it's always going to be the start of a logical line.
192pub fn line_beginning(
193 map: &DisplaySnapshot,
194 display_point: DisplayPoint,
195 stop_at_soft_boundaries: bool,
196) -> DisplayPoint {
197 let point = display_point.to_point(map);
198 let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
199 let line_start = map.prev_line_boundary(point).1;
200
201 if stop_at_soft_boundaries && display_point != soft_line_start {
202 soft_line_start
203 } else {
204 line_start
205 }
206}
207
208/// Returns the last indented position on a given line.
209/// If `stop_at_soft_boundaries` is true, the returned [`DisplayPoint`] is that of a
210/// displayed line (e.g. if there's soft wrap it's gonna be returned),
211/// otherwise it's always going to be a start of a logical line.
212pub fn indented_line_beginning(
213 map: &DisplaySnapshot,
214 display_point: DisplayPoint,
215 stop_at_soft_boundaries: bool,
216) -> DisplayPoint {
217 let point = display_point.to_point(map);
218 let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
219 let indent_start = Point::new(
220 point.row,
221 map.buffer_snapshot
222 .indent_size_for_line(MultiBufferRow(point.row))
223 .len,
224 )
225 .to_display_point(map);
226 let line_start = map.prev_line_boundary(point).1;
227
228 if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start
229 {
230 soft_line_start
231 } else if stop_at_soft_boundaries && display_point != indent_start {
232 indent_start
233 } else {
234 line_start
235 }
236}
237
238/// Returns a position of the end of line.
239
240/// If `stop_at_soft_boundaries` is true, the returned position is that of the
241/// displayed line (e.g. it could actually be in the middle of a text line if that line is soft-wrapped).
242/// Otherwise it's always going to be the end of a logical line.
243pub fn line_end(
244 map: &DisplaySnapshot,
245 display_point: DisplayPoint,
246 stop_at_soft_boundaries: bool,
247) -> DisplayPoint {
248 let soft_line_end = map.clip_point(
249 DisplayPoint::new(display_point.row(), map.line_len(display_point.row())),
250 Bias::Left,
251 );
252 if stop_at_soft_boundaries && display_point != soft_line_end {
253 soft_line_end
254 } else {
255 map.next_line_boundary(display_point.to_point(map)).1
256 }
257}
258
259/// Returns a position of the previous word boundary, where a word character is defined as either
260/// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS).
261pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
262 let raw_point = point.to_point(map);
263 let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
264
265 find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
266 (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(right))
267 || left == '\n'
268 })
269}
270
271/// Returns a position of the previous word boundary, where a word character is defined as either
272/// uppercase letter, lowercase letter, '_' character, language-specific word character (like '-' in CSS) or newline.
273pub fn previous_word_start_or_newline(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
274 let raw_point = point.to_point(map);
275 let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
276
277 find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
278 (classifier.kind(left) != classifier.kind(right) && !right.is_whitespace())
279 || left == '\n'
280 || right == '\n'
281 })
282}
283
284/// Returns a position of the previous subword boundary, where a subword is defined as a run of
285/// word characters of the same "subkind" - where subcharacter kinds are '_' character,
286/// lowerspace characters and uppercase characters.
287pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
288 let raw_point = point.to_point(map);
289 let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
290
291 find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
292 let is_word_start =
293 classifier.kind(left) != classifier.kind(right) && !right.is_whitespace();
294 let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
295 || left == '_' && right != '_'
296 || left.is_lowercase() && right.is_uppercase();
297 is_word_start || is_subword_start || left == '\n'
298 })
299}
300
301/// Returns a position of the next word boundary, where a word character is defined as either
302/// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS).
303pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
304 let raw_point = point.to_point(map);
305 let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
306
307 find_boundary(map, point, FindRange::MultiLine, |left, right| {
308 (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(left))
309 || right == '\n'
310 })
311}
312
313/// Returns a position of the next word boundary, where a word character is defined as either
314/// uppercase letter, lowercase letter, '_' character, language-specific word character (like '-' in CSS) or newline.
315pub fn next_word_end_or_newline(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
316 let raw_point = point.to_point(map);
317 let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
318
319 let mut on_starting_row = true;
320 find_boundary(map, point, FindRange::MultiLine, |left, right| {
321 if left == '\n' {
322 on_starting_row = false;
323 }
324 (classifier.kind(left) != classifier.kind(right)
325 && ((on_starting_row && !left.is_whitespace())
326 || (!on_starting_row && !right.is_whitespace())))
327 || right == '\n'
328 })
329}
330
331/// Returns a position of the next subword boundary, where a subword is defined as a run of
332/// word characters of the same "subkind" - where subcharacter kinds are '_' character,
333/// lowerspace characters and uppercase characters.
334pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
335 let raw_point = point.to_point(map);
336 let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
337
338 find_boundary(map, point, FindRange::MultiLine, |left, right| {
339 let is_word_end =
340 (classifier.kind(left) != classifier.kind(right)) && !classifier.is_whitespace(left);
341 let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
342 || left != '_' && right == '_'
343 || left.is_lowercase() && right.is_uppercase();
344 is_word_end || is_subword_end || right == '\n'
345 })
346}
347
348/// Returns a position of the start of the current paragraph, where a paragraph
349/// is defined as a run of non-blank lines.
350pub fn start_of_paragraph(
351 map: &DisplaySnapshot,
352 display_point: DisplayPoint,
353 mut count: usize,
354) -> DisplayPoint {
355 let point = display_point.to_point(map);
356 if point.row == 0 {
357 return DisplayPoint::zero();
358 }
359
360 let mut found_non_blank_line = false;
361 for row in (0..point.row + 1).rev() {
362 let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
363 if found_non_blank_line && blank {
364 if count <= 1 {
365 return Point::new(row, 0).to_display_point(map);
366 }
367 count -= 1;
368 found_non_blank_line = false;
369 }
370
371 found_non_blank_line |= !blank;
372 }
373
374 DisplayPoint::zero()
375}
376
377/// Returns a position of the end of the current paragraph, where a paragraph
378/// is defined as a run of non-blank lines.
379pub fn end_of_paragraph(
380 map: &DisplaySnapshot,
381 display_point: DisplayPoint,
382 mut count: usize,
383) -> DisplayPoint {
384 let point = display_point.to_point(map);
385 if point.row == map.max_buffer_row().0 {
386 return map.max_point();
387 }
388
389 let mut found_non_blank_line = false;
390 for row in point.row..map.max_buffer_row().next_row().0 {
391 let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row));
392 if found_non_blank_line && blank {
393 if count <= 1 {
394 return Point::new(row, 0).to_display_point(map);
395 }
396 count -= 1;
397 found_non_blank_line = false;
398 }
399
400 found_non_blank_line |= !blank;
401 }
402
403 map.max_point()
404}
405
406/// Scans for a boundary preceding the given start point `from` until a boundary is found,
407/// indicated by the given predicate returning true.
408/// The predicate is called with the character to the left and right of the candidate boundary location.
409/// 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.
410pub fn find_preceding_boundary_point(
411 buffer_snapshot: &MultiBufferSnapshot,
412 from: Point,
413 find_range: FindRange,
414 mut is_boundary: impl FnMut(char, char) -> bool,
415) -> Point {
416 let mut prev_ch = None;
417 let mut offset = from.to_offset(buffer_snapshot);
418
419 for ch in buffer_snapshot.reversed_chars_at(offset) {
420 if find_range == FindRange::SingleLine && ch == '\n' {
421 break;
422 }
423 if let Some(prev_ch) = prev_ch {
424 if is_boundary(ch, prev_ch) {
425 break;
426 }
427 }
428
429 offset -= ch.len_utf8();
430 prev_ch = Some(ch);
431 }
432
433 offset.to_point(buffer_snapshot)
434}
435
436/// Scans for a boundary preceding the given start point `from` until a boundary is found,
437/// indicated by the given predicate returning true.
438/// The predicate is called with the character to the left and right of the candidate boundary location.
439/// 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.
440pub fn find_preceding_boundary_display_point(
441 map: &DisplaySnapshot,
442 from: DisplayPoint,
443 find_range: FindRange,
444 is_boundary: impl FnMut(char, char) -> bool,
445) -> DisplayPoint {
446 let result = find_preceding_boundary_point(
447 &map.buffer_snapshot,
448 from.to_point(map),
449 find_range,
450 is_boundary,
451 );
452 map.clip_point(result.to_display_point(map), Bias::Left)
453}
454
455/// Scans for a boundary following the given start point until a boundary is found, indicated by the
456/// given predicate returning true. The predicate is called with the character to the left and right
457/// of the candidate boundary location, and will be called with `\n` characters indicating the start
458/// or end of a line. The function supports optionally returning the point just before the boundary
459/// is found via return_point_before_boundary.
460pub fn find_boundary_point(
461 map: &DisplaySnapshot,
462 from: DisplayPoint,
463 find_range: FindRange,
464 mut is_boundary: impl FnMut(char, char) -> bool,
465 return_point_before_boundary: bool,
466) -> DisplayPoint {
467 let mut offset = from.to_offset(map, Bias::Right);
468 let mut prev_offset = offset;
469 let mut prev_ch = None;
470
471 for ch in map.buffer_snapshot.chars_at(offset) {
472 if find_range == FindRange::SingleLine && ch == '\n' {
473 break;
474 }
475 if let Some(prev_ch) = prev_ch {
476 if is_boundary(prev_ch, ch) {
477 if return_point_before_boundary {
478 return map.clip_point(prev_offset.to_display_point(map), Bias::Right);
479 } else {
480 break;
481 }
482 }
483 }
484 prev_offset = offset;
485 offset += ch.len_utf8();
486 prev_ch = Some(ch);
487 }
488 map.clip_point(offset.to_display_point(map), Bias::Right)
489}
490
491pub fn find_boundary(
492 map: &DisplaySnapshot,
493 from: DisplayPoint,
494 find_range: FindRange,
495 is_boundary: impl FnMut(char, char) -> bool,
496) -> DisplayPoint {
497 find_boundary_point(map, from, find_range, is_boundary, false)
498}
499
500pub fn find_boundary_exclusive(
501 map: &DisplaySnapshot,
502 from: DisplayPoint,
503 find_range: FindRange,
504 is_boundary: impl FnMut(char, char) -> bool,
505) -> DisplayPoint {
506 find_boundary_point(map, from, find_range, is_boundary, true)
507}
508
509/// Returns an iterator over the characters following a given offset in the [`DisplaySnapshot`].
510/// The returned value also contains a range of the start/end of a returned character in
511/// the [`DisplaySnapshot`]. The offsets are relative to the start of a buffer.
512pub fn chars_after(
513 map: &DisplaySnapshot,
514 mut offset: usize,
515) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
516 map.buffer_snapshot.chars_at(offset).map(move |ch| {
517 let before = offset;
518 offset += ch.len_utf8();
519 (ch, before..offset)
520 })
521}
522
523/// Returns a reverse iterator over the characters following a given offset in the [`DisplaySnapshot`].
524/// The returned value also contains a range of the start/end of a returned character in
525/// the [`DisplaySnapshot`]. The offsets are relative to the start of a buffer.
526pub fn chars_before(
527 map: &DisplaySnapshot,
528 mut offset: usize,
529) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
530 map.buffer_snapshot
531 .reversed_chars_at(offset)
532 .map(move |ch| {
533 let after = offset;
534 offset -= ch.len_utf8();
535 (ch, offset..after)
536 })
537}
538
539pub(crate) fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
540 let raw_point = point.to_point(map);
541 let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
542 let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
543 let text = &map.buffer_snapshot;
544 let next_char_kind = text.chars_at(ix).next().map(|c| classifier.kind(c));
545 let prev_char_kind = text
546 .reversed_chars_at(ix)
547 .next()
548 .map(|c| classifier.kind(c));
549 prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
550}
551
552pub(crate) fn surrounding_word(
553 map: &DisplaySnapshot,
554 position: DisplayPoint,
555) -> Range<DisplayPoint> {
556 let position = map
557 .clip_point(position, Bias::Left)
558 .to_offset(map, Bias::Left);
559 let (range, _) = map.buffer_snapshot.surrounding_word(position, false);
560 let start = range
561 .start
562 .to_point(&map.buffer_snapshot)
563 .to_display_point(map);
564 let end = range
565 .end
566 .to_point(&map.buffer_snapshot)
567 .to_display_point(map);
568 start..end
569}
570
571/// Returns a list of lines (represented as a [`DisplayPoint`] range) contained
572/// within a passed range.
573///
574/// The line ranges are **always* going to be in bounds of a requested range, which means that
575/// the first and the last lines might not necessarily represent the
576/// full range of a logical line (as their `.start`/`.end` values are clipped to those of a passed in range).
577pub fn split_display_range_by_lines(
578 map: &DisplaySnapshot,
579 range: Range<DisplayPoint>,
580) -> Vec<Range<DisplayPoint>> {
581 let mut result = Vec::new();
582
583 let mut start = range.start;
584 // Loop over all the covered rows until the one containing the range end
585 for row in range.start.row().0..range.end.row().0 {
586 let row_end_column = map.line_len(DisplayRow(row));
587 let end = map.clip_point(
588 DisplayPoint::new(DisplayRow(row), row_end_column),
589 Bias::Left,
590 );
591 if start != end {
592 result.push(start..end);
593 }
594 start = map.clip_point(DisplayPoint::new(DisplayRow(row + 1), 0), Bias::Left);
595 }
596
597 // Add the final range from the start of the last end to the original range end.
598 result.push(start..range.end);
599
600 result
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606 use crate::{
607 display_map::Inlay,
608 test::{editor_test_context::EditorTestContext, marked_display_snapshot},
609 Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, InlayId, MultiBuffer,
610 };
611 use gpui::{font, px, Context as _};
612 use language::Capability;
613 use project::Project;
614 use settings::SettingsStore;
615 use util::post_inc;
616
617 #[gpui::test]
618 fn test_previous_word_start(cx: &mut gpui::AppContext) {
619 init_test(cx);
620
621 fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
622 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
623 assert_eq!(
624 previous_word_start(&snapshot, display_points[1]),
625 display_points[0]
626 );
627 }
628
629 assert("\nˇ ˇlorem", cx);
630 assert("ˇ\nˇ lorem", cx);
631 assert(" ˇloremˇ", cx);
632 assert("ˇ ˇlorem", cx);
633 assert(" ˇlorˇem", cx);
634 assert("\nlorem\nˇ ˇipsum", cx);
635 assert("\n\nˇ\nˇ", cx);
636 assert(" ˇlorem ˇipsum", cx);
637 assert("loremˇ-ˇipsum", cx);
638 assert("loremˇ-#$@ˇipsum", cx);
639 assert("ˇlorem_ˇipsum", cx);
640 assert(" ˇdefγˇ", cx);
641 assert(" ˇbcΔˇ", cx);
642 assert(" abˇ——ˇcd", cx);
643 }
644
645 #[gpui::test]
646 fn test_previous_subword_start(cx: &mut gpui::AppContext) {
647 init_test(cx);
648
649 fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
650 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
651 assert_eq!(
652 previous_subword_start(&snapshot, display_points[1]),
653 display_points[0]
654 );
655 }
656
657 // Subword boundaries are respected
658 assert("lorem_ˇipˇsum", cx);
659 assert("lorem_ˇipsumˇ", cx);
660 assert("ˇlorem_ˇipsum", cx);
661 assert("lorem_ˇipsum_ˇdolor", cx);
662 assert("loremˇIpˇsum", cx);
663 assert("loremˇIpsumˇ", cx);
664
665 // Word boundaries are still respected
666 assert("\nˇ ˇlorem", cx);
667 assert(" ˇloremˇ", cx);
668 assert(" ˇlorˇem", cx);
669 assert("\nlorem\nˇ ˇipsum", cx);
670 assert("\n\nˇ\nˇ", cx);
671 assert(" ˇlorem ˇipsum", cx);
672 assert("loremˇ-ˇipsum", cx);
673 assert("loremˇ-#$@ˇipsum", cx);
674 assert(" ˇdefγˇ", cx);
675 assert(" bcˇΔˇ", cx);
676 assert(" ˇbcδˇ", cx);
677 assert(" abˇ——ˇcd", cx);
678 }
679
680 #[gpui::test]
681 fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
682 init_test(cx);
683
684 fn assert(
685 marked_text: &str,
686 cx: &mut gpui::AppContext,
687 is_boundary: impl FnMut(char, char) -> bool,
688 ) {
689 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
690 assert_eq!(
691 find_preceding_boundary_display_point(
692 &snapshot,
693 display_points[1],
694 FindRange::MultiLine,
695 is_boundary
696 ),
697 display_points[0]
698 );
699 }
700
701 assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
702 left == 'c' && right == 'd'
703 });
704 assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
705 left == '\n' && right == 'g'
706 });
707 let mut line_count = 0;
708 assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
709 if left == '\n' {
710 line_count += 1;
711 line_count == 2
712 } else {
713 false
714 }
715 });
716 }
717
718 #[gpui::test]
719 fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
720 init_test(cx);
721
722 let input_text = "abcdefghijklmnopqrstuvwxys";
723 let font = font("Helvetica");
724 let font_size = px(14.0);
725 let buffer = MultiBuffer::build_simple(input_text, cx);
726 let buffer_snapshot = buffer.read(cx).snapshot(cx);
727
728 let display_map = cx.new_model(|cx| {
729 DisplayMap::new(
730 buffer,
731 font,
732 font_size,
733 None,
734 true,
735 1,
736 1,
737 1,
738 FoldPlaceholder::test(),
739 cx,
740 )
741 });
742
743 // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
744 let mut id = 0;
745 let inlays = (0..buffer_snapshot.len())
746 .flat_map(|offset| {
747 [
748 Inlay {
749 id: InlayId::Suggestion(post_inc(&mut id)),
750 position: buffer_snapshot.anchor_at(offset, Bias::Left),
751 text: "test".into(),
752 },
753 Inlay {
754 id: InlayId::Suggestion(post_inc(&mut id)),
755 position: buffer_snapshot.anchor_at(offset, Bias::Right),
756 text: "test".into(),
757 },
758 Inlay {
759 id: InlayId::Hint(post_inc(&mut id)),
760 position: buffer_snapshot.anchor_at(offset, Bias::Left),
761 text: "test".into(),
762 },
763 Inlay {
764 id: InlayId::Hint(post_inc(&mut id)),
765 position: buffer_snapshot.anchor_at(offset, Bias::Right),
766 text: "test".into(),
767 },
768 ]
769 })
770 .collect();
771 let snapshot = display_map.update(cx, |map, cx| {
772 map.splice_inlays(Vec::new(), inlays, cx);
773 map.snapshot(cx)
774 });
775
776 assert_eq!(
777 find_preceding_boundary_display_point(
778 &snapshot,
779 buffer_snapshot.len().to_display_point(&snapshot),
780 FindRange::MultiLine,
781 |left, _| left == 'e',
782 ),
783 snapshot
784 .buffer_snapshot
785 .offset_to_point(5)
786 .to_display_point(&snapshot),
787 "Should not stop at inlays when looking for boundaries"
788 );
789 }
790
791 #[gpui::test]
792 fn test_next_word_end(cx: &mut gpui::AppContext) {
793 init_test(cx);
794
795 fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
796 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
797 assert_eq!(
798 next_word_end(&snapshot, display_points[0]),
799 display_points[1]
800 );
801 }
802
803 assert("\nˇ loremˇ", cx);
804 assert(" ˇloremˇ", cx);
805 assert(" lorˇemˇ", cx);
806 assert(" loremˇ ˇ\nipsum\n", cx);
807 assert("\nˇ\nˇ\n\n", cx);
808 assert("loremˇ ipsumˇ ", cx);
809 assert("loremˇ-ˇipsum", cx);
810 assert("loremˇ#$@-ˇipsum", cx);
811 assert("loremˇ_ipsumˇ", cx);
812 assert(" ˇbcΔˇ", cx);
813 assert(" abˇ——ˇcd", cx);
814 }
815
816 #[gpui::test]
817 fn test_next_subword_end(cx: &mut gpui::AppContext) {
818 init_test(cx);
819
820 fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
821 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
822 assert_eq!(
823 next_subword_end(&snapshot, display_points[0]),
824 display_points[1]
825 );
826 }
827
828 // Subword boundaries are respected
829 assert("loˇremˇ_ipsum", cx);
830 assert("ˇloremˇ_ipsum", cx);
831 assert("loremˇ_ipsumˇ", cx);
832 assert("loremˇ_ipsumˇ_dolor", cx);
833 assert("loˇremˇIpsum", cx);
834 assert("loremˇIpsumˇDolor", cx);
835
836 // Word boundaries are still respected
837 assert("\nˇ loremˇ", cx);
838 assert(" ˇloremˇ", cx);
839 assert(" lorˇemˇ", cx);
840 assert(" loremˇ ˇ\nipsum\n", cx);
841 assert("\nˇ\nˇ\n\n", cx);
842 assert("loremˇ ipsumˇ ", cx);
843 assert("loremˇ-ˇipsum", cx);
844 assert("loremˇ#$@-ˇipsum", cx);
845 assert("loremˇ_ipsumˇ", cx);
846 assert(" ˇbcˇΔ", cx);
847 assert(" abˇ——ˇcd", cx);
848 }
849
850 #[gpui::test]
851 fn test_find_boundary(cx: &mut gpui::AppContext) {
852 init_test(cx);
853
854 fn assert(
855 marked_text: &str,
856 cx: &mut gpui::AppContext,
857 is_boundary: impl FnMut(char, char) -> bool,
858 ) {
859 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
860 assert_eq!(
861 find_boundary(
862 &snapshot,
863 display_points[0],
864 FindRange::MultiLine,
865 is_boundary,
866 ),
867 display_points[1]
868 );
869 }
870
871 assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
872 left == 'j' && right == 'k'
873 });
874 assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
875 left == '\n' && right == 'i'
876 });
877 let mut line_count = 0;
878 assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
879 if left == '\n' {
880 line_count += 1;
881 line_count == 2
882 } else {
883 false
884 }
885 });
886 }
887
888 #[gpui::test]
889 fn test_surrounding_word(cx: &mut gpui::AppContext) {
890 init_test(cx);
891
892 fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
893 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
894 assert_eq!(
895 surrounding_word(&snapshot, display_points[1]),
896 display_points[0]..display_points[2],
897 "{}",
898 marked_text
899 );
900 }
901
902 assert("ˇˇloremˇ ipsum", cx);
903 assert("ˇloˇremˇ ipsum", cx);
904 assert("ˇloremˇˇ ipsum", cx);
905 assert("loremˇ ˇ ˇipsum", cx);
906 assert("lorem\nˇˇˇ\nipsum", cx);
907 assert("lorem\nˇˇipsumˇ", cx);
908 assert("loremˇ,ˇˇ ipsum", cx);
909 assert("ˇloremˇˇ, ipsum", cx);
910 }
911
912 #[gpui::test]
913 async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
914 cx.update(|cx| {
915 init_test(cx);
916 });
917
918 let mut cx = EditorTestContext::new(cx).await;
919 let editor = cx.editor.clone();
920 let window = cx.window;
921 _ = cx.update_window(window, |_, cx| {
922 let text_layout_details =
923 editor.update(cx, |editor, cx| editor.text_layout_details(cx));
924
925 let font = font("Helvetica");
926
927 let buffer = cx.new_model(|cx| Buffer::local("abc\ndefg\nhijkl\nmn", cx));
928 let multibuffer = cx.new_model(|cx| {
929 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
930 multibuffer.push_excerpts(
931 buffer.clone(),
932 [
933 ExcerptRange {
934 context: Point::new(0, 0)..Point::new(1, 4),
935 primary: None,
936 },
937 ExcerptRange {
938 context: Point::new(2, 0)..Point::new(3, 2),
939 primary: None,
940 },
941 ],
942 cx,
943 );
944 multibuffer
945 });
946 let display_map = cx.new_model(|cx| {
947 DisplayMap::new(
948 multibuffer,
949 font,
950 px(14.0),
951 None,
952 true,
953 0,
954 2,
955 0,
956 FoldPlaceholder::test(),
957 cx,
958 )
959 });
960 let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
961
962 assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
963
964 let col_2_x = snapshot
965 .x_for_display_point(DisplayPoint::new(DisplayRow(2), 2), &text_layout_details);
966
967 // Can't move up into the first excerpt's header
968 assert_eq!(
969 up(
970 &snapshot,
971 DisplayPoint::new(DisplayRow(2), 2),
972 SelectionGoal::HorizontalPosition(col_2_x.0),
973 false,
974 &text_layout_details
975 ),
976 (
977 DisplayPoint::new(DisplayRow(2), 0),
978 SelectionGoal::HorizontalPosition(col_2_x.0),
979 ),
980 );
981 assert_eq!(
982 up(
983 &snapshot,
984 DisplayPoint::new(DisplayRow(2), 0),
985 SelectionGoal::None,
986 false,
987 &text_layout_details
988 ),
989 (
990 DisplayPoint::new(DisplayRow(2), 0),
991 SelectionGoal::HorizontalPosition(0.0),
992 ),
993 );
994
995 let col_4_x = snapshot
996 .x_for_display_point(DisplayPoint::new(DisplayRow(3), 4), &text_layout_details);
997
998 // Move up and down within first excerpt
999 assert_eq!(
1000 up(
1001 &snapshot,
1002 DisplayPoint::new(DisplayRow(3), 4),
1003 SelectionGoal::HorizontalPosition(col_4_x.0),
1004 false,
1005 &text_layout_details
1006 ),
1007 (
1008 DisplayPoint::new(DisplayRow(2), 3),
1009 SelectionGoal::HorizontalPosition(col_4_x.0)
1010 ),
1011 );
1012 assert_eq!(
1013 down(
1014 &snapshot,
1015 DisplayPoint::new(DisplayRow(2), 3),
1016 SelectionGoal::HorizontalPosition(col_4_x.0),
1017 false,
1018 &text_layout_details
1019 ),
1020 (
1021 DisplayPoint::new(DisplayRow(3), 4),
1022 SelectionGoal::HorizontalPosition(col_4_x.0)
1023 ),
1024 );
1025
1026 let col_5_x = snapshot
1027 .x_for_display_point(DisplayPoint::new(DisplayRow(6), 5), &text_layout_details);
1028
1029 // Move up and down across second excerpt's header
1030 assert_eq!(
1031 up(
1032 &snapshot,
1033 DisplayPoint::new(DisplayRow(6), 5),
1034 SelectionGoal::HorizontalPosition(col_5_x.0),
1035 false,
1036 &text_layout_details
1037 ),
1038 (
1039 DisplayPoint::new(DisplayRow(3), 4),
1040 SelectionGoal::HorizontalPosition(col_5_x.0)
1041 ),
1042 );
1043 assert_eq!(
1044 down(
1045 &snapshot,
1046 DisplayPoint::new(DisplayRow(3), 4),
1047 SelectionGoal::HorizontalPosition(col_5_x.0),
1048 false,
1049 &text_layout_details
1050 ),
1051 (
1052 DisplayPoint::new(DisplayRow(6), 5),
1053 SelectionGoal::HorizontalPosition(col_5_x.0)
1054 ),
1055 );
1056
1057 let max_point_x = snapshot
1058 .x_for_display_point(DisplayPoint::new(DisplayRow(7), 2), &text_layout_details);
1059
1060 // Can't move down off the end, and attempting to do so leaves the selection goal unchanged
1061 assert_eq!(
1062 down(
1063 &snapshot,
1064 DisplayPoint::new(DisplayRow(7), 0),
1065 SelectionGoal::HorizontalPosition(0.0),
1066 false,
1067 &text_layout_details
1068 ),
1069 (
1070 DisplayPoint::new(DisplayRow(7), 2),
1071 SelectionGoal::HorizontalPosition(0.0)
1072 ),
1073 );
1074 assert_eq!(
1075 down(
1076 &snapshot,
1077 DisplayPoint::new(DisplayRow(7), 2),
1078 SelectionGoal::HorizontalPosition(max_point_x.0),
1079 false,
1080 &text_layout_details
1081 ),
1082 (
1083 DisplayPoint::new(DisplayRow(7), 2),
1084 SelectionGoal::HorizontalPosition(max_point_x.0)
1085 ),
1086 );
1087 });
1088 }
1089
1090 fn init_test(cx: &mut gpui::AppContext) {
1091 let settings_store = SettingsStore::test(cx);
1092 cx.set_global(settings_store);
1093 theme::init(theme::LoadThemes::JustBase, cx);
1094 language::init(cx);
1095 crate::init(cx);
1096 Project::init_settings(cx);
1097 }
1098}