ratatui_textarea/cursor.rs
1use crate::TextArea;
2use crate::widget::Viewport;
3use crate::word::{
4 find_word_inclusive_end_forward, find_word_start_backward, find_word_start_forward,
5};
6#[cfg(feature = "arbitrary")]
7use arbitrary::Arbitrary;
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10use std::cmp;
11
12/// Specify how to move the cursor.
13///
14/// This type is marked as `#[non_exhaustive]` since more variations may be supported in the future.
15#[non_exhaustive]
16#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
17#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
18#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
19pub enum CursorMove {
20 /// Move cursor forward by one character. When the cursor is at the end of line, it moves to the head of next line.
21 /// ```
22 /// use ratatui_textarea::{TextArea, CursorMove};
23 ///
24 /// let mut textarea = TextArea::from(["abc"]);
25 ///
26 /// textarea.move_cursor(CursorMove::Forward);
27 /// assert_eq!(textarea.cursor(), (0, 1));
28 /// textarea.move_cursor(CursorMove::Forward);
29 /// assert_eq!(textarea.cursor(), (0, 2));
30 /// ```
31 Forward,
32 /// Move cursor backward by one character. When the cursor is at the head of line, it moves to the end of previous
33 /// line.
34 /// ```
35 /// use ratatui_textarea::{TextArea, CursorMove};
36 ///
37 /// let mut textarea = TextArea::from(["abc"]);
38 ///
39 /// textarea.move_cursor(CursorMove::Forward);
40 /// textarea.move_cursor(CursorMove::Forward);
41 /// textarea.move_cursor(CursorMove::Back);
42 /// assert_eq!(textarea.cursor(), (0, 1));
43 /// ```
44 Back,
45 /// Move cursor up by one line.
46 /// ```
47 /// use ratatui_textarea::{TextArea, CursorMove};
48 ///
49 /// let mut textarea = TextArea::from(["a", "b", "c"]);
50 ///
51 /// textarea.move_cursor(CursorMove::Down);
52 /// textarea.move_cursor(CursorMove::Down);
53 /// textarea.move_cursor(CursorMove::Up);
54 /// assert_eq!(textarea.cursor(), (1, 0));
55 /// ```
56 Up,
57 /// Move cursor down by one line.
58 /// ```
59 /// use ratatui_textarea::{TextArea, CursorMove};
60 ///
61 /// let mut textarea = TextArea::from(["a", "b", "c"]);
62 ///
63 /// textarea.move_cursor(CursorMove::Down);
64 /// assert_eq!(textarea.cursor(), (1, 0));
65 /// textarea.move_cursor(CursorMove::Down);
66 /// assert_eq!(textarea.cursor(), (2, 0));
67 /// ```
68 Down,
69 /// Move cursor to the head of line. When the cursor is at the head of line, it moves to the end of previous line.
70 /// ```
71 /// use ratatui_textarea::{TextArea, CursorMove};
72 ///
73 /// let mut textarea = TextArea::from(["abc"]);
74 ///
75 /// textarea.move_cursor(CursorMove::Forward);
76 /// textarea.move_cursor(CursorMove::Forward);
77 /// textarea.move_cursor(CursorMove::Head);
78 /// assert_eq!(textarea.cursor(), (0, 0));
79 /// ```
80 Head,
81 /// Move cursor to the end of line. When the cursor is at the end of line, it moves to the head of next line.
82 /// ```
83 /// use ratatui_textarea::{TextArea, CursorMove};
84 ///
85 /// let mut textarea = TextArea::from(["abc"]);
86 ///
87 /// textarea.move_cursor(CursorMove::End);
88 /// assert_eq!(textarea.cursor(), (0, 3));
89 /// ```
90 End,
91 /// Move cursor to the top of lines.
92 /// ```
93 /// use ratatui_textarea::{TextArea, CursorMove};
94 ///
95 /// let mut textarea = TextArea::from(["a", "b", "c"]);
96 ///
97 /// textarea.move_cursor(CursorMove::Down);
98 /// textarea.move_cursor(CursorMove::Down);
99 /// textarea.move_cursor(CursorMove::Top);
100 /// assert_eq!(textarea.cursor(), (0, 0));
101 /// ```
102 Top,
103 /// Move cursor to the bottom of lines.
104 /// ```
105 /// use ratatui_textarea::{TextArea, CursorMove};
106 ///
107 /// let mut textarea = TextArea::from(["a", "b", "c"]);
108 ///
109 /// textarea.move_cursor(CursorMove::Bottom);
110 /// assert_eq!(textarea.cursor(), (2, 0));
111 /// ```
112 Bottom,
113 /// Move cursor forward by one word. Word boundary appears at spaces, punctuations, and others. For example
114 /// `fn foo(a)` consists of words `fn`, `foo`, `(`, `a`, `)`. When the cursor is at the end of line, it moves to the
115 /// head of next line.
116 /// ```
117 /// use ratatui_textarea::{TextArea, CursorMove};
118 ///
119 /// let mut textarea = TextArea::from(["aaa bbb ccc"]);
120 ///
121 /// textarea.move_cursor(CursorMove::WordForward);
122 /// assert_eq!(textarea.cursor(), (0, 4));
123 /// textarea.move_cursor(CursorMove::WordForward);
124 /// assert_eq!(textarea.cursor(), (0, 8));
125 /// ```
126 WordForward,
127 /// Move cursor forward to the next end of word. Word boundary appears at spaces, punctuations, and others. For example
128 /// `fn foo(a)` consists of words `fn`, `foo`, `(`, `a`, `)`. When the cursor is at the end of line, it moves to the
129 /// end of the first word of the next line. This is similar to the 'e' mapping of Vim in normal mode.
130 /// ```
131 /// use ratatui_textarea::{TextArea, CursorMove};
132 ///
133 /// let mut textarea = TextArea::from([
134 /// "aaa bbb [[[ccc]]]",
135 /// "",
136 /// " ddd",
137 /// ]);
138 ///
139 ///
140 /// textarea.move_cursor(CursorMove::WordEnd);
141 /// assert_eq!(textarea.cursor(), (0, 2)); // At the end of 'aaa'
142 /// textarea.move_cursor(CursorMove::WordEnd);
143 /// assert_eq!(textarea.cursor(), (0, 6)); // At the end of 'bbb'
144 /// textarea.move_cursor(CursorMove::WordEnd);
145 /// assert_eq!(textarea.cursor(), (0, 10)); // At the end of '[[['
146 /// textarea.move_cursor(CursorMove::WordEnd);
147 /// assert_eq!(textarea.cursor(), (0, 13)); // At the end of 'ccc'
148 /// textarea.move_cursor(CursorMove::WordEnd);
149 /// assert_eq!(textarea.cursor(), (0, 16)); // At the end of ']]]'
150 /// textarea.move_cursor(CursorMove::WordEnd);
151 /// assert_eq!(textarea.cursor(), (2, 3)); // At the end of 'ddd'
152 /// ```
153 WordEnd,
154 /// Move cursor backward by one word. Word boundary appears at spaces, punctuations, and others. For example
155 /// `fn foo(a)` consists of words `fn`, `foo`, `(`, `a`, `)`.When the cursor is at the head of line, it moves to
156 /// the end of previous line.
157 /// ```
158 /// use ratatui_textarea::{TextArea, CursorMove};
159 ///
160 /// let mut textarea = TextArea::from(["aaa bbb ccc"]);
161 ///
162 /// textarea.move_cursor(CursorMove::End);
163 /// textarea.move_cursor(CursorMove::WordBack);
164 /// assert_eq!(textarea.cursor(), (0, 8));
165 /// textarea.move_cursor(CursorMove::WordBack);
166 /// assert_eq!(textarea.cursor(), (0, 4));
167 /// textarea.move_cursor(CursorMove::WordBack);
168 /// assert_eq!(textarea.cursor(), (0, 0));
169 /// ```
170 WordBack,
171 /// Move cursor down by one paragraph. Paragraph is a chunk of non-empty lines. Cursor moves to the first line of paragraph.
172 /// ```
173 /// use ratatui_textarea::{TextArea, CursorMove};
174 ///
175 /// // aaa
176 /// //
177 /// // bbb
178 /// //
179 /// // ccc
180 /// // ddd
181 /// let mut textarea = TextArea::from(["aaa", "", "bbb", "", "ccc", "ddd"]);
182 ///
183 /// textarea.move_cursor(CursorMove::ParagraphForward);
184 /// assert_eq!(textarea.cursor(), (2, 0));
185 /// textarea.move_cursor(CursorMove::ParagraphForward);
186 /// assert_eq!(textarea.cursor(), (4, 0));
187 /// ```
188 ParagraphForward,
189 /// Move cursor up by one paragraph. Paragraph is a chunk of non-empty lines. Cursor moves to the first line of paragraph.
190 /// ```
191 /// use ratatui_textarea::{TextArea, CursorMove};
192 ///
193 /// // aaa
194 /// //
195 /// // bbb
196 /// //
197 /// // ccc
198 /// // ddd
199 /// let mut textarea = TextArea::from(["aaa", "", "bbb", "", "ccc", "ddd"]);
200 ///
201 /// textarea.move_cursor(CursorMove::Bottom);
202 /// textarea.move_cursor(CursorMove::ParagraphBack);
203 /// assert_eq!(textarea.cursor(), (4, 0));
204 /// textarea.move_cursor(CursorMove::ParagraphBack);
205 /// assert_eq!(textarea.cursor(), (2, 0));
206 /// textarea.move_cursor(CursorMove::ParagraphBack);
207 /// assert_eq!(textarea.cursor(), (0, 0));
208 /// ```
209 ParagraphBack,
210 /// Move cursor to (row, col) position. When the position points outside the text, the cursor position is made fit
211 /// within the text. Note that row and col are 0-based. (0, 0) means the first character of the first line.
212 ///
213 /// When there are 10 lines, jumping to row 15 moves the cursor to the last line (row is 9 in the case). When there
214 /// are 10 characters in the line, jumping to col 15 moves the cursor to end of the line (col is 10 in the case).
215 /// ```
216 /// use ratatui_textarea::{TextArea, CursorMove};
217 ///
218 /// let mut textarea = TextArea::from(["aaaa", "bbbb", "cccc"]);
219 ///
220 /// textarea.move_cursor(CursorMove::Jump(1, 2));
221 /// assert_eq!(textarea.cursor(), (1, 2));
222 ///
223 /// textarea.move_cursor(CursorMove::Jump(10, 10));
224 /// assert_eq!(textarea.cursor(), (2, 4));
225 /// ```
226 Jump(u16, u16),
227 /// Move cursor to keep it within the viewport. For example, when a viewport displays line 8 to line 16:
228 ///
229 /// - cursor at line 4 is moved to line 8
230 /// - cursor at line 20 is moved to line 16
231 /// - cursor at line 12 is not moved
232 ///
233 /// This is useful when you moved a cursor but you don't want to move the viewport.
234 /// ```
235 /// # use ratatui_core::buffer::Buffer;
236 /// # use ratatui_core::layout::Rect;
237 /// # use ratatui_core::widgets::Widget as _;
238 /// use ratatui_textarea::{TextArea, CursorMove};
239 ///
240 /// // Let's say terminal height is 8.
241 ///
242 /// // Create textarea with 20 lines "0", "1", "2", "3", ...
243 /// // The viewport is displaying from line 1 to line 8.
244 /// let mut textarea: TextArea = (0..20).into_iter().map(|i| i.to_string()).collect();
245 /// # // Call `render` at least once to populate terminal size
246 /// # let r = Rect { x: 0, y: 0, width: 24, height: 8 };
247 /// # let mut b = Buffer::empty(r.clone());
248 /// # textarea.render(r, &mut b);
249 ///
250 /// // Move cursor to the end of lines (line 20). It is outside the viewport (line 1 to line 8)
251 /// textarea.move_cursor(CursorMove::Bottom);
252 /// assert_eq!(textarea.cursor(), (19, 0));
253 ///
254 /// // Cursor is moved to line 8 to enter the viewport
255 /// textarea.move_cursor(CursorMove::InViewport);
256 /// assert_eq!(textarea.cursor(), (7, 0));
257 /// ```
258 InViewport,
259}
260
261#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
262#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
263#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
264pub struct DataCursor(pub usize, pub usize);
265
266impl DataCursor {
267 pub(crate) fn to_screen_cursor(self, ta: &TextArea) -> ScreenCursor {
268 ta.array_to_screen(self)
269 }
270}
271
272impl PartialEq<(usize, usize)> for DataCursor {
273 fn eq(&self, other: &(usize, usize)) -> bool {
274 self.0 == other.0 && self.1 == other.1
275 }
276}
277
278impl From<(usize, usize)> for DataCursor {
279 fn from((row, col): (usize, usize)) -> Self {
280 Self(row, col)
281 }
282}
283
284#[derive(Clone, Copy, Debug, PartialEq, Eq)]
285#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
286#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
287pub struct ScreenCursor {
288 pub row: usize,
289 pub col: usize,
290 pub char: Option<char>,
291 pub dc: Option<DataCursor>,
292}
293
294impl ScreenCursor {
295 pub(crate) fn to_array_cursor(self, ta: &TextArea) -> DataCursor {
296 ta.screen_to_array(self)
297 }
298}
299
300impl CursorMove {
301 pub(crate) fn next_cursor(
302 &self,
303 cursor: ScreenCursor,
304 ta: &TextArea,
305 viewport: &Viewport,
306 ) -> Option<ScreenCursor> {
307 use CursorMove::*;
308 let row = cursor.row;
309 let col = cursor.col;
310 let dc = cursor.dc.unwrap_or_else(|| cursor.to_array_cursor(ta));
311
312 match self {
313 Forward if col >= ta.screen_line_width(row) => (row + 1 < ta.screen_lines_count())
314 .then(|| ScreenCursor {
315 row: row + 1,
316 col: 0,
317 char: None,
318 dc: None,
319 }),
320 Forward => Some(ta.increment_screen_cursor(cursor)),
321 Back if col == 0 => {
322 let row = row.checked_sub(1)?;
323 Some(ScreenCursor {
324 row,
325 col: ta.screen_line_max_cursor_col(row),
326 char: None,
327 dc: None,
328 })
329 }
330 Up => {
331 let row = row.checked_sub(1)?;
332 Some(ScreenCursor {
333 row,
334 col: cmp::min(col, ta.screen_line_max_cursor_col(row)),
335 char: None,
336 dc: None,
337 })
338 }
339 Down => {
340 if row + 1 >= ta.screen_lines_count() {
341 None
342 } else {
343 Some(ScreenCursor {
344 row: row + 1,
345 col: cmp::min(col, ta.screen_line_max_cursor_col(row + 1)),
346 char: None,
347 dc: None,
348 })
349 }
350 }
351 Back => Some(ta.decrement_screen_cursor(cursor)),
352 Head => Some(DataCursor(dc.0, 0).to_screen_cursor(ta)),
353 End => Some(DataCursor(dc.0, ta.lines[dc.0].chars().count()).to_screen_cursor(ta)),
354 Top => Some(
355 DataCursor(0, cmp::min(dc.1, ta.lines[0].chars().count())).to_screen_cursor(ta),
356 ),
357 Bottom => {
358 let row = ta.lines.len() - 1;
359 let col = cmp::min(dc.1, ta.lines[row].chars().count());
360 Some(DataCursor(row, col).to_screen_cursor(ta))
361 }
362 WordEnd => {
363 // `+ 1` for not accepting the current cursor position
364 if let Some(col) = find_word_inclusive_end_forward(&ta.lines[dc.0], dc.1 + 1) {
365 Some(DataCursor(dc.0, col).to_screen_cursor(ta))
366 } else {
367 let mut row = dc.0;
368 loop {
369 if row == ta.lines.len() - 1 {
370 break Some(
371 DataCursor(row, ta.lines[row].chars().count()).to_screen_cursor(ta),
372 );
373 }
374 row += 1;
375 if let Some(col) = find_word_inclusive_end_forward(&ta.lines[row], 0) {
376 break Some(DataCursor(row, col).to_screen_cursor(ta));
377 }
378 }
379 }
380 }
381 WordForward => {
382 if let Some(col) = find_word_start_forward(&ta.lines[dc.0], dc.1) {
383 Some(DataCursor(dc.0, col).to_screen_cursor(ta))
384 } else if dc.0 + 1 < ta.lines.len() {
385 Some(DataCursor(dc.0 + 1, 0).to_screen_cursor(ta))
386 } else {
387 Some(DataCursor(dc.0, ta.lines[dc.0].chars().count()).to_screen_cursor(ta))
388 }
389 }
390 WordBack => {
391 if let Some(col) = find_word_start_backward(&ta.lines[dc.0], dc.1) {
392 Some(DataCursor(dc.0, col).to_screen_cursor(ta))
393 } else if dc.0 > 0 {
394 let row = dc.0 - 1;
395 Some(DataCursor(row, ta.lines[row].chars().count()).to_screen_cursor(ta))
396 } else {
397 Some(DataCursor(dc.0, 0).to_screen_cursor(ta))
398 }
399 }
400 ParagraphForward => {
401 let mut prev_is_empty = ta.lines[dc.0].is_empty();
402 for row in dc.0 + 1..ta.lines.len() {
403 let line = &ta.lines[row];
404 let is_empty = line.is_empty();
405 if !is_empty && prev_is_empty {
406 let col = cmp::min(dc.1, line.chars().count());
407 return Some(DataCursor(row, col).to_screen_cursor(ta));
408 }
409 prev_is_empty = is_empty;
410 }
411 let row = ta.lines.len() - 1;
412 let col = cmp::min(dc.1, ta.lines[row].chars().count());
413 Some(DataCursor(row, col).to_screen_cursor(ta))
414 }
415 ParagraphBack => {
416 let row = dc.0.checked_sub(1)?;
417 let mut prev_is_empty = ta.lines[row].is_empty();
418 for row in (0..row).rev() {
419 let is_empty = ta.lines[row].is_empty();
420 if is_empty && !prev_is_empty {
421 let target = row + 1;
422 let col = cmp::min(dc.1, ta.lines[target].chars().count());
423 return Some(DataCursor(target, col).to_screen_cursor(ta));
424 }
425 prev_is_empty = is_empty;
426 }
427 let col = cmp::min(dc.1, ta.lines[0].chars().count());
428 Some(DataCursor(0, col).to_screen_cursor(ta))
429 }
430 Jump(row, col) => {
431 let row = cmp::min(*row as usize, ta.lines.len() - 1);
432 let col = cmp::min(*col as usize, ta.lines[row].chars().count());
433 Some(DataCursor(row, col).to_screen_cursor(ta))
434 }
435 InViewport => {
436 let (row_top, col_top, row_bottom, col_bottom) = viewport.position();
437
438 let row = row.clamp(row_top as usize, row_bottom as usize);
439 let row = cmp::min(row, ta.screen_lines_count() - 1);
440 let col = col.clamp(col_top as usize, col_bottom as usize);
441 let col = cmp::min(col, ta.screen_line_max_cursor_col(row));
442 Some(ScreenCursor {
443 row,
444 col,
445 char: None,
446 dc: None,
447 })
448 }
449 }
450 }
451}
452
453#[cfg(test)]
454mod tests {
455
456 // Separate tests for tui-rs support
457 #[test]
458 fn in_viewport() {
459 use crate::{CursorMove, TextArea};
460 use ratatui_core::widgets::Widget;
461 use ratatui_core::{buffer::Buffer, layout::Rect};
462
463 let mut textarea: TextArea = (0..20).map(|i| i.to_string()).collect();
464 let r = Rect {
465 x: 0,
466 y: 0,
467 width: 24,
468 height: 8,
469 };
470 let mut b = Buffer::empty(r);
471 textarea.render(r, &mut b);
472
473 textarea.move_cursor(CursorMove::Bottom);
474 assert_eq!(textarea.cursor(), (19, 0));
475
476 textarea.move_cursor(CursorMove::InViewport);
477 assert_eq!(textarea.cursor(), (7, 0));
478 }
479}