diff --git a/src/cursor.rs b/src/cursor.rs index 052598a..0ad7c8c 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -1,3 +1,4 @@ +use crate::word::{find_word_start_backward, find_word_start_forward}; use std::cmp; #[derive(Clone, Copy, Debug)] @@ -16,25 +17,6 @@ pub enum CursorMove { ParagraphBack, } -#[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 { pub fn next_cursor( &self, @@ -76,19 +58,7 @@ impl CursorMove { 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) { + if let Some(col) = find_word_start_forward(&lines[row], col) { Some((row, col)) } else if row + 1 < lines.len() { Some((row + 1, 0)) @@ -97,28 +67,7 @@ impl CursorMove { } } 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) { + if let Some(col) = find_word_start_backward(&lines[row], col) { Some((row, col)) } else if row > 0 { Some((row - 1, lines[row - 1].chars().count())) diff --git a/src/lib.rs b/src/lib.rs index 289fe4d..d84095b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ mod cursor; mod history; mod input; mod textarea; +mod word; pub use cursor::CursorMove; pub use input::{Input, Key}; diff --git a/src/textarea.rs b/src/textarea.rs index 4663552..2f3b3b8 100644 --- a/src/textarea.rs +++ b/src/textarea.rs @@ -1,6 +1,7 @@ use crate::cursor::CursorMove; use crate::history::{Edit, EditKind, History}; use crate::input::{Input, Key}; +use crate::word::{find_word_end_forward, find_word_start_backward}; use std::sync::atomic::{AtomicU16, Ordering}; use tui::buffer::Buffer; use tui::layout::Rect; @@ -74,7 +75,8 @@ impl<'a> TextArea<'a> { } | Input { key: Key::Backspace, - .. + ctrl: false, + alt: false, } => self.delete_char(), Input { key: Key::Char('d'), @@ -82,7 +84,9 @@ impl<'a> TextArea<'a> { alt: false, } | Input { - key: Key::Delete, .. + key: Key::Delete, + ctrl: false, + alt: false, } => self.delete_next_char(), Input { key: Key::Char('m'), @@ -102,6 +106,31 @@ impl<'a> TextArea<'a> { ctrl: true, alt: false, } => self.delete_line_by_head(), + Input { + key: Key::Char('w'), + ctrl: true, + alt: false, + } + | Input { + key: Key::Char('h'), + ctrl: false, + alt: true, + } + | Input { + key: Key::Backspace, + ctrl: false, + alt: true, + } => self.delete_word(), + Input { + key: Key::Delete, + ctrl: false, + alt: true, + } + | Input { + key: Key::Char('d'), + ctrl: false, + alt: true, + } => self.delete_next_word(), Input { key: Key::Char('n'), ctrl: true, @@ -336,8 +365,6 @@ impl<'a> TextArea<'a> { let row = self.cursor.0; let line = &mut self.lines[row]; if let Some((i, _)) = line.char_indices().nth(col) { - // push/pop ' ' to preserve ' ' at the end of line - line.pop(); let bytes = line[i..] .char_indices() .nth(chars) @@ -345,7 +372,6 @@ impl<'a> TextArea<'a> { .unwrap_or_else(|| line[i..].len()); let removed = line[i..i + bytes].to_string(); line.replace_range(i..i + bytes, ""); - line.push(' '); self.cursor = (row, col); self.push_history(EditKind::Remove(removed.clone(), i), (row, col)); @@ -376,18 +402,25 @@ impl<'a> TextArea<'a> { self.push_history(EditKind::InsertNewline(idx), (row, col)); } + pub fn delete_newline(&mut self) { + let (row, col) = self.cursor; + if row == 0 { + return; + } + + let line = self.lines.remove(row); + let prev_line = &mut self.lines[row - 1]; + let prev_line_end = prev_line.len(); + + self.cursor = (row - 1, prev_line.chars().count()); + prev_line.push_str(&line); + self.push_history(EditKind::DeleteNewline(prev_line_end), (row, col)); + } + pub fn delete_char(&mut self) { let (row, col) = self.cursor; if col == 0 { - if row > 0 { - let line = self.lines.remove(row); - let prev_line = &mut self.lines[row - 1]; - let prev_line_end = prev_line.len(); - - self.cursor = (row - 1, prev_line.chars().count()); - prev_line.push_str(&line); - self.push_history(EditKind::DeleteNewline(prev_line_end), (row, col)); - } + self.delete_newline(); return; } @@ -416,6 +449,33 @@ impl<'a> TextArea<'a> { self.delete_str(0, self.cursor.1); } + pub fn delete_word(&mut self) { + let (r, c) = self.cursor; + if let Some(col) = find_word_start_backward(&self.lines[r], c) { + self.delete_str(col, c - col); + } else if c > 0 { + self.delete_str(0, c); + } else { + self.delete_newline(); + } + } + + pub fn delete_next_word(&mut self) { + let (r, c) = self.cursor; + let line = &self.lines[r]; + if let Some(col) = find_word_end_forward(line, c) { + self.delete_str(c, col - c); + } else { + let end_col = line.chars().count(); + if c < end_col { + self.delete_str(c, end_col - c); + } else if r + 1 < self.lines.len() { + self.cursor = (r + 1, 0); + self.delete_newline(); + } + } + } + pub fn paste(&mut self) { let yank = std::mem::take(&mut self.yank); self.insert_str(yank); diff --git a/src/word.rs b/src/word.rs new file mode 100644 index 0000000..8edb814 --- /dev/null +++ b/src/word.rs @@ -0,0 +1,62 @@ +#[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 + } + } +} + +pub fn find_word_start_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 { + let cur = CharKind::new(c); + if cur != CharKind::Space && prev != cur { + return Some(col); + } + prev = cur; + } + None +} + +pub fn find_word_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 { + let cur = CharKind::new(c); + if prev != CharKind::Space && prev != cur { + return Some(col); + } + prev = cur; + } + None +} + +pub fn find_word_start_backward(line: &str, start_col: usize) -> Option { + let idx = line + .char_indices() + .nth(start_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(start_col - i); + } + cur = next; + } + (cur != CharKind::Space).then(|| 0) +}