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