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 prev_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
136 let mut line_start = 0;
137 if point.row() > 0 {
138 if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
139 line_start = indent;
140 }
141 }
142
143 if point.column() == line_start {
144 if point.row() == 0 {
145 return DisplayPoint::new(0, 0);
146 } else {
147 let row = point.row() - 1;
148 point = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left);
149 }
150 }
151
152 let mut boundary = DisplayPoint::new(point.row(), 0);
153 let mut column = 0;
154 let mut prev_char_kind = CharKind::Newline;
155 for c in map.chars_at(DisplayPoint::new(point.row(), 0)) {
156 if column >= point.column() {
157 break;
158 }
159
160 let char_kind = char_kind(c);
161 if char_kind != prev_char_kind
162 && char_kind != CharKind::Whitespace
163 && char_kind != CharKind::Newline
164 {
165 *boundary.column_mut() = column;
166 }
167
168 prev_char_kind = char_kind;
169 column += c.len_utf8() as u32;
170 }
171 boundary
172}
173
174pub fn next_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
175 let mut prev_char_kind = None;
176 for c in map.chars_at(point) {
177 let char_kind = char_kind(c);
178 if let Some(prev_char_kind) = prev_char_kind {
179 if c == '\n' {
180 break;
181 }
182 if prev_char_kind != char_kind
183 && prev_char_kind != CharKind::Whitespace
184 && prev_char_kind != CharKind::Newline
185 {
186 break;
187 }
188 }
189
190 if c == '\n' {
191 *point.row_mut() += 1;
192 *point.column_mut() = 0;
193 } else {
194 *point.column_mut() += c.len_utf8() as u32;
195 }
196 prev_char_kind = Some(char_kind);
197 }
198 map.clip_point(point, Bias::Right)
199}
200
201pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
202 let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
203 let text = &map.buffer_snapshot;
204 let next_char_kind = text.chars_at(ix).next().map(char_kind);
205 let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind);
206 prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
207}
208
209pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<DisplayPoint> {
210 let position = map
211 .clip_point(position, Bias::Left)
212 .to_offset(map, Bias::Left);
213 let (range, _) = map.buffer_snapshot.surrounding_word(position);
214 let start = range
215 .start
216 .to_point(&map.buffer_snapshot)
217 .to_display_point(map);
218 let end = range
219 .end
220 .to_point(&map.buffer_snapshot)
221 .to_display_point(map);
222 start..end
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use crate::{
229 display_map::{BlockDisposition, BlockProperties},
230 Buffer, DisplayMap, ExcerptProperties, MultiBuffer,
231 };
232 use gpui::{elements::Empty, Element};
233 use language::Point;
234 use std::sync::Arc;
235
236 #[gpui::test]
237 fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
238 let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
239 let font_id = cx
240 .font_cache()
241 .select_font(family_id, &Default::default())
242 .unwrap();
243
244 let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx));
245 let mut excerpt1_header_position = None;
246 let mut excerpt2_header_position = None;
247 let multibuffer = cx.add_model(|cx| {
248 let mut multibuffer = MultiBuffer::new(0);
249 let excerpt1_id = multibuffer.push_excerpt(
250 ExcerptProperties {
251 buffer: &buffer,
252 range: Point::new(0, 0)..Point::new(1, 4),
253 },
254 cx,
255 );
256 let excerpt2_id = multibuffer.push_excerpt(
257 ExcerptProperties {
258 buffer: &buffer,
259 range: Point::new(2, 0)..Point::new(3, 2),
260 },
261 cx,
262 );
263
264 excerpt1_header_position = Some(
265 multibuffer
266 .read(cx)
267 .anchor_in_excerpt(excerpt1_id, language::Anchor::min()),
268 );
269 excerpt2_header_position = Some(
270 multibuffer
271 .read(cx)
272 .anchor_in_excerpt(excerpt2_id, language::Anchor::min()),
273 );
274 multibuffer
275 });
276
277 let display_map =
278 cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, cx));
279 display_map.update(cx, |display_map, cx| {
280 display_map.insert_blocks(
281 [
282 BlockProperties {
283 position: excerpt1_header_position.unwrap(),
284 height: 2,
285 render: Arc::new(|_| Empty::new().boxed()),
286 disposition: BlockDisposition::Above,
287 },
288 BlockProperties {
289 position: excerpt2_header_position.unwrap(),
290 height: 3,
291 render: Arc::new(|_| Empty::new().boxed()),
292 disposition: BlockDisposition::Above,
293 },
294 ],
295 cx,
296 )
297 });
298
299 let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
300 assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\n\nhijkl\nmn");
301
302 // Can't move up into the first excerpt's header
303 assert_eq!(
304 up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)).unwrap(),
305 (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
306 );
307 assert_eq!(
308 up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None).unwrap(),
309 (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
310 );
311
312 // Move up and down within first excerpt
313 assert_eq!(
314 up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)).unwrap(),
315 (DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
316 );
317 assert_eq!(
318 down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)).unwrap(),
319 (DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
320 );
321
322 // Move up and down across second excerpt's header
323 assert_eq!(
324 up(&snapshot, DisplayPoint::new(7, 5), SelectionGoal::Column(5)).unwrap(),
325 (DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
326 );
327 assert_eq!(
328 down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)).unwrap(),
329 (DisplayPoint::new(7, 5), SelectionGoal::Column(5)),
330 );
331
332 // Can't move down off the end
333 assert_eq!(
334 down(&snapshot, DisplayPoint::new(8, 0), SelectionGoal::Column(0)).unwrap(),
335 (DisplayPoint::new(8, 2), SelectionGoal::Column(2)),
336 );
337 assert_eq!(
338 down(&snapshot, DisplayPoint::new(8, 2), SelectionGoal::Column(2)).unwrap(),
339 (DisplayPoint::new(8, 2), SelectionGoal::Column(2)),
340 );
341 }
342
343 #[gpui::test]
344 fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
345 let tab_size = 4;
346 let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
347 let font_id = cx
348 .font_cache()
349 .select_font(family_id, &Default::default())
350 .unwrap();
351 let font_size = 14.0;
352
353 let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx);
354 let display_map =
355 cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
356 let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
357 assert_eq!(
358 prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)),
359 DisplayPoint::new(0, 7)
360 );
361 assert_eq!(
362 prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
363 DisplayPoint::new(0, 2)
364 );
365 assert_eq!(
366 prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
367 DisplayPoint::new(0, 2)
368 );
369 assert_eq!(
370 prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
371 DisplayPoint::new(0, 0)
372 );
373 assert_eq!(
374 prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
375 DisplayPoint::new(0, 0)
376 );
377
378 assert_eq!(
379 next_word_boundary(&snapshot, DisplayPoint::new(0, 0)),
380 DisplayPoint::new(0, 1)
381 );
382 assert_eq!(
383 next_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
384 DisplayPoint::new(0, 6)
385 );
386 assert_eq!(
387 next_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
388 DisplayPoint::new(0, 6)
389 );
390 assert_eq!(
391 next_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
392 DisplayPoint::new(0, 12)
393 );
394 assert_eq!(
395 next_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
396 DisplayPoint::new(0, 12)
397 );
398 }
399
400 #[gpui::test]
401 fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
402 let tab_size = 4;
403 let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
404 let font_id = cx
405 .font_cache()
406 .select_font(family_id, &Default::default())
407 .unwrap();
408 let font_size = 14.0;
409 let buffer = MultiBuffer::build_simple("lorem ipsum dolor\n sit", cx);
410 let display_map =
411 cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
412 let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
413
414 assert_eq!(
415 surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
416 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
417 );
418 assert_eq!(
419 surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
420 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
421 );
422 assert_eq!(
423 surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
424 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
425 );
426 assert_eq!(
427 surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
428 DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
429 );
430 assert_eq!(
431 surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
432 DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
433 );
434 assert_eq!(
435 surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
436 DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
437 );
438 assert_eq!(
439 surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
440 DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14),
441 );
442 assert_eq!(
443 surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
444 DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
445 );
446 assert_eq!(
447 surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
448 DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
449 );
450 assert_eq!(
451 surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
452 DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
453 );
454 assert_eq!(
455 surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
456 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
457 );
458 assert_eq!(
459 surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
460 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
461 );
462 assert_eq!(
463 surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
464 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
465 );
466 assert_eq!(
467 surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
468 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
469 );
470 }
471}