feat(kebab-tui): p9-fb-12 follow-up — heuristic 제거, mode-authoritative dispatch
p9-fb-12 partial (PR #84) 의 deferred 부분 finalize. spec contract 의 \"기존 P9-3 ask 의 e/j/k input-empty heuristic 제거 — mode 로 명확히\" 완료. spec status `in_progress` → `completed`. ## 핵심 변경 - **`search::is_typing_mod`** (CTRL/ALT chord filter) 함수 삭제. search Char dispatch 가 `state.mode` 로 분기: - Normal + plain `j`/`k` → 선택 이동 (Char 이라도 Normal 이면 navigation) - Insert + plain `j`/`k`/Char(c) (chord 제외) → input.push - Insert + CTRL/ALT chord → no-op (예약 — 향후 binding 위해) - Normal + 그 외 Char → no-op (no typing in Normal) - **`search::handle_key_search` 의 `i` (chunk inspect) / `g` (editor jump) pre-pass** 가 `state.mode == Mode::Normal` 일 때만 fire. Insert 모드면 typed char (input 에 push). 기존 SHIFT-aware matches!() 가드는 Normal-mode 진입 가드로 흡수. - **`ask::handle_key_ask`** 의 input-empty heuristic 삭제. e/j/k: - Normal + `e` → toggle explain - Normal + `j` → scroll down (saturating_add) - Normal + `k` → scroll up (saturating_sub) - Insert + 모든 plain Char (chord 제외) → input.push - **테스트 fixture** (`tests/search.rs::fresh_app`, `tests/ask.rs::fresh_app`) 에 `app.mode = Mode::auto_for(focus)` 추가 — run loop 의 auto-flip 동작을 테스트가 mirror. - **기존 nav 테스트** (`j_k_move_selection_within_bounds`, `g_key_enqueues_pending_editor_request`, `e_toggles_explain_in_ normal_mode`) 가 `app.mode = Mode::Normal` 명시. - **신규 4 테스트** mode-authoritative 동작 회귀 방지: - search: `j_in_insert_types_does_not_move_selection`, `arbitrary_char_in_normal_mode_is_noop` - ask: `e_types_in_insert_mode_does_not_toggle_explain`, `jk_scroll_in_normal_mode_type_in_insert` ## 테스트 - 기존 109 + 신규 4 = 113 TUI 테스트 통과 (38 lib + 20 ask + 12 inspect + 10 library + 6 mode + 25 search + 2 chat — search 23→25, ask 18→20) - `cargo test --workspace --no-fail-fast -j 1` exit 0 - `cargo clippy --workspace --all-targets -- -D warnings` clean ## 문서 - README `kebab tui` 행: \"mode-authoritative dispatch — Search 의 j/k/i/g, Ask 의 e/j/k 는 NORMAL 모드에서만 명령으로 동작, INSERT 에서는 입력 문자로 typing\" 명시 - HANDOFF: 2026-05-03 follow-up entry - spec status `in_progress` → `completed` ## HOTFIXES p9-fb-12 partial PR (#84) 의 \"Deferred\" 항목이 본 PR 로 finalized — HOTFIXES 새 entry 불필요 (기존 entry 가 이미 deferral 사유 + 해결 조건 명시). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,11 @@ fn fresh_app() -> App {
|
||||
config.workspace.root = "/tmp/kebab-tui-search-tests-noop/workspace".to_string();
|
||||
let mut app = App::new(config).expect("App::new");
|
||||
app.focus = Pane::Search;
|
||||
// p9-fb-12 follow-up: mirror the run loop's auto-flip — Search
|
||||
// pane auto-Insert. Tests that exercise Normal-mode navigation
|
||||
// (j/k move selection, i / g pre-pass) set Mode::Normal
|
||||
// explicitly.
|
||||
app.mode = kebab_tui::Mode::auto_for(Pane::Search);
|
||||
app.search = Some(SearchState::default());
|
||||
app
|
||||
}
|
||||
@@ -138,6 +143,10 @@ fn enter_with_empty_query_is_continue() {
|
||||
#[test]
|
||||
fn j_k_move_selection_within_bounds() {
|
||||
let mut app = fresh_app();
|
||||
// p9-fb-12 follow-up: j/k navigate only in Normal mode. Search
|
||||
// pane auto-Insert via fresh_app, flip to Normal explicitly to
|
||||
// exercise the navigation branch.
|
||||
app.mode = kebab_tui::Mode::Normal;
|
||||
{
|
||||
let s = app.search.as_mut().unwrap();
|
||||
s.hits = vec![
|
||||
@@ -248,6 +257,46 @@ fn empty_state_renders_without_panic() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// p9-fb-12 follow-up: in Insert mode, plain `j` types into input
|
||||
/// (does NOT move selection). Replaces the pre-fb-12 heuristic
|
||||
/// "is_typing_mod" with mode-authoritative dispatch.
|
||||
#[test]
|
||||
fn j_in_insert_types_does_not_move_selection() {
|
||||
let mut app = fresh_app();
|
||||
// Insert is auto for Search, but explicit for clarity.
|
||||
app.mode = kebab_tui::Mode::Insert;
|
||||
{
|
||||
let s = app.search.as_mut().unwrap();
|
||||
s.hits = vec![
|
||||
make_hit(1, "a.md", "snip", line_citation("a.md", 1)),
|
||||
make_hit(2, "b.md", "snip", line_citation("b.md", 1)),
|
||||
];
|
||||
s.selected_hit = 0;
|
||||
}
|
||||
handle_key_search(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE),
|
||||
);
|
||||
let s = app.search.as_ref().unwrap();
|
||||
assert_eq!(s.input, "j", "j must type in Insert mode");
|
||||
assert_eq!(s.selected_hit, 0, "selection must NOT move in Insert");
|
||||
}
|
||||
|
||||
/// p9-fb-12 follow-up: in Normal mode, plain Char other than j/k/i/g
|
||||
/// is a no-op (no typing in Normal). Pin so a future char binding
|
||||
/// addition has to think about Normal-mode behavior.
|
||||
#[test]
|
||||
fn arbitrary_char_in_normal_mode_is_noop() {
|
||||
let mut app = fresh_app();
|
||||
app.mode = kebab_tui::Mode::Normal;
|
||||
handle_key_search(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE),
|
||||
);
|
||||
let s = app.search.as_ref().unwrap();
|
||||
assert_eq!(s.input, "", "Normal-mode Char must NOT type");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_j_stays_in_input_does_not_move_selection() {
|
||||
// R1 fix: SHIFT-J / SHIFT-K must reach the typing branch so
|
||||
@@ -295,6 +344,9 @@ fn shift_g_does_not_trigger_editor_jump() {
|
||||
#[test]
|
||||
fn g_key_enqueues_pending_editor_request() {
|
||||
let mut app = fresh_app();
|
||||
// p9-fb-12 follow-up: `g` (editor jump) is a Normal-mode command;
|
||||
// in Insert mode it types as 'g'. Flip explicitly.
|
||||
app.mode = kebab_tui::Mode::Normal;
|
||||
{
|
||||
let s = app.search.as_mut().unwrap();
|
||||
s.hits = vec![make_hit(1, "notes/x.md", "snippet", line_citation("notes/x.md", 42))];
|
||||
|
||||
Reference in New Issue
Block a user