add ^Y and kill-buffer to textarea (#5075)
## Summary - add a kill buffer to the text area and wire Ctrl+Y to yank it - capture text from Ctrl+W, Ctrl+U, and Ctrl+K operations into the kill buffer - add regression coverage ensuring the last kill can be yanked back Fixes #5017 ------ https://chatgpt.com/codex/tasks/task_i_68e95bf06c48832cbf3d2ba8fa2035d2
This commit is contained in:
@@ -26,6 +26,7 @@ pub(crate) struct TextArea {
|
|||||||
wrap_cache: RefCell<Option<WrapCache>>,
|
wrap_cache: RefCell<Option<WrapCache>>,
|
||||||
preferred_col: Option<usize>,
|
preferred_col: Option<usize>,
|
||||||
elements: Vec<TextElement>,
|
elements: Vec<TextElement>,
|
||||||
|
kill_buffer: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -48,6 +49,7 @@ impl TextArea {
|
|||||||
wrap_cache: RefCell::new(None),
|
wrap_cache: RefCell::new(None),
|
||||||
preferred_col: None,
|
preferred_col: None,
|
||||||
elements: Vec::new(),
|
elements: Vec::new(),
|
||||||
|
kill_buffer: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +59,7 @@ impl TextArea {
|
|||||||
self.wrap_cache.replace(None);
|
self.wrap_cache.replace(None);
|
||||||
self.preferred_col = None;
|
self.preferred_col = None;
|
||||||
self.elements.clear();
|
self.elements.clear();
|
||||||
|
self.kill_buffer.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text(&self) -> &str {
|
pub fn text(&self) -> &str {
|
||||||
@@ -305,6 +308,13 @@ impl TextArea {
|
|||||||
} => {
|
} => {
|
||||||
self.kill_to_end_of_line();
|
self.kill_to_end_of_line();
|
||||||
}
|
}
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Char('y'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.yank();
|
||||||
|
}
|
||||||
|
|
||||||
// Cursor movement
|
// Cursor movement
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
@@ -437,7 +447,7 @@ impl TextArea {
|
|||||||
|
|
||||||
pub fn delete_backward_word(&mut self) {
|
pub fn delete_backward_word(&mut self) {
|
||||||
let start = self.beginning_of_previous_word();
|
let start = self.beginning_of_previous_word();
|
||||||
self.replace_range(start..self.cursor_pos, "");
|
self.kill_range(start..self.cursor_pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete text to the right of the cursor using "word" semantics.
|
/// Delete text to the right of the cursor using "word" semantics.
|
||||||
@@ -448,32 +458,63 @@ impl TextArea {
|
|||||||
pub fn delete_forward_word(&mut self) {
|
pub fn delete_forward_word(&mut self) {
|
||||||
let end = self.end_of_next_word();
|
let end = self.end_of_next_word();
|
||||||
if end > self.cursor_pos {
|
if end > self.cursor_pos {
|
||||||
self.replace_range(self.cursor_pos..end, "");
|
self.kill_range(self.cursor_pos..end);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn kill_to_end_of_line(&mut self) {
|
pub fn kill_to_end_of_line(&mut self) {
|
||||||
let eol = self.end_of_current_line();
|
let eol = self.end_of_current_line();
|
||||||
if self.cursor_pos == eol {
|
let range = if self.cursor_pos == eol {
|
||||||
if eol < self.text.len() {
|
if eol < self.text.len() {
|
||||||
self.replace_range(self.cursor_pos..eol + 1, "");
|
Some(self.cursor_pos..eol + 1)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.replace_range(self.cursor_pos..eol, "");
|
Some(self.cursor_pos..eol)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(range) = range {
|
||||||
|
self.kill_range(range);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn kill_to_beginning_of_line(&mut self) {
|
pub fn kill_to_beginning_of_line(&mut self) {
|
||||||
let bol = self.beginning_of_current_line();
|
let bol = self.beginning_of_current_line();
|
||||||
if self.cursor_pos == bol {
|
let range = if self.cursor_pos == bol {
|
||||||
if bol > 0 {
|
if bol > 0 { Some(bol - 1..bol) } else { None }
|
||||||
self.replace_range(bol - 1..bol, "");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
self.replace_range(bol..self.cursor_pos, "");
|
Some(bol..self.cursor_pos)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(range) = range {
|
||||||
|
self.kill_range(range);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn yank(&mut self) {
|
||||||
|
if self.kill_buffer.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let text = self.kill_buffer.clone();
|
||||||
|
self.insert_str(&text);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kill_range(&mut self, range: Range<usize>) {
|
||||||
|
let range = self.expand_range_to_element_boundaries(range);
|
||||||
|
if range.start >= range.end {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let removed = self.text[range.clone()].to_string();
|
||||||
|
if removed.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.kill_buffer = removed;
|
||||||
|
self.replace_range_raw(range, "");
|
||||||
|
}
|
||||||
|
|
||||||
/// Move the cursor left by a single grapheme cluster.
|
/// Move the cursor left by a single grapheme cluster.
|
||||||
pub fn move_cursor_left(&mut self) {
|
pub fn move_cursor_left(&mut self) {
|
||||||
self.cursor_pos = self.prev_atomic_boundary(self.cursor_pos);
|
self.cursor_pos = self.prev_atomic_boundary(self.cursor_pos);
|
||||||
@@ -1198,6 +1239,39 @@ mod tests {
|
|||||||
assert_eq!(t.cursor(), elem_range.start);
|
assert_eq!(t.cursor(), elem_range.start);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yank_restores_last_kill() {
|
||||||
|
let mut t = ta_with("hello");
|
||||||
|
t.set_cursor(0);
|
||||||
|
t.kill_to_end_of_line();
|
||||||
|
assert_eq!(t.text(), "");
|
||||||
|
assert_eq!(t.cursor(), 0);
|
||||||
|
|
||||||
|
t.yank();
|
||||||
|
assert_eq!(t.text(), "hello");
|
||||||
|
assert_eq!(t.cursor(), 5);
|
||||||
|
|
||||||
|
let mut t = ta_with("hello world");
|
||||||
|
t.set_cursor(t.text().len());
|
||||||
|
t.delete_backward_word();
|
||||||
|
assert_eq!(t.text(), "hello ");
|
||||||
|
assert_eq!(t.cursor(), 6);
|
||||||
|
|
||||||
|
t.yank();
|
||||||
|
assert_eq!(t.text(), "hello world");
|
||||||
|
assert_eq!(t.cursor(), 11);
|
||||||
|
|
||||||
|
let mut t = ta_with("hello");
|
||||||
|
t.set_cursor(5);
|
||||||
|
t.kill_to_beginning_of_line();
|
||||||
|
assert_eq!(t.text(), "");
|
||||||
|
assert_eq!(t.cursor(), 0);
|
||||||
|
|
||||||
|
t.yank();
|
||||||
|
assert_eq!(t.text(), "hello");
|
||||||
|
assert_eq!(t.cursor(), 5);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cursor_left_and_right_handle_graphemes() {
|
fn cursor_left_and_right_handle_graphemes() {
|
||||||
let mut t = ta_with("a👍b");
|
let mut t = ta_with("a👍b");
|
||||||
|
|||||||
Reference in New Issue
Block a user