1use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
2use anyhow::Result;
3use language::multi_buffer::ToPoint;
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::display_map::DisplayMap;
248 use language::MultiBuffer;
249
250 #[gpui::test]
251 fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
252 let tab_size = 4;
253 let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
254 let font_id = cx
255 .font_cache()
256 .select_font(family_id, &Default::default())
257 .unwrap();
258 let font_size = 14.0;
259
260 let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx);
261 let display_map =
262 cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
263 let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
264 assert_eq!(
265 prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)),
266 DisplayPoint::new(0, 7)
267 );
268 assert_eq!(
269 prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
270 DisplayPoint::new(0, 2)
271 );
272 assert_eq!(
273 prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
274 DisplayPoint::new(0, 2)
275 );
276 assert_eq!(
277 prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
278 DisplayPoint::new(0, 0)
279 );
280 assert_eq!(
281 prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
282 DisplayPoint::new(0, 0)
283 );
284
285 assert_eq!(
286 next_word_boundary(&snapshot, DisplayPoint::new(0, 0)),
287 DisplayPoint::new(0, 1)
288 );
289 assert_eq!(
290 next_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
291 DisplayPoint::new(0, 6)
292 );
293 assert_eq!(
294 next_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
295 DisplayPoint::new(0, 6)
296 );
297 assert_eq!(
298 next_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
299 DisplayPoint::new(0, 12)
300 );
301 assert_eq!(
302 next_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
303 DisplayPoint::new(0, 12)
304 );
305 }
306
307 #[gpui::test]
308 fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
309 let tab_size = 4;
310 let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
311 let font_id = cx
312 .font_cache()
313 .select_font(family_id, &Default::default())
314 .unwrap();
315 let font_size = 14.0;
316 let buffer = MultiBuffer::build_simple("lorem ipsum dolor\n sit", cx);
317 let display_map =
318 cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
319 let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
320
321 assert_eq!(
322 surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
323 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
324 );
325 assert_eq!(
326 surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
327 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
328 );
329 assert_eq!(
330 surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
331 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
332 );
333 assert_eq!(
334 surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
335 DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
336 );
337 assert_eq!(
338 surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
339 DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
340 );
341 assert_eq!(
342 surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
343 DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
344 );
345 assert_eq!(
346 surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
347 DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14)
348 );
349 assert_eq!(
350 surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
351 DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
352 );
353 assert_eq!(
354 surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
355 DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
356 );
357 assert_eq!(
358 surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
359 DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
360 );
361 assert_eq!(
362 surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
363 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4)
364 );
365 assert_eq!(
366 surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
367 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4)
368 );
369 assert_eq!(
370 surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
371 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7)
372 );
373 assert_eq!(
374 surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
375 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7)
376 );
377 }
378}