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::{Buffer, DisplayMap, MultiBuffer};
229 use language::Point;
230
231 #[gpui::test]
232 fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
233 let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
234 let font_id = cx
235 .font_cache()
236 .select_font(family_id, &Default::default())
237 .unwrap();
238
239 let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx));
240 let multibuffer = cx.add_model(|cx| {
241 let mut multibuffer = MultiBuffer::new(0);
242 multibuffer.push_excerpt(buffer.clone(), Point::new(0, 0)..Point::new(1, 4), cx);
243 multibuffer.push_excerpt(buffer.clone(), Point::new(2, 0)..Point::new(3, 2), cx);
244 multibuffer
245 });
246
247 let display_map =
248 cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, 2, 2, cx));
249
250 let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
251 assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
252
253 // Can't move up into the first excerpt's header
254 assert_eq!(
255 up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)).unwrap(),
256 (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
257 );
258 assert_eq!(
259 up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None).unwrap(),
260 (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
261 );
262
263 // Move up and down within first excerpt
264 assert_eq!(
265 up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)).unwrap(),
266 (DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
267 );
268 assert_eq!(
269 down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)).unwrap(),
270 (DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
271 );
272
273 // Move up and down across second excerpt's header
274 assert_eq!(
275 up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)).unwrap(),
276 (DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
277 );
278 assert_eq!(
279 down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)).unwrap(),
280 (DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
281 );
282
283 // Can't move down off the end
284 assert_eq!(
285 down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)).unwrap(),
286 (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
287 );
288 assert_eq!(
289 down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)).unwrap(),
290 (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
291 );
292 }
293
294 #[gpui::test]
295 fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
296 let tab_size = 4;
297 let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
298 let font_id = cx
299 .font_cache()
300 .select_font(family_id, &Default::default())
301 .unwrap();
302 let font_size = 14.0;
303
304 let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx);
305 let display_map = cx
306 .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
307 let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
308 assert_eq!(
309 prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)),
310 DisplayPoint::new(0, 7)
311 );
312 assert_eq!(
313 prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
314 DisplayPoint::new(0, 2)
315 );
316 assert_eq!(
317 prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
318 DisplayPoint::new(0, 2)
319 );
320 assert_eq!(
321 prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
322 DisplayPoint::new(0, 0)
323 );
324 assert_eq!(
325 prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
326 DisplayPoint::new(0, 0)
327 );
328
329 assert_eq!(
330 next_word_boundary(&snapshot, DisplayPoint::new(0, 0)),
331 DisplayPoint::new(0, 1)
332 );
333 assert_eq!(
334 next_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
335 DisplayPoint::new(0, 6)
336 );
337 assert_eq!(
338 next_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
339 DisplayPoint::new(0, 6)
340 );
341 assert_eq!(
342 next_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
343 DisplayPoint::new(0, 12)
344 );
345 assert_eq!(
346 next_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
347 DisplayPoint::new(0, 12)
348 );
349 }
350
351 #[gpui::test]
352 fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
353 let tab_size = 4;
354 let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
355 let font_id = cx
356 .font_cache()
357 .select_font(family_id, &Default::default())
358 .unwrap();
359 let font_size = 14.0;
360 let buffer = MultiBuffer::build_simple("lorem ipsum dolor\n sit", cx);
361 let display_map = cx
362 .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
363 let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
364
365 assert_eq!(
366 surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
367 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
368 );
369 assert_eq!(
370 surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
371 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
372 );
373 assert_eq!(
374 surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
375 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
376 );
377 assert_eq!(
378 surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
379 DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
380 );
381 assert_eq!(
382 surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
383 DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
384 );
385 assert_eq!(
386 surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
387 DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
388 );
389 assert_eq!(
390 surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
391 DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14),
392 );
393 assert_eq!(
394 surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
395 DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
396 );
397 assert_eq!(
398 surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
399 DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
400 );
401 assert_eq!(
402 surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
403 DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
404 );
405 assert_eq!(
406 surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
407 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
408 );
409 assert_eq!(
410 surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
411 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
412 );
413 assert_eq!(
414 surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
415 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
416 );
417 assert_eq!(
418 surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
419 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
420 );
421 }
422}