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