1use ropey::Rope;
2
3use crate::{
4 graphemes::{RopeExt, RopeGraphemes},
5 Cursor,
6};
7
8#[derive(Debug, Copy, Clone, PartialEq, Eq)]
10pub enum Direction {
11 Forward,
12 Backward,
13}
14
15#[inline]
18pub fn move_horizontally(text: &Rope, cursor: &mut Cursor, direction: Direction, count: usize) {
19 let grapheme_start = match direction {
20 Direction::Forward => text.next_grapheme_boundary_n(cursor.range.start, count),
21 Direction::Backward => text.prev_grapheme_boundary_n(cursor.range.start, count),
22 };
23 cursor.range = grapheme_start..text.next_grapheme_boundary(grapheme_start);
24 cursor.visual_horizontal_offset = None;
25}
26
27#[inline]
29pub fn move_vertically(
30 text: &Rope,
31 cursor: &mut Cursor,
32 tab_width: usize,
33 direction: Direction,
34 count: usize,
35) {
36 let max_line_index = text.len_lines().saturating_sub(1);
38
39 let current_line_index = text.char_to_line(cursor.range.start);
41
42 let new_line_index = match direction {
44 Direction::Forward if current_line_index < max_line_index => {
47 std::cmp::min(current_line_index + count, max_line_index)
48 }
49 Direction::Forward if current_line_index == max_line_index => {
52 move_to_end_of_line(text, cursor);
53 return;
54 }
55 Direction::Backward if current_line_index > 0 => current_line_index.saturating_sub(count),
58 _ => {
60 return;
61 }
62 };
63
64 let current_visual_x = cursor.visual_horizontal_offset.get_or_insert_with(|| {
65 let current_line_start = text.line_to_char(current_line_index);
66 let line_to_cursor = text.slice(current_line_start..cursor.range.start);
67 crate::graphemes::width(tab_width, &line_to_cursor)
68 });
69
70 let new_line = text.line(new_line_index);
71 let mut graphemes = RopeGraphemes::new(&new_line);
72 let mut new_visual_x = 0;
73 let mut char_offset = text.line_to_char(new_line_index);
74 for grapheme in &mut graphemes {
75 let width = crate::graphemes::width(tab_width, &grapheme);
76 if new_visual_x + width > *current_visual_x || grapheme.slice == "\n" {
77 break;
78 }
79 char_offset += grapheme.slice.len_chars();
80 new_visual_x += width;
81 }
82
83 cursor.range = char_offset..text.next_grapheme_boundary(char_offset);
84}
85
86#[inline]
88pub fn move_word(text: &Rope, cursor: &mut Cursor, direction: Direction, count: usize) {
89 match direction {
90 Direction::Forward => {
91 for _ in 0..count {
92 move_forward_word(text, cursor);
93 }
94 }
95 Direction::Backward => {
96 for _ in 0..count {
97 move_backward_word(text, cursor);
98 }
99 }
100 }
101}
102
103#[inline]
105pub fn move_forward_word(text: &Rope, cursor: &mut Cursor) {
106 let first_word_character =
107 skip_while_forward(text, cursor.range.start, |c| !is_word_character(c))
108 .unwrap_or_else(|| text.len_chars());
109 let grapheme_start = skip_while_forward(text, first_word_character, is_word_character)
110 .unwrap_or_else(|| text.len_chars());
111 cursor.range = grapheme_start..text.next_grapheme_boundary(grapheme_start);
112 cursor.visual_horizontal_offset = None;
113}
114
115#[inline]
117pub fn move_backward_word(text: &Rope, cursor: &mut Cursor) {
118 let first_word_character =
119 skip_while_backward(text, cursor.range.start, |c| !is_word_character(c)).unwrap_or(0);
120 let grapheme_start =
121 skip_while_backward(text, first_word_character, is_word_character).unwrap_or(0);
122 cursor.range = grapheme_start..text.next_grapheme_boundary(grapheme_start);
123 cursor.visual_horizontal_offset = None;
124}
125
126#[inline]
128pub fn move_paragraph(text: &Rope, cursor: &mut Cursor, direction: Direction, count: usize) {
129 match direction {
130 Direction::Forward => {
131 for _ in 0..count {
132 move_forward_paragraph(text, cursor);
133 }
134 }
135 Direction::Backward => {
136 for _ in 0..count {
137 move_backward_paragraph(text, cursor);
138 }
139 }
140 }
141}
142
143#[inline]
145pub fn move_forward_paragraph(text: &Rope, cursor: &mut Cursor) {
146 let current_line = text.char_to_line(cursor.range.start);
147 let lines = text.lines_at(current_line + 1);
148
149 let start = lines
150 .enumerate()
151 .find_map(|(index, line)| {
152 line.chars()
153 .all(char::is_whitespace)
154 .then(|| text.line_to_char(current_line + index + 1))
155 })
156 .unwrap_or_else(|| text.len_chars());
157 cursor.range = start..text.next_grapheme_boundary(start);
158 cursor.visual_horizontal_offset = None;
159}
160
161#[inline]
163pub fn move_backward_paragraph(text: &Rope, cursor: &mut Cursor) {
164 let current_line = text.char_to_line(cursor.range.start);
165 let mut lines = text.lines_at(current_line.saturating_sub(1));
166 lines.reverse();
167
168 let start = lines
169 .enumerate()
170 .find_map(|(index, line)| {
171 line.chars()
172 .all(char::is_whitespace)
173 .then(|| text.line_to_char(current_line.saturating_sub(index + 1)))
174 })
175 .unwrap_or(0);
176 cursor.range = start..text.next_grapheme_boundary(start);
177 cursor.visual_horizontal_offset = None;
178}
179
180#[inline]
182pub fn move_to_start_of_line(text: &Rope, cursor: &mut Cursor) {
183 let line_start = text.line_to_char(text.char_to_line(cursor.range.start));
184 cursor.range = line_start..text.next_grapheme_boundary(line_start);
185 cursor.visual_horizontal_offset = None;
186}
187
188#[inline]
190pub fn move_to_end_of_line(text: &Rope, cursor: &mut Cursor) {
191 let line_index = text.char_to_line(cursor.range.start);
192 let line = text.line(line_index);
193 let line_start = text.line_to_char(line_index);
194 let line_length = line.len_chars();
195 let range_end = line_start + line_length;
196
197 let range_start = if line_length == 0 || line.char(line_length - 1) != '\n' {
198 range_end
201 } else {
202 range_end.saturating_sub(1)
204 };
205
206 cursor.range = range_start..range_end;
207 cursor.visual_horizontal_offset = None;
208}
209
210#[inline]
212pub fn move_to_start_of_buffer(text: &Rope, cursor: &mut Cursor) {
213 cursor.range = 0..text.next_grapheme_boundary(0);
214 cursor.visual_horizontal_offset = None;
215}
216
217#[inline]
219pub fn move_to_end_of_buffer(text: &Rope, cursor: &mut Cursor) {
220 let length = text.len_chars();
221 cursor.range = length..length;
222 cursor.visual_horizontal_offset = None;
223}
224
225#[inline]
226fn skip_while_forward(
227 text: &Rope,
228 position: usize,
229 predicate: impl Fn(char) -> bool,
230) -> Option<usize> {
231 text.chars_at(position)
232 .enumerate()
233 .find_map(|(index, character)| (!predicate(character)).then(|| position + index))
234}
235
236#[inline]
237fn skip_while_backward(
238 text: &Rope,
239 position: usize,
240 predicate: impl Fn(char) -> bool,
241) -> Option<usize> {
242 let mut chars = text.chars_at(position);
243 chars.reverse();
244 chars.enumerate().find_map(|(index, character)| {
245 (!predicate(character)).then(|| position.saturating_sub(index))
246 })
247}
248
249#[inline]
250fn is_word_character(character: char) -> bool {
251 character == '_' || (!character.is_whitespace() && !character.is_ascii_punctuation())
252}
253
254#[cfg(test)]
255mod tests {
256 use super::{super::RopeCursorExt, *};
257 use ropey::Rope;
258
259 impl Cursor {
261 fn move_right(&mut self, text: &Rope) {
262 move_horizontally(text, self, Direction::Forward, 1)
263 }
264
265 fn move_left(&mut self, text: &Rope) {
266 move_horizontally(text, self, Direction::Backward, 1)
267 }
268 }
269
270 fn text_with_cursor(text: impl Into<Rope>) -> (Rope, Cursor) {
271 (text.into(), Cursor::new())
272 }
273
274 #[test]
275 fn move_right_on_empty_text() {
276 let (text, mut cursor) = text_with_cursor("");
277 cursor.move_right(&text);
278 assert_eq!(cursor, Cursor::new());
279
280 let (text, mut cursor) = text_with_cursor("\n");
281 cursor.move_right(&text);
282 assert_eq!(cursor, Cursor::with_range(1..1));
283 }
284
285 #[test]
286 fn move_right_at_the_end() {
287 let (text, mut cursor) = text_with_cursor(TEXT);
288 move_to_end_of_buffer(&text, &mut cursor);
289 let cursor_at_end = cursor.clone();
290 cursor.move_right(&text);
291 assert_eq!(cursor_at_end, cursor);
292 assert_eq!(
293 cursor,
294 Cursor::with_range(text.len_chars()..text.len_chars())
295 );
296 }
297
298 #[test]
299 fn move_left_at_the_begining() {
300 let text = Rope::from(TEXT);
301 let mut cursor = Cursor::new();
302 cursor.move_left(&text);
303 assert_eq!(Cursor::with_range(0..1), cursor);
304 }
305
306 #[test]
307 fn move_wide_grapheme() {
308 let text = Rope::from(MULTI_CHAR_EMOJI);
309 let mut cursor = Cursor::new();
310 move_to_start_of_buffer(&text, &mut cursor);
311 assert_eq!(0..text.len_chars(), cursor.range);
312 }
313
314 #[test]
315 fn move_by_zero_positions() {
316 let (text, mut cursor) = text_with_cursor("Hello\n");
317 move_horizontally(&text, &mut cursor, Direction::Backward, 0);
318 assert_eq!(Cursor::with_range(0..1), cursor);
319 move_horizontally(&text, &mut cursor, Direction::Forward, 0);
320 assert_eq!(Cursor::with_range(0..1), cursor);
321
322 cursor.range = 1..2;
323 move_horizontally(&text, &mut cursor, Direction::Backward, 0);
324 assert_eq!(cursor.range, 1..2);
325 move_horizontally(&text, &mut cursor, Direction::Forward, 0);
326 assert_eq!(cursor.range, 1..2);
327 }
328
329 #[test]
330 fn move_backward_on_empty_text() {
331 let (text, mut cursor) = text_with_cursor("");
332 move_horizontally(&text, &mut cursor, Direction::Backward, 1);
333 assert_eq!(Cursor::new(), cursor);
334 }
335
336 #[test]
337 fn move_backward_at_the_begining() {
338 let (text, mut cursor) = text_with_cursor("The flowers were blooming.\n");
339 move_horizontally(&text, &mut cursor, Direction::Backward, 1);
340 assert_eq!(cursor, Cursor::with_range(0..1),);
341 assert_eq!(text.slice_cursor(&cursor), "T");
342 }
343
344 const TEXT: &str = r#"
345Basic Latin
346 ! " # $ % & ' ( ) *+,-./012ABCDEFGHI` a m t u v z { | } ~
347CJK
348 ๏ค ๏ค ๏ค โ
ง
349"#;
350 const MULTI_CHAR_EMOJI: &str = r#"๐จโ๐จโ๐งโ๐ง"#;
351}