diff --git a/src/cursor.rs b/src/cursor.rs index 2c9ae61..c87e40f 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -10,6 +10,27 @@ pub enum CursorMove { End, Top, Bottom, + WordForward, + WordBack, +} + +#[derive(PartialEq, Eq, Clone, Copy)] +enum CharKind { + Space, + Punct, + Other, +} + +impl CharKind { + fn new(c: char) -> Self { + if c.is_whitespace() { + Self::Space + } else if c.is_ascii_punctuation() { + Self::Punct + } else { + Self::Other + } + } } impl CursorMove { @@ -18,38 +39,91 @@ impl CursorMove { (row, col): (usize, usize), lines: &[String], ) -> Option<(usize, usize)> { + use CursorMove::*; + fn fit_col(col: usize, line: &str) -> usize { cmp::min(col, line.chars().count()) } match self { - CursorMove::Forward if col >= lines[row].chars().count() => { + Forward if col >= lines[row].chars().count() => { if row + 1 < lines.len() { Some((row + 1, 0)) } else { None } } - CursorMove::Forward => Some((row, col + 1)), - CursorMove::Back if col == 0 => { + Forward => Some((row, col + 1)), + Back if col == 0 => { if row > 0 { Some((row - 1, lines[row - 1].chars().count())) } else { None } } - CursorMove::Back => Some((row, col - 1)), - CursorMove::Up if row == 0 => None, - CursorMove::Up => Some((row - 1, fit_col(col, &lines[row - 1]))), - CursorMove::Down if row + 1 >= lines.len() => None, - CursorMove::Down => (Some((row + 1, fit_col(col, &lines[row + 1])))), - CursorMove::Head => Some((row, 0)), - CursorMove::End => Some((row, lines[row].chars().count())), - CursorMove::Top => Some((0, fit_col(col, &lines[0]))), - CursorMove::Bottom => { + Back => Some((row, col - 1)), + Up if row == 0 => None, + Up => Some((row - 1, fit_col(col, &lines[row - 1]))), + Down if row + 1 >= lines.len() => None, + Down => (Some((row + 1, fit_col(col, &lines[row + 1])))), + Head => Some((row, 0)), + End => Some((row, lines[row].chars().count())), + Top => Some((0, fit_col(col, &lines[0]))), + Bottom => { let row = lines.len() - 1; Some((row, fit_col(col, &lines[row]))) } + WordForward => { + fn find_word_forward(line: &str, col: usize) -> Option { + let mut it = line.chars().enumerate().skip(col); + let mut prev = CharKind::new(it.next()?.1); + for (col, c) in it { + let cur = CharKind::new(c); + if cur != CharKind::Space && prev != cur { + return Some(col); + } + prev = cur; + } + None + } + if let Some(col) = find_word_forward(&lines[row], col) { + Some((row, col)) + } else if row + 1 < lines.len() { + Some((row + 1, 0)) + } else { + Some((row, lines[row].chars().count())) + } + } + WordBack => { + fn find_word_back(line: &str, col: usize) -> Option { + let idx = line + .char_indices() + .nth(col) + .map(|(i, _)| i) + .unwrap_or(line.len()); + let mut it = line[..idx].chars().rev().enumerate(); + let mut cur = CharKind::new(it.next()?.1); + for (i, c) in it { + let next = CharKind::new(c); + if cur != CharKind::Space && next != cur { + return Some(col - i); + } + cur = next; + } + if cur != CharKind::Space { + Some(0) + } else { + None + } + } + if let Some(col) = find_word_back(&lines[row], col) { + Some((row, col)) + } else if row > 0 { + Some((row - 1, lines[row - 1].chars().count())) + } else { + Some((row, 0)) + } + } } } } diff --git a/src/textarea.rs b/src/textarea.rs index 9dd08f4..918036d 100644 --- a/src/textarea.rs +++ b/src/textarea.rs @@ -158,6 +158,26 @@ impl<'a> TextArea<'a> { ctrl: true, alt: true, } => self.move_cursor(CursorMove::Bottom), + Input { + key: Key::Char('f'), + ctrl: false, + alt: true, + } + | Input { + key: Key::Right, + ctrl: true, + alt: false, + } => self.move_cursor(CursorMove::WordForward), + Input { + key: Key::Char('b'), + ctrl: false, + alt: true, + } + | Input { + key: Key::Left, + ctrl: true, + alt: false, + } => self.move_cursor(CursorMove::WordBack), Input { key: Key::Char('u'), ctrl: true,