tuiの単語単位Backspace

This commit is contained in:
2026-04-29 21:31:19 +09:00
parent 99dbb1c6c0
commit b79747bd0c
5 changed files with 102 additions and 6 deletions
+3
View File
@@ -398,6 +398,9 @@ impl App {
pub fn delete_char_after(&mut self) {
self.input.delete_after();
}
pub fn delete_word_before(&mut self) {
self.input.delete_word_before();
}
pub fn move_cursor_left(&mut self) {
self.input.move_left();
}
+87
View File
@@ -148,6 +148,15 @@ impl InputBuffer {
}
}
/// Delete one word backward — the same span [`move_word_left`] would
/// jump over.
pub fn delete_word_before(&mut self) {
let end = self.cursor;
self.move_word_left();
let start = self.cursor;
self.atoms.drain(start..end);
}
pub fn move_left(&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
@@ -724,6 +733,84 @@ mod word_motion_tests {
assert_eq!(cursor(&buf), 0);
}
/// Render atoms as a string for assertions; pastes become `<P>`.
fn as_text(buf: &InputBuffer) -> String {
let mut out = String::new();
for a in &buf.atoms {
match a {
Atom::Char(c) => out.push(*c),
Atom::Paste(_) => out.push_str("<P>"),
}
}
out
}
#[test]
fn delete_word_removes_trailing_word_at_end() {
let mut buf = buf_from("foo bar");
buf.delete_word_before();
assert_eq!(as_text(&buf), "foo ");
assert_eq!(cursor(&buf), 4);
}
#[test]
fn delete_word_removes_word_at_cursor() {
let mut buf = buf_from("foo bar");
buf.cursor = 3; // right after "foo"
buf.delete_word_before();
assert_eq!(as_text(&buf), " bar");
assert_eq!(cursor(&buf), 0);
}
#[test]
fn delete_word_swallows_trailing_separators() {
let mut buf = buf_from("foo ");
buf.delete_word_before();
assert_eq!(as_text(&buf), "");
assert_eq!(cursor(&buf), 0);
}
#[test]
fn delete_word_at_start_is_noop() {
let mut buf = buf_from("foo");
buf.cursor = 0;
buf.delete_word_before();
assert_eq!(as_text(&buf), "foo");
assert_eq!(cursor(&buf), 0);
}
#[test]
fn delete_word_respects_script_boundary() {
// 「日本語の」末尾から1回削除すると、ひらがな部分「の」だけ消える
let mut buf = buf_from("日本語の");
buf.delete_word_before();
assert_eq!(as_text(&buf), "日本語");
assert_eq!(cursor(&buf), 3);
buf.delete_word_before();
assert_eq!(as_text(&buf), "");
assert_eq!(cursor(&buf), 0);
}
#[test]
fn delete_word_treats_paste_as_one_unit() {
let mut buf = InputBuffer::new();
for c in "foo ".chars() {
buf.insert_char(c);
}
buf.insert_paste("anything".into());
for c in " bar".chars() {
buf.insert_char(c);
}
// atoms: f o o ' ' [P] ' ' b a r (cursor at end = 9)
buf.delete_word_before();
assert_eq!(as_text(&buf), "foo <P> ");
assert_eq!(cursor(&buf), 6);
// Next deletion: trailing space then the paste atom (kind=Paste)
buf.delete_word_before();
assert_eq!(as_text(&buf), "foo ");
assert_eq!(cursor(&buf), 4);
}
#[test]
fn japanese_punctuation_is_a_separator() {
// 「、」 (U+3001) and 「。」 (U+3002) are not word chars.
+4
View File
@@ -407,6 +407,10 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option<Method> {
None
}
KeyCode::Enter => app.submit_input(),
KeyCode::Backspace if ctrl => {
app.delete_word_before();
None
}
KeyCode::Backspace => {
app.delete_char_before();
None