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, 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.buffer_snapshot.max_row().0 {
386 return map.max_point();
387 }
388
389 let mut found_non_blank_line = false;
390 for row in point.row..=map.buffer_snapshot.max_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_preceding_boundary_trail(
492 map: &DisplaySnapshot,
493 head: DisplayPoint,
494 mut is_boundary: impl FnMut(char, char) -> bool,
495) -> (Option<DisplayPoint>, DisplayPoint) {
496 let mut offset = head.to_offset(map, Bias::Left);
497 let mut trail_offset = None;
498
499 let mut prev_ch = map.buffer_snapshot.chars_at(offset).next();
500 let mut forward = map.buffer_snapshot.reversed_chars_at(offset).peekable();
501
502 // Skip newlines
503 while let Some(&ch) = forward.peek() {
504 if ch == '\n' {
505 prev_ch = forward.next();
506 offset -= ch.len_utf8();
507 trail_offset = Some(offset);
508 } else {
509 break;
510 }
511 }
512
513 // Find the boundary
514 let start_offset = offset;
515 for ch in forward {
516 if let Some(prev_ch) = prev_ch {
517 if is_boundary(prev_ch, ch) {
518 if start_offset == offset {
519 trail_offset = Some(offset);
520 } else {
521 break;
522 }
523 }
524 }
525 offset -= ch.len_utf8();
526 prev_ch = Some(ch);
527 }
528
529 let trail = trail_offset
530 .map(|trail_offset: usize| map.clip_point(trail_offset.to_display_point(map), Bias::Left));
531
532 (
533 trail,
534 map.clip_point(offset.to_display_point(map), Bias::Left),
535 )
536}
537
538/// Finds the location of a boundary
539pub fn find_boundary_trail(
540 map: &DisplaySnapshot,
541 head: DisplayPoint,
542 mut is_boundary: impl FnMut(char, char) -> bool,
543) -> (Option<DisplayPoint>, DisplayPoint) {
544 let mut offset = head.to_offset(map, Bias::Right);
545 let mut trail_offset = None;
546
547 let mut prev_ch = map.buffer_snapshot.reversed_chars_at(offset).next();
548 let mut forward = map.buffer_snapshot.chars_at(offset).peekable();
549
550 // Skip newlines
551 while let Some(&ch) = forward.peek() {
552 if ch == '\n' {
553 prev_ch = forward.next();
554 offset += ch.len_utf8();
555 trail_offset = Some(offset);
556 } else {
557 break;
558 }
559 }
560
561 // Find the boundary
562 let start_offset = offset;
563 for ch in forward {
564 if let Some(prev_ch) = prev_ch {
565 if is_boundary(prev_ch, ch) {
566 if start_offset == offset {
567 trail_offset = Some(offset);
568 } else {
569 break;
570 }
571 }
572 }
573 offset += ch.len_utf8();
574 prev_ch = Some(ch);
575 }
576
577 let trail = trail_offset
578 .map(|trail_offset: usize| map.clip_point(trail_offset.to_display_point(map), Bias::Right));
579
580 (
581 trail,
582 map.clip_point(offset.to_display_point(map), Bias::Right),
583 )
584}
585
586pub fn find_boundary(
587 map: &DisplaySnapshot,
588 from: DisplayPoint,
589 find_range: FindRange,
590 is_boundary: impl FnMut(char, char) -> bool,
591) -> DisplayPoint {
592 find_boundary_point(map, from, find_range, is_boundary, false)
593}
594
595pub fn find_boundary_exclusive(
596 map: &DisplaySnapshot,
597 from: DisplayPoint,
598 find_range: FindRange,
599 is_boundary: impl FnMut(char, char) -> bool,
600) -> DisplayPoint {
601 find_boundary_point(map, from, find_range, is_boundary, true)
602}
603
604/// Returns an iterator over the characters following a given offset in the [`DisplaySnapshot`].
605/// The returned value also contains a range of the start/end of a returned character in
606/// the [`DisplaySnapshot`]. The offsets are relative to the start of a buffer.
607pub fn chars_after(
608 map: &DisplaySnapshot,
609 mut offset: usize,
610) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
611 map.buffer_snapshot.chars_at(offset).map(move |ch| {
612 let before = offset;
613 offset += ch.len_utf8();
614 (ch, before..offset)
615 })
616}
617
618/// Returns a reverse iterator over the characters following a given offset in the [`DisplaySnapshot`].
619/// The returned value also contains a range of the start/end of a returned character in
620/// the [`DisplaySnapshot`]. The offsets are relative to the start of a buffer.
621pub fn chars_before(
622 map: &DisplaySnapshot,
623 mut offset: usize,
624) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
625 map.buffer_snapshot
626 .reversed_chars_at(offset)
627 .map(move |ch| {
628 let after = offset;
629 offset -= ch.len_utf8();
630 (ch, offset..after)
631 })
632}
633
634pub(crate) fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
635 let raw_point = point.to_point(map);
636 let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
637 let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
638 let text = &map.buffer_snapshot;
639 let next_char_kind = text.chars_at(ix).next().map(|c| classifier.kind(c));
640 let prev_char_kind = text
641 .reversed_chars_at(ix)
642 .next()
643 .map(|c| classifier.kind(c));
644 prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
645}
646
647pub(crate) fn surrounding_word(
648 map: &DisplaySnapshot,
649 position: DisplayPoint,
650) -> Range<DisplayPoint> {
651 let position = map
652 .clip_point(position, Bias::Left)
653 .to_offset(map, Bias::Left);
654 let (range, _) = map.buffer_snapshot.surrounding_word(position, false);
655 let start = range
656 .start
657 .to_point(&map.buffer_snapshot)
658 .to_display_point(map);
659 let end = range
660 .end
661 .to_point(&map.buffer_snapshot)
662 .to_display_point(map);
663 start..end
664}
665
666/// Returns a list of lines (represented as a [`DisplayPoint`] range) contained
667/// within a passed range.
668///
669/// The line ranges are **always* going to be in bounds of a requested range, which means that
670/// the first and the last lines might not necessarily represent the
671/// full range of a logical line (as their `.start`/`.end` values are clipped to those of a passed in range).
672pub fn split_display_range_by_lines(
673 map: &DisplaySnapshot,
674 range: Range<DisplayPoint>,
675) -> Vec<Range<DisplayPoint>> {
676 let mut result = Vec::new();
677
678 let mut start = range.start;
679 // Loop over all the covered rows until the one containing the range end
680 for row in range.start.row().0..range.end.row().0 {
681 let row_end_column = map.line_len(DisplayRow(row));
682 let end = map.clip_point(
683 DisplayPoint::new(DisplayRow(row), row_end_column),
684 Bias::Left,
685 );
686 if start != end {
687 result.push(start..end);
688 }
689 start = map.clip_point(DisplayPoint::new(DisplayRow(row + 1), 0), Bias::Left);
690 }
691
692 // Add the final range from the start of the last end to the original range end.
693 result.push(start..range.end);
694
695 result
696}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701 use crate::{
702 display_map::Inlay,
703 test::{editor_test_context::EditorTestContext, marked_display_snapshot},
704 Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, InlayId, MultiBuffer,
705 };
706 use gpui::{font, px, AppContext as _};
707 use language::Capability;
708 use project::Project;
709 use settings::SettingsStore;
710 use util::post_inc;
711
712 #[gpui::test]
713 fn test_previous_word_start(cx: &mut gpui::App) {
714 init_test(cx);
715
716 fn assert(marked_text: &str, cx: &mut gpui::App) {
717 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
718 assert_eq!(
719 previous_word_start(&snapshot, display_points[1]),
720 display_points[0]
721 );
722 }
723
724 assert("\nˇ ˇlorem", cx);
725 assert("ˇ\nˇ lorem", cx);
726 assert(" ˇloremˇ", cx);
727 assert("ˇ ˇlorem", cx);
728 assert(" ˇlorˇem", cx);
729 assert("\nlorem\nˇ ˇipsum", cx);
730 assert("\n\nˇ\nˇ", cx);
731 assert(" ˇlorem ˇipsum", cx);
732 assert("loremˇ-ˇipsum", cx);
733 assert("loremˇ-#$@ˇipsum", cx);
734 assert("ˇlorem_ˇipsum", cx);
735 assert(" ˇdefγˇ", cx);
736 assert(" ˇbcΔˇ", cx);
737 assert(" abˇ——ˇcd", cx);
738 }
739
740 #[gpui::test]
741 fn test_previous_subword_start(cx: &mut gpui::App) {
742 init_test(cx);
743
744 fn assert(marked_text: &str, cx: &mut gpui::App) {
745 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
746 assert_eq!(
747 previous_subword_start(&snapshot, display_points[1]),
748 display_points[0]
749 );
750 }
751
752 // Subword boundaries are respected
753 assert("lorem_ˇipˇsum", cx);
754 assert("lorem_ˇipsumˇ", cx);
755 assert("ˇlorem_ˇipsum", cx);
756 assert("lorem_ˇipsum_ˇdolor", cx);
757 assert("loremˇIpˇsum", cx);
758 assert("loremˇIpsumˇ", cx);
759
760 // Word boundaries are still respected
761 assert("\nˇ ˇlorem", cx);
762 assert(" ˇloremˇ", cx);
763 assert(" ˇlorˇem", cx);
764 assert("\nlorem\nˇ ˇipsum", cx);
765 assert("\n\nˇ\nˇ", cx);
766 assert(" ˇlorem ˇipsum", cx);
767 assert("loremˇ-ˇipsum", cx);
768 assert("loremˇ-#$@ˇipsum", cx);
769 assert(" ˇdefγˇ", cx);
770 assert(" bcˇΔˇ", cx);
771 assert(" ˇbcδˇ", cx);
772 assert(" abˇ——ˇcd", cx);
773 }
774
775 #[gpui::test]
776 fn test_find_preceding_boundary(cx: &mut gpui::App) {
777 init_test(cx);
778
779 fn assert(
780 marked_text: &str,
781 cx: &mut gpui::App,
782 is_boundary: impl FnMut(char, char) -> bool,
783 ) {
784 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
785 assert_eq!(
786 find_preceding_boundary_display_point(
787 &snapshot,
788 display_points[1],
789 FindRange::MultiLine,
790 is_boundary
791 ),
792 display_points[0]
793 );
794 }
795
796 assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
797 left == 'c' && right == 'd'
798 });
799 assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
800 left == '\n' && right == 'g'
801 });
802 let mut line_count = 0;
803 assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
804 if left == '\n' {
805 line_count += 1;
806 line_count == 2
807 } else {
808 false
809 }
810 });
811 }
812
813 #[gpui::test]
814 fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::App) {
815 init_test(cx);
816
817 let input_text = "abcdefghijklmnopqrstuvwxys";
818 let font = font("Helvetica");
819 let font_size = px(14.0);
820 let buffer = MultiBuffer::build_simple(input_text, cx);
821 let buffer_snapshot = buffer.read(cx).snapshot(cx);
822
823 let display_map = cx.new(|cx| {
824 DisplayMap::new(
825 buffer,
826 font,
827 font_size,
828 None,
829 true,
830 1,
831 1,
832 1,
833 FoldPlaceholder::test(),
834 cx,
835 )
836 });
837
838 // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
839 let mut id = 0;
840 let inlays = (0..buffer_snapshot.len())
841 .flat_map(|offset| {
842 [
843 Inlay {
844 id: InlayId::InlineCompletion(post_inc(&mut id)),
845 position: buffer_snapshot.anchor_at(offset, Bias::Left),
846 text: "test".into(),
847 },
848 Inlay {
849 id: InlayId::InlineCompletion(post_inc(&mut id)),
850 position: buffer_snapshot.anchor_at(offset, Bias::Right),
851 text: "test".into(),
852 },
853 Inlay {
854 id: InlayId::Hint(post_inc(&mut id)),
855 position: buffer_snapshot.anchor_at(offset, Bias::Left),
856 text: "test".into(),
857 },
858 Inlay {
859 id: InlayId::Hint(post_inc(&mut id)),
860 position: buffer_snapshot.anchor_at(offset, Bias::Right),
861 text: "test".into(),
862 },
863 ]
864 })
865 .collect();
866 let snapshot = display_map.update(cx, |map, cx| {
867 map.splice_inlays(&[], inlays, cx);
868 map.snapshot(cx)
869 });
870
871 assert_eq!(
872 find_preceding_boundary_display_point(
873 &snapshot,
874 buffer_snapshot.len().to_display_point(&snapshot),
875 FindRange::MultiLine,
876 |left, _| left == 'e',
877 ),
878 snapshot
879 .buffer_snapshot
880 .offset_to_point(5)
881 .to_display_point(&snapshot),
882 "Should not stop at inlays when looking for boundaries"
883 );
884 }
885
886 #[gpui::test]
887 fn test_next_word_end(cx: &mut gpui::App) {
888 init_test(cx);
889
890 fn assert(marked_text: &str, cx: &mut gpui::App) {
891 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
892 assert_eq!(
893 next_word_end(&snapshot, display_points[0]),
894 display_points[1]
895 );
896 }
897
898 assert("\nˇ loremˇ", cx);
899 assert(" ˇloremˇ", cx);
900 assert(" lorˇemˇ", cx);
901 assert(" loremˇ ˇ\nipsum\n", cx);
902 assert("\nˇ\nˇ\n\n", cx);
903 assert("loremˇ ipsumˇ ", cx);
904 assert("loremˇ-ˇipsum", cx);
905 assert("loremˇ#$@-ˇipsum", cx);
906 assert("loremˇ_ipsumˇ", cx);
907 assert(" ˇbcΔˇ", cx);
908 assert(" abˇ——ˇcd", cx);
909 }
910
911 #[gpui::test]
912 fn test_next_subword_end(cx: &mut gpui::App) {
913 init_test(cx);
914
915 fn assert(marked_text: &str, cx: &mut gpui::App) {
916 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
917 assert_eq!(
918 next_subword_end(&snapshot, display_points[0]),
919 display_points[1]
920 );
921 }
922
923 // Subword boundaries are respected
924 assert("loˇremˇ_ipsum", cx);
925 assert("ˇloremˇ_ipsum", cx);
926 assert("loremˇ_ipsumˇ", cx);
927 assert("loremˇ_ipsumˇ_dolor", cx);
928 assert("loˇremˇIpsum", cx);
929 assert("loremˇIpsumˇDolor", cx);
930
931 // Word boundaries are still respected
932 assert("\nˇ loremˇ", cx);
933 assert(" ˇloremˇ", cx);
934 assert(" lorˇemˇ", cx);
935 assert(" loremˇ ˇ\nipsum\n", cx);
936 assert("\nˇ\nˇ\n\n", cx);
937 assert("loremˇ ipsumˇ ", cx);
938 assert("loremˇ-ˇipsum", cx);
939 assert("loremˇ#$@-ˇipsum", cx);
940 assert("loremˇ_ipsumˇ", cx);
941 assert(" ˇbcˇΔ", cx);
942 assert(" abˇ——ˇcd", cx);
943 }
944
945 #[gpui::test]
946 fn test_find_boundary(cx: &mut gpui::App) {
947 init_test(cx);
948
949 fn assert(
950 marked_text: &str,
951 cx: &mut gpui::App,
952 is_boundary: impl FnMut(char, char) -> bool,
953 ) {
954 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
955 assert_eq!(
956 find_boundary(
957 &snapshot,
958 display_points[0],
959 FindRange::MultiLine,
960 is_boundary,
961 ),
962 display_points[1]
963 );
964 }
965
966 assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
967 left == 'j' && right == 'k'
968 });
969 assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
970 left == '\n' && right == 'i'
971 });
972 let mut line_count = 0;
973 assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
974 if left == '\n' {
975 line_count += 1;
976 line_count == 2
977 } else {
978 false
979 }
980 });
981 }
982
983 #[gpui::test]
984 fn test_surrounding_word(cx: &mut gpui::App) {
985 init_test(cx);
986
987 fn assert(marked_text: &str, cx: &mut gpui::App) {
988 let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
989 assert_eq!(
990 surrounding_word(&snapshot, display_points[1]),
991 display_points[0]..display_points[2],
992 "{}",
993 marked_text
994 );
995 }
996
997 assert("ˇˇloremˇ ipsum", cx);
998 assert("ˇloˇremˇ ipsum", cx);
999 assert("ˇloremˇˇ ipsum", cx);
1000 assert("loremˇ ˇ ˇipsum", cx);
1001 assert("lorem\nˇˇˇ\nipsum", cx);
1002 assert("lorem\nˇˇipsumˇ", cx);
1003 assert("loremˇ,ˇˇ ipsum", cx);
1004 assert("ˇloremˇˇ, ipsum", cx);
1005 }
1006
1007 #[gpui::test]
1008 async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
1009 cx.update(|cx| {
1010 init_test(cx);
1011 });
1012
1013 let mut cx = EditorTestContext::new(cx).await;
1014 let editor = cx.editor.clone();
1015 let window = cx.window;
1016 _ = cx.update_window(window, |_, window, cx| {
1017 let text_layout_details = editor.read(cx).text_layout_details(window);
1018
1019 let font = font("Helvetica");
1020
1021 let buffer = cx.new(|cx| Buffer::local("abc\ndefg\nhijkl\nmn", cx));
1022 let multibuffer = cx.new(|cx| {
1023 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
1024 multibuffer.push_excerpts(
1025 buffer.clone(),
1026 [
1027 ExcerptRange {
1028 context: Point::new(0, 0)..Point::new(1, 4),
1029 primary: None,
1030 },
1031 ExcerptRange {
1032 context: Point::new(2, 0)..Point::new(3, 2),
1033 primary: None,
1034 },
1035 ],
1036 cx,
1037 );
1038 multibuffer
1039 });
1040 let display_map = cx.new(|cx| {
1041 DisplayMap::new(
1042 multibuffer,
1043 font,
1044 px(14.0),
1045 None,
1046 true,
1047 0,
1048 2,
1049 0,
1050 FoldPlaceholder::test(),
1051 cx,
1052 )
1053 });
1054 let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
1055
1056 assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
1057
1058 let col_2_x = snapshot
1059 .x_for_display_point(DisplayPoint::new(DisplayRow(2), 2), &text_layout_details);
1060
1061 // Can't move up into the first excerpt's header
1062 assert_eq!(
1063 up(
1064 &snapshot,
1065 DisplayPoint::new(DisplayRow(2), 2),
1066 SelectionGoal::HorizontalPosition(col_2_x.0),
1067 false,
1068 &text_layout_details
1069 ),
1070 (
1071 DisplayPoint::new(DisplayRow(2), 0),
1072 SelectionGoal::HorizontalPosition(col_2_x.0),
1073 ),
1074 );
1075 assert_eq!(
1076 up(
1077 &snapshot,
1078 DisplayPoint::new(DisplayRow(2), 0),
1079 SelectionGoal::None,
1080 false,
1081 &text_layout_details
1082 ),
1083 (
1084 DisplayPoint::new(DisplayRow(2), 0),
1085 SelectionGoal::HorizontalPosition(0.0),
1086 ),
1087 );
1088
1089 let col_4_x = snapshot
1090 .x_for_display_point(DisplayPoint::new(DisplayRow(3), 4), &text_layout_details);
1091
1092 // Move up and down within first excerpt
1093 assert_eq!(
1094 up(
1095 &snapshot,
1096 DisplayPoint::new(DisplayRow(3), 4),
1097 SelectionGoal::HorizontalPosition(col_4_x.0),
1098 false,
1099 &text_layout_details
1100 ),
1101 (
1102 DisplayPoint::new(DisplayRow(2), 3),
1103 SelectionGoal::HorizontalPosition(col_4_x.0)
1104 ),
1105 );
1106 assert_eq!(
1107 down(
1108 &snapshot,
1109 DisplayPoint::new(DisplayRow(2), 3),
1110 SelectionGoal::HorizontalPosition(col_4_x.0),
1111 false,
1112 &text_layout_details
1113 ),
1114 (
1115 DisplayPoint::new(DisplayRow(3), 4),
1116 SelectionGoal::HorizontalPosition(col_4_x.0)
1117 ),
1118 );
1119
1120 let col_5_x = snapshot
1121 .x_for_display_point(DisplayPoint::new(DisplayRow(6), 5), &text_layout_details);
1122
1123 // Move up and down across second excerpt's header
1124 assert_eq!(
1125 up(
1126 &snapshot,
1127 DisplayPoint::new(DisplayRow(6), 5),
1128 SelectionGoal::HorizontalPosition(col_5_x.0),
1129 false,
1130 &text_layout_details
1131 ),
1132 (
1133 DisplayPoint::new(DisplayRow(3), 4),
1134 SelectionGoal::HorizontalPosition(col_5_x.0)
1135 ),
1136 );
1137 assert_eq!(
1138 down(
1139 &snapshot,
1140 DisplayPoint::new(DisplayRow(3), 4),
1141 SelectionGoal::HorizontalPosition(col_5_x.0),
1142 false,
1143 &text_layout_details
1144 ),
1145 (
1146 DisplayPoint::new(DisplayRow(6), 5),
1147 SelectionGoal::HorizontalPosition(col_5_x.0)
1148 ),
1149 );
1150
1151 let max_point_x = snapshot
1152 .x_for_display_point(DisplayPoint::new(DisplayRow(7), 2), &text_layout_details);
1153
1154 // Can't move down off the end, and attempting to do so leaves the selection goal unchanged
1155 assert_eq!(
1156 down(
1157 &snapshot,
1158 DisplayPoint::new(DisplayRow(7), 0),
1159 SelectionGoal::HorizontalPosition(0.0),
1160 false,
1161 &text_layout_details
1162 ),
1163 (
1164 DisplayPoint::new(DisplayRow(7), 2),
1165 SelectionGoal::HorizontalPosition(0.0)
1166 ),
1167 );
1168 assert_eq!(
1169 down(
1170 &snapshot,
1171 DisplayPoint::new(DisplayRow(7), 2),
1172 SelectionGoal::HorizontalPosition(max_point_x.0),
1173 false,
1174 &text_layout_details
1175 ),
1176 (
1177 DisplayPoint::new(DisplayRow(7), 2),
1178 SelectionGoal::HorizontalPosition(max_point_x.0)
1179 ),
1180 );
1181 });
1182 }
1183
1184 fn init_test(cx: &mut gpui::App) {
1185 let settings_store = SettingsStore::test(cx);
1186 cx.set_global(settings_store);
1187 workspace::init_settings(cx);
1188 theme::init(theme::LoadThemes::JustBase, cx);
1189 language::init(cx);
1190 crate::init(cx);
1191 Project::init_settings(cx);
1192 }
1193}