fix: wire tui word navigation

This commit is contained in:
Keisuke Hirata 2026-06-03 09:22:50 +09:00
parent d0bdad58c2
commit cc98c0eb28
No known key found for this signature in database
3 changed files with 154 additions and 7 deletions

View File

@ -47,10 +47,8 @@ fn merge_usage(acc: &mut UsageEvent, new: &UsageEvent) {
pub trait ErasedHandler<K: Kind>: Send + Sync {
/// イベントをディスパッチ
fn dispatch(&mut self, event: &K::Event);
/// スコープを開始Block開始時
/// スコープを開始
fn start_scope(&mut self);
/// スコープを終了Block終了時
fn end_scope(&mut self);
}
/// `Handler<K>`を`ErasedHandler<K>`として扱うためのラッパー
@ -94,10 +92,6 @@ where
fn start_scope(&mut self) {
self.scope = Some(H::Scope::default());
}
fn end_scope(&mut self) {
self.scope = None;
}
}
// =============================================================================

View File

@ -586,10 +586,12 @@ impl App {
self.queued_inputs.len()
}
#[cfg(test)]
pub fn input_history_len(&self) -> usize {
self.input_history.entries.len()
}
#[cfg(test)]
pub fn input_history_is_browsing(&self) -> bool {
self.input_history.is_browsing()
}
@ -1701,6 +1703,22 @@ impl App {
pub fn move_cursor_right(&mut self) {
self.active_input_mut().move_right();
}
pub fn move_cursor_word_left(&mut self) {
self.active_input_mut().move_word_left();
}
pub fn move_cursor_word_right(&mut self) {
self.active_input_mut().move_word_right();
}
pub fn delete_word_before_cursor(&mut self) {
let command_mode = self.is_command_mode();
if !command_mode {
self.input_history.cancel_browse();
}
self.active_input_mut().delete_word_before();
if command_mode {
self.command_completion_selected = None;
}
}
pub fn move_cursor_start(&mut self) {
self.active_input_mut().move_start();
}

View File

@ -573,6 +573,22 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option<Method> {
app.move_cursor_start();
Some(app.refresh_completion())
}
KeyCode::Left if ctrl || alt => {
app.move_cursor_word_left();
Some(app.refresh_completion())
}
KeyCode::Right if ctrl || alt => {
app.move_cursor_word_right();
Some(app.refresh_completion())
}
KeyCode::Backspace if ctrl || alt => {
app.delete_word_before_cursor();
Some(app.refresh_completion())
}
KeyCode::Char('w') if ctrl => {
app.delete_word_before_cursor();
Some(app.refresh_completion())
}
KeyCode::Char('u') if ctrl && app.is_command_mode() => {
app.clear_command_input();
Some(None)
@ -1067,6 +1083,125 @@ mod tests {
assert_eq!(app.queued_input_count(), 0);
}
#[test]
fn word_navigation_keys_edit_composer() {
let mut app = App::new("agent".to_string());
for c in "foo bar".chars() {
assert!(
handle_key(
&mut app,
KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)
)
.is_none()
);
}
assert!(
handle_key(
&mut app,
KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL)
)
.is_none()
);
assert!(
handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('_'), KeyModifiers::NONE)
)
.is_none()
);
assert_eq!(input_text(&app), "foo _bar");
assert!(handle_key(&mut app, KeyEvent::new(KeyCode::Right, KeyModifiers::ALT)).is_none());
assert!(
handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('!'), KeyModifiers::NONE)
)
.is_none()
);
assert_eq!(input_text(&app), "foo _bar!");
assert!(
handle_key(
&mut app,
KeyEvent::new(KeyCode::Backspace, KeyModifiers::CONTROL)
)
.is_none()
);
assert_eq!(input_text(&app), "foo ");
}
#[test]
fn ctrl_w_deletes_word_before_cursor() {
let mut app = App::new("agent".to_string());
for c in "foo bar baz".chars() {
assert!(
handle_key(
&mut app,
KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)
)
.is_none()
);
}
assert!(
handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL)
)
.is_none()
);
assert_eq!(input_text(&app), "foo bar ");
}
#[test]
fn word_navigation_keys_edit_command_input() {
let mut app = App::new("agent".to_string());
assert!(
handle_key(
&mut app,
KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE)
)
.is_none()
);
for c in "peer alpha beta".chars() {
assert!(
handle_key(
&mut app,
KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)
)
.is_none()
);
}
assert!(
handle_key(
&mut app,
KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL)
)
.is_none()
);
assert!(
handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('_'), KeyModifiers::NONE)
)
.is_none()
);
assert_eq!(app.command_text(), "peer alpha _beta");
assert!(
handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL)
)
.is_none()
);
assert_eq!(app.command_text(), "peer alpha beta");
assert_eq!(input_text(&app), "");
}
#[test]
fn command_mode_enters_with_colon_and_esc_restores_composer() {
let mut app = App::new("agent".to_string());