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