1use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
2use crate::ToPoint;
3use anyhow::Result;
4use std::{cmp, ops::Range};
5
6pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
7 if point.column() > 0 {
8 *point.column_mut() -= 1;
9 } else if point.row() > 0 {
10 *point.row_mut() -= 1;
11 *point.column_mut() = map.line_len(point.row());
12 }
13 Ok(map.clip_point(point, Bias::Left))
14}
15
16pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
17 let max_column = map.line_len(point.row());
18 if point.column() < max_column {
19 *point.column_mut() += 1;
20 } else if point.row() < map.max_point().row() {
21 *point.row_mut() += 1;
22 *point.column_mut() = 0;
23 }
24 Ok(map.clip_point(point, Bias::Right))
25}
26
27pub fn up(
28 map: &DisplaySnapshot,
29 mut point: DisplayPoint,
30 goal: SelectionGoal,
31) -> Result<(DisplayPoint, SelectionGoal)> {
32 let goal_column = if let SelectionGoal::Column(column) = goal {
33 column
34 } else {
35 map.column_to_chars(point.row(), point.column())
36 };
37
38 loop {
39 if point.row() > 0 {
40 *point.row_mut() -= 1;
41 *point.column_mut() = map.column_from_chars(point.row(), goal_column);
42 if !map.is_block_line(point.row()) {
43 break;
44 }
45 } else {
46 point = DisplayPoint::new(0, 0);
47 break;
48 }
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 mut point: DisplayPoint,
66 goal: SelectionGoal,
67) -> Result<(DisplayPoint, SelectionGoal)> {
68 let max_point = map.max_point();
69 let goal_column = if let SelectionGoal::Column(column) = goal {
70 column
71 } else {
72 map.column_to_chars(point.row(), point.column())
73 };
74
75 loop {
76 if point.row() < max_point.row() {
77 *point.row_mut() += 1;
78 *point.column_mut() = map.column_from_chars(point.row(), goal_column);
79 if !map.is_block_line(point.row()) {
80 break;
81 }
82 } else {
83 point = max_point;
84 break;
85 }
86 }
87
88 let clip_bias = if point.column() == map.line_len(point.row()) {
89 Bias::Left
90 } else {
91 Bias::Right
92 };
93
94 Ok((
95 map.clip_point(point, clip_bias),
96 SelectionGoal::Column(goal_column),
97 ))
98}
99
100pub fn line_beginning(
101 map: &DisplaySnapshot,
102 point: DisplayPoint,
103 toggle_indent: bool,
104) -> DisplayPoint {
105 let (indent, is_blank) = map.line_indent(point.row());
106 if toggle_indent && !is_blank && point.column() != indent {
107 DisplayPoint::new(point.row(), indent)
108 } else {
109 DisplayPoint::new(point.row(), 0)
110 }
111}
112
113pub fn line_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
114 let line_end = DisplayPoint::new(point.row(), map.line_len(point.row()));
115 map.clip_point(line_end, Bias::Left)
116}
117
118pub fn prev_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
119 let mut line_start = 0;
120 if point.row() > 0 {
121 if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
122 line_start = indent;
123 }
124 }
125
126 if point.column() == line_start {
127 if point.row() == 0 {
128 return DisplayPoint::new(0, 0);
129 } else {
130 let row = point.row() - 1;
131 point = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left);
132 }
133 }
134
135 let mut boundary = DisplayPoint::new(point.row(), 0);
136 let mut column = 0;
137 let mut prev_char_kind = CharKind::Newline;
138 for c in map.chars_at(DisplayPoint::new(point.row(), 0)) {
139 if column >= point.column() {
140 break;
141 }
142
143 let char_kind = char_kind(c);
144 if char_kind != prev_char_kind
145 && char_kind != CharKind::Whitespace
146 && char_kind != CharKind::Newline
147 {
148 *boundary.column_mut() = column;
149 }
150
151 prev_char_kind = char_kind;
152 column += c.len_utf8() as u32;
153 }
154 boundary
155}
156
157pub fn next_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
158 let mut prev_char_kind = None;
159 for c in map.chars_at(point) {
160 let char_kind = char_kind(c);
161 if let Some(prev_char_kind) = prev_char_kind {
162 if c == '\n' {
163 break;
164 }
165 if prev_char_kind != char_kind
166 && prev_char_kind != CharKind::Whitespace
167 && prev_char_kind != CharKind::Newline
168 {
169 break;
170 }
171 }
172
173 if c == '\n' {
174 *point.row_mut() += 1;
175 *point.column_mut() = 0;
176 } else {
177 *point.column_mut() += c.len_utf8() as u32;
178 }
179 prev_char_kind = Some(char_kind);
180 }
181 point
182}
183
184pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
185 let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
186 let text = &map.buffer_snapshot;
187 let next_char_kind = text.chars_at(ix).next().map(char_kind);
188 let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind);
189 prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
190}
191
192pub fn surrounding_word(map: &DisplaySnapshot, point: DisplayPoint) -> Range<DisplayPoint> {
193 let mut start = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
194 let mut end = start;
195
196 let text = &map.buffer_snapshot;
197 let mut next_chars = text.chars_at(start).peekable();
198 let mut prev_chars = text.reversed_chars_at(start).peekable();
199 let word_kind = cmp::max(
200 prev_chars.peek().copied().map(char_kind),
201 next_chars.peek().copied().map(char_kind),
202 );
203
204 for ch in prev_chars {
205 if Some(char_kind(ch)) == word_kind {
206 start -= ch.len_utf8();
207 } else {
208 break;
209 }
210 }
211
212 for ch in next_chars {
213 if Some(char_kind(ch)) == word_kind {
214 end += ch.len_utf8();
215 } else {
216 break;
217 }
218 }
219
220 start.to_point(&map.buffer_snapshot).to_display_point(map)
221 ..end.to_point(&map.buffer_snapshot).to_display_point(map)
222}
223
224#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
225enum CharKind {
226 Newline,
227 Punctuation,
228 Whitespace,
229 Word,
230}
231
232fn char_kind(c: char) -> CharKind {
233 if c == '\n' {
234 CharKind::Newline
235 } else if c.is_whitespace() {
236 CharKind::Whitespace
237 } else if c.is_alphanumeric() || c == '_' {
238 CharKind::Word
239 } else {
240 CharKind::Punctuation
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use crate::{DisplayMap, MultiBuffer};
248
249 #[gpui::test]
250 fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
251 let tab_size = 4;
252 let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
253 let font_id = cx
254 .font_cache()
255 .select_font(family_id, &Default::default())
256 .unwrap();
257 let font_size = 14.0;
258
259 let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx);
260 let display_map =
261 cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
262 let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
263 assert_eq!(
264 prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)),
265 DisplayPoint::new(0, 7)
266 );
267 assert_eq!(
268 prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
269 DisplayPoint::new(0, 2)
270 );
271 assert_eq!(
272 prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
273 DisplayPoint::new(0, 2)
274 );
275 assert_eq!(
276 prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
277 DisplayPoint::new(0, 0)
278 );
279 assert_eq!(
280 prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
281 DisplayPoint::new(0, 0)
282 );
283
284 assert_eq!(
285 next_word_boundary(&snapshot, DisplayPoint::new(0, 0)),
286 DisplayPoint::new(0, 1)
287 );
288 assert_eq!(
289 next_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
290 DisplayPoint::new(0, 6)
291 );
292 assert_eq!(
293 next_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
294 DisplayPoint::new(0, 6)
295 );
296 assert_eq!(
297 next_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
298 DisplayPoint::new(0, 12)
299 );
300 assert_eq!(
301 next_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
302 DisplayPoint::new(0, 12)
303 );
304 }
305
306 #[gpui::test]
307 fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
308 let tab_size = 4;
309 let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
310 let font_id = cx
311 .font_cache()
312 .select_font(family_id, &Default::default())
313 .unwrap();
314 let font_size = 14.0;
315 let buffer = MultiBuffer::build_simple("lorem ipsum dolor\n sit", cx);
316 let display_map =
317 cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
318 let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
319
320 assert_eq!(
321 surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
322 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
323 );
324 assert_eq!(
325 surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
326 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
327 );
328 assert_eq!(
329 surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
330 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
331 );
332 assert_eq!(
333 surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
334 DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
335 );
336 assert_eq!(
337 surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
338 DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
339 );
340 assert_eq!(
341 surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
342 DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
343 );
344 assert_eq!(
345 surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
346 DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14)
347 );
348 assert_eq!(
349 surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
350 DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
351 );
352 assert_eq!(
353 surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
354 DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
355 );
356 assert_eq!(
357 surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
358 DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
359 );
360 assert_eq!(
361 surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
362 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4)
363 );
364 assert_eq!(
365 surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
366 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4)
367 );
368 assert_eq!(
369 surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
370 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7)
371 );
372 assert_eq!(
373 surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
374 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7)
375 );
376 }
377}