From a3b6dd152dcbc7f241fbb13d6a0e5d1cbb977ffb Mon Sep 17 00:00:00 2001 From: rhysd Date: Thu, 1 Aug 2024 01:39:44 +0900 Subject: [PATCH] fix `WordEnd` does not consider going across multiple empty lines --- src/cursor.rs | 45 +++++++++++++++++++++++++++------------------ src/textarea.rs | 4 ++-- src/word.rs | 27 +++++++++++---------------- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/src/cursor.rs b/src/cursor.rs index 2ed576f..4e51449 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -1,5 +1,7 @@ use crate::widget::Viewport; -use crate::word::{find_word_end_next, find_word_start_backward, find_word_start_forward}; +use crate::word::{ + find_word_inclusive_end_forward, find_word_start_backward, find_word_start_forward, +}; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(feature = "serde")] @@ -123,25 +125,29 @@ pub enum CursorMove { WordForward, /// Move cursor forward to the next end of word. Word boundary appears at spaces, punctuations, and others. For example /// `fn foo(a)` consists of words `fn`, `foo`, `(`, `a`, `)`. When the cursor is at the end of line, it moves to the - /// end of the first word of the next line. + /// end of the first word of the next line. This is similar to the 'e' mapping of Vim in normal mode. /// ``` /// use tui_textarea::{TextArea, CursorMove}; /// - /// let mut textarea = TextArea::from(["aaa bbb (c)", " dd"]); + /// let mut textarea = TextArea::from([ + /// "aaa bbb [[[ccc]]]", + /// "", + /// " ddd", + /// ]); /// /// /// textarea.move_cursor(CursorMove::WordEnd); - /// assert_eq!(textarea.cursor(), (0, 2)); + /// assert_eq!(textarea.cursor(), (0, 2)); // At the end of 'aaa' /// textarea.move_cursor(CursorMove::WordEnd); - /// assert_eq!(textarea.cursor(), (0, 6)); + /// assert_eq!(textarea.cursor(), (0, 6)); // At the end of 'bbb' /// textarea.move_cursor(CursorMove::WordEnd); - /// assert_eq!(textarea.cursor(), (0, 8)); + /// assert_eq!(textarea.cursor(), (0, 10)); // At the end of '[[[' /// textarea.move_cursor(CursorMove::WordEnd); - /// assert_eq!(textarea.cursor(), (0, 9)); + /// assert_eq!(textarea.cursor(), (0, 13)); // At the end of 'ccc' /// textarea.move_cursor(CursorMove::WordEnd); - /// assert_eq!(textarea.cursor(), (0, 10)); + /// assert_eq!(textarea.cursor(), (0, 16)); // At the end of ']]]' /// textarea.move_cursor(CursorMove::WordEnd); - /// assert_eq!(textarea.cursor(), (1, 2)); + /// assert_eq!(textarea.cursor(), (2, 3)); // At the end of 'ddd' /// ``` WordEnd, /// Move cursor backward by one word. Word boundary appears at spaces, punctuations, and others. For example @@ -287,17 +293,20 @@ impl CursorMove { Some((row, fit_col(col, &lines[row]))) } WordEnd => { - if let Some(col) = find_word_end_next(&lines[row], col) { + // `+ 1` for not accepting the current cursor position + if let Some(col) = find_word_inclusive_end_forward(&lines[row], col + 1) { Some((row, col)) - } else if let Some(col) = (row + 1 < lines.len()) - .then(|| find_word_end_next(&lines[row + 1], 0)) - .unwrap_or_default() - { - Some((row + 1, col)) - } else if row + 1 < lines.len() { - Some((row + 1, lines[row + 1].chars().count().saturating_sub(1))) } else { - Some((row, lines[row].chars().count().saturating_sub(1))) + let mut row = row; + loop { + if row == lines.len() - 1 { + break Some((row, lines[row].chars().count())); + } + row += 1; + if let Some(col) = find_word_inclusive_end_forward(&lines[row], 0) { + break Some((row, col)); + } + } } } WordForward => { diff --git a/src/textarea.rs b/src/textarea.rs index 1908874..0efd170 100644 --- a/src/textarea.rs +++ b/src/textarea.rs @@ -10,7 +10,7 @@ use crate::scroll::Scrolling; use crate::search::Search; use crate::util::{spaces, Pos}; use crate::widget::{Renderer, Viewport}; -use crate::word::{find_word_end_forward, find_word_start_backward}; +use crate::word::{find_word_exclusive_end_forward, find_word_start_backward}; #[cfg(feature = "ratatui")] use ratatui::text::Line; use std::cmp::Ordering; @@ -1256,7 +1256,7 @@ impl<'a> TextArea<'a> { } let (r, c) = self.cursor; let line = &self.lines[r]; - if let Some(col) = find_word_end_forward(line, c) { + if let Some(col) = find_word_exclusive_end_forward(line, c) { self.delete_piece(c, col - c) } else { let end_col = line.chars().count(); diff --git a/src/word.rs b/src/word.rs index dcad8be..57b4058 100644 --- a/src/word.rs +++ b/src/word.rs @@ -30,7 +30,7 @@ pub fn find_word_start_forward(line: &str, start_col: usize) -> Option { None } -pub fn find_word_end_forward(line: &str, start_col: usize) -> Option { +pub fn find_word_exclusive_end_forward(line: &str, start_col: usize) -> Option { let mut it = line.chars().enumerate().skip(start_col); let mut prev = CharKind::new(it.next()?.1); for (col, c) in it { @@ -43,24 +43,19 @@ pub fn find_word_end_forward(line: &str, start_col: usize) -> Option { None } -pub fn find_word_end_next(line: &str, start_col: usize) -> Option { +pub fn find_word_inclusive_end_forward(line: &str, start_col: usize) -> Option { let mut it = line.chars().enumerate().skip(start_col); - let (mut cur_col, cur_char) = it.next()?; - let mut cur = CharKind::new(cur_char); - for (next_col, c) in it { - let next = CharKind::new(c); - // if cursor started at the end of a word, don't stop - if next_col.saturating_sub(start_col) > 1 && cur != CharKind::Space && next != cur { - return Some(next_col.saturating_sub(1)); + let (mut last_col, c) = it.next()?; + let mut prev = CharKind::new(c); + for (col, c) in it { + let cur = CharKind::new(c); + if prev != CharKind::Space && cur != prev { + return Some(col.saturating_sub(1)); } - cur = next; - cur_col = next_col; + prev = cur; + last_col = col; } - // if end of line is whitespace, don't stop the cursor - if cur != CharKind::Space && cur_col.saturating_sub(start_col) >= 1 { - return Some(cur_col); - } - None + (prev != CharKind::Space).then_some(last_col) } pub fn find_word_start_backward(line: &str, start_col: usize) -> Option {