tuiの単語単位Backspace
This commit is contained in:
parent
99dbb1c6c0
commit
b79747bd0c
2
TODO.md
2
TODO.md
|
|
@ -12,7 +12,7 @@
|
|||
- [ ] フルスクリーン化によるオーバーホール → [tickets/tui-fullscreen-overhaul.md](tickets/tui-fullscreen-overhaul.md)
|
||||
- [ ] Run 中の入力キューイング → [tickets/tui-input-queue.md](tickets/tui-input-queue.md)
|
||||
- [ ] ユーザーマニフェストのモデル設定 wizard → [tickets/tui-user-model-setup.md](tickets/tui-user-model-setup.md)
|
||||
- [ ] 入力欄の単語単位カーソル移動 → [tickets/tui-input-word-motion.md](tickets/tui-input-word-motion.md)
|
||||
- [ ] 入力欄の単語単位カーソル移動・削除 → [tickets/tui-input-word-motion.md](tickets/tui-input-word-motion.md)
|
||||
- [ ] サブミット入力
|
||||
- [ ] TUI 補完 + 型付き atom 化 → [tickets/submit-tui-completion.md](tickets/submit-tui-completion.md)
|
||||
- [ ] セッションログの Segment 保持 → [tickets/session-log-segments.md](tickets/session-log-segments.md)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# TUI: 入力欄の単語単位カーソル移動
|
||||
# TUI: 入力欄の単語単位カーソル移動・削除
|
||||
|
||||
## 背景
|
||||
|
||||
TUI の入力欄では現在、`Left/Right` で1文字単位の移動、`Home/End` で行端への移動ができるが、単語単位で飛ぶ手段がない。長めの行を編集するときに左右キーを押し続けることになりテンポが悪い。
|
||||
TUI の入力欄では現在、`Left/Right` で1文字単位の移動、`Home/End` で行端への移動ができるが、単語単位で飛ぶ手段がない。`Backspace` も1文字ずつしか消せない。長めの行を編集するときに左右キーや Backspace を押し続けることになりテンポが悪い。
|
||||
|
||||
シェルやエディタで広く使われている `Ctrl+Left` / `Ctrl+Right` での単語単位移動を提供したい。
|
||||
シェルやエディタで広く使われている `Ctrl+Left` / `Ctrl+Right` での単語単位移動と、`Ctrl+Backspace` での単語単位削除を提供したい。
|
||||
|
||||
## 要件
|
||||
|
||||
|
|
@ -20,16 +20,18 @@ TUI の入力欄では現在、`Left/Right` で1文字単位の移動、`Home/En
|
|||
- 同じ種別の連続は1単語、種別が切り替わる位置で境界となる。形態素解析は使わない(送り仮名の途中で切れることは許容、VSCode/emacs と同等の挙動)。
|
||||
- ペースト atom (`Atom::Paste`) は不可分な1単語として扱う(カーソルが内部に入らない既存の不変条件を維持する)。
|
||||
- 既存の `Ctrl+Home/End` (履歴のスクロール)や `Ctrl+[` / `Ctrl+]` (ターンジャンプ)と衝突しないこと。
|
||||
- 既存の `Left/Right`(1文字移動)の挙動は変えない。
|
||||
- 既存の `Left/Right`(1文字移動)と `Backspace`(1文字削除)の挙動は変えない。
|
||||
- `Ctrl+Backspace` でカーソルから1単語ぶん手前を削除する。境界判定は `Ctrl+Left` と同じ(同じロジックを共有)。
|
||||
|
||||
## 完了条件
|
||||
|
||||
- `crates/tui` で `Ctrl+Left` / `Ctrl+Right` が単語単位移動として動作する。
|
||||
- `Ctrl+Backspace` で単語単位削除が動作する。
|
||||
- 単語境界の判定にユニットテストが付いている(空・連続スペース・`\n` をまたぐ・Paste をまたぐ・ひらがな/カタカナ/漢字/ASCII の混在)。
|
||||
- 既存テストが通る。
|
||||
|
||||
## 範囲外
|
||||
|
||||
- `Ctrl+Backspace` / `Alt+Backspace` などによる単語単位削除(別チケット候補)。
|
||||
- `Ctrl+Delete` / `Alt+d` などによる単語単位の前方削除(別チケット候補)。
|
||||
- `Alt+Left/Right` など他の単語移動キーバインドの追加。
|
||||
- 形態素解析による日本語の単語分割(辞書サイズ・起動コストの観点で TUI には過剰)。送り仮名や複合語の途中で切れる挙動は許容する。
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user