feat(kebab-tui): P9-2 Search pane #44

Merged
altair823 merged 2 commits from feat/p9-2-tui-search into main 2026-05-02 15:05:47 +00:00
Owner

요약

P9-1 Library 의 / 키 활성화. App.search slot 채움 (parallel-safety contract 그대로). 사용자가 kebab tui 에서 / → 입력 → Tab 으로 mode 순환 → Enter 또는 200 ms debounce 로 자동 검색 → j/k 이동 → g 로 $EDITOR jump → Esc Library 복귀.

핵심 결정

  • SearchState: app.rs 의 forward decl 본체 채움. cross-module 접근 (run.rs 가 debounce 시 시간 검사) 위해 fields pub.
  • Layout: 3-pane (input bar / 결과 리스트 §1.5 4-line dense / preview chunk text).
  • Debounce: 200 ms after last keystroke. run-loop 가 idle tick (150 ms poll) 에 debounce_due 검사 후 fire_search 호출. blocking 검색 동안 "searching…" 헤더에 표시.
  • Editor jump: vim/nvim/vi/emacs/hx 는 +<line> <path>, VS Code/Cursor 는 -g <path>:<line>. $EDITOR env 의 leading args (nvim -p) 그대로 보존. RAII raw-mode + AltScreen suspend/restore — panic 안전.
  • Lazy preview: 선택 변경 시 preview None 으로 invalidate, idle tick 가 inspect_chunk_with_config 로 fetch.

Spec deviation (HOTFIXES 2026-05-02 P9-2)

  1. render_search<B: Backend> generic 제거 — ratatui 0.28 Frame backend-agnostic. P9-1 와 같은 사유.
  2. jump_to_citationworkspace_root: &Path 인자 추가 — citation.path 가 workspace 상대라 editor 호출 시 절대 경로 필요. spec literal 시그니처 unimplementable.

테스트 (13개, tests/search.rs)

  • esc_returns_to_library / no_search_state_returns_to_library
  • typing_appends_to_input_and_marks_dirty
  • backspace_removes_last_char
  • tab_cycles_mode_lex_vec_hybrid
  • enter_with_query_emits_refresh / enter_with_empty_query_is_continue
  • j_k_move_selection_within_bounds (clamp 검증)
  • build_jump_command_line_uses_plus_n_for_vim
  • build_jump_command_line_uses_g_flag_for_code
  • build_jump_command_passes_through_editor_args (nvim -p leading 보존)
  • render_search_with_hits_shows_input_and_path (TestBackend)
  • empty_state_renders_without_panic

Docs (sync rule)

  • README: TUI 행 "Library + Search 패널, ask/inspect 진행 중" + Quick start 의 kebab tui 코멘트 갱신.
  • HANDOFF: 한 줄 요약 + Phase status (P9 1/5 → 2/5) + deviation 한 줄.
  • HOTFIXES: P9-2 entry.
  • tasks/p9/p9-2: status completed.

(ARCHITECTURE 변경 없음 — crate 구성 동일.)

검증

  • cargo test -p kebab-tui 23 passed (10 library + 13 search)
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo build --release -p kebab-cli clean

Test plan

  • 단위 13개 통과
  • clippy -D warnings 통과
  • release rebuild
  • HOTFIXES + README + HANDOFF + spec status 동시 갱신
  • 사용자 manual smoke: real terminal kebab tui/ → 입력 → Tab → Enter → g

Out of scope (spec)

  • Inline citation render of LLM answers — P9-3 Ask pane.
  • --explain retrieval trace UI.
  • Mouse selection.
## 요약 P9-1 Library 의 `/` 키 활성화. `App.search` slot 채움 (parallel-safety contract 그대로). 사용자가 `kebab tui` 에서 `/` → 입력 → Tab 으로 mode 순환 → Enter 또는 200 ms debounce 로 자동 검색 → j/k 이동 → g 로 `$EDITOR` jump → Esc Library 복귀. ## 핵심 결정 - **SearchState**: `app.rs` 의 forward decl 본체 채움. cross-module 접근 (run.rs 가 debounce 시 시간 검사) 위해 fields `pub`. - **Layout**: 3-pane (input bar / 결과 리스트 §1.5 4-line dense / preview chunk text). - **Debounce**: 200 ms after last keystroke. run-loop 가 idle tick (150 ms poll) 에 `debounce_due` 검사 후 `fire_search` 호출. blocking 검색 동안 \"searching…\" 헤더에 표시. - **Editor jump**: vim/nvim/vi/emacs/hx 는 `+<line> <path>`, VS Code/Cursor 는 `-g <path>:<line>`. `$EDITOR` env 의 leading args (`nvim -p`) 그대로 보존. RAII raw-mode + AltScreen suspend/restore — panic 안전. - **Lazy preview**: 선택 변경 시 preview None 으로 invalidate, idle tick 가 `inspect_chunk_with_config` 로 fetch. ## Spec deviation (HOTFIXES `2026-05-02 P9-2`) 1. `render_search<B: Backend>` generic 제거 — ratatui 0.28 Frame backend-agnostic. P9-1 와 같은 사유. 2. `jump_to_citation` 가 `workspace_root: &Path` 인자 추가 — citation.path 가 workspace 상대라 editor 호출 시 절대 경로 필요. spec literal 시그니처 unimplementable. ## 테스트 (13개, `tests/search.rs`) - `esc_returns_to_library` / `no_search_state_returns_to_library` - `typing_appends_to_input_and_marks_dirty` - `backspace_removes_last_char` - `tab_cycles_mode_lex_vec_hybrid` - `enter_with_query_emits_refresh` / `enter_with_empty_query_is_continue` - `j_k_move_selection_within_bounds` (clamp 검증) - `build_jump_command_line_uses_plus_n_for_vim` - `build_jump_command_line_uses_g_flag_for_code` - `build_jump_command_passes_through_editor_args` (`nvim -p` leading 보존) - `render_search_with_hits_shows_input_and_path` (TestBackend) - `empty_state_renders_without_panic` ## Docs (sync rule) - **README**: TUI 행 \"Library + Search 패널, ask/inspect 진행 중\" + Quick start 의 `kebab tui` 코멘트 갱신. - **HANDOFF**: 한 줄 요약 + Phase status (P9 1/5 → 2/5) + deviation 한 줄. - **HOTFIXES**: P9-2 entry. - **tasks/p9/p9-2**: status `completed`. (ARCHITECTURE 변경 없음 — crate 구성 동일.) ## 검증 - `cargo test -p kebab-tui` 23 passed (10 library + 13 search) - `cargo clippy --workspace --all-targets -- -D warnings` clean - `cargo build --release -p kebab-cli` clean ## Test plan - [x] 단위 13개 통과 - [x] clippy `-D warnings` 통과 - [x] release rebuild - [x] HOTFIXES + README + HANDOFF + spec status 동시 갱신 - [ ] 사용자 manual smoke: real terminal `kebab tui` → `/` → 입력 → Tab → Enter → `g` ## Out of scope (spec) - Inline citation render of LLM answers — P9-3 Ask pane. - `--explain` retrieval trace UI. - Mouse selection.
altair823 added 1 commit 2026-05-02 14:39:47 +00:00
Library 의 / 키가 활성화. App.search slot 이 lazy 채워지고 (run loop 가 SwitchPane(Search) 받을 때),
debounce 200 ms 후 kebab-app::search 호출, 선택된 hit 의 chunk 를 preview pane 에 표시.
g 키로 $EDITOR (vim/nvim/code/cursor 자동 감지) 에서 citation 위치 열림.

핵심:
- SearchState 본체 (`app.rs` 의 forward decl 채움) — input / mode / hits /
  selected_hit / input_dirty_at / last_query / searching / preview.
- `src/search.rs` (신규):
  - `render_search(f, area, state)` — 3-pane layout (input bar / 결과 리스트 / preview).
    각 hit 는 §1.5 dense 4-line format (rank.score URI / heading / snippet).
  - `handle_key_search`: typing → input + dirty mark. Tab → mode 순환. Enter →
    immediate refresh. j/k → 선택 이동 + preview invalidate. g → editor jump
    (RAII raw-mode suspend). Esc → Library 복귀.
  - `build_jump_command(citation, editor_env, workspace_root)` 가 vim 류
    `+<line> path` / VS Code `code -g path:line` / cursor `cursor -g`
    자동 분기. unit test 로 잠금.
  - `jump_to_citation` 가 raw-mode + AltScreen 을 RAII 로 suspend/restore
    (panic 안전).
  - run-loop hook 4 함수: `debounce_due` / `fire_search` /
    `refresh_preview` (private to crate).
- run.rs:
  - Pane::Search arm 이 `handle_key_search` 로 dispatch + `render_search`.
  - SwitchPane(Search) 시 `app.search = Some(SearchState::default())` lazy init.
  - Idle tick 마다 debounce_due → fire_search, preview None → refresh_preview.
- 테스트 13개 (`tests/search.rs`) — Esc/typing/backspace/Tab cycle/Enter
  refresh/j-k 이동/jump cmd vim+code+args/render w/hits/empty render/no slot.

Spec deviation (HOTFIXES `2026-05-02 P9-2`):
- `render_search<B: Backend>` generic 제거 (P9-1 와 동일 사유 — ratatui 0.28
  Frame backend-agnostic).
- `jump_to_citation` 가 `workspace_root: &Path` 인자 추가. Citation.path 가
  workspace 상대 라 editor 호출 시 절대 경로 필요. spec literal 의 시그니처
  는 unimplementable.

Docs (sync rule):
- README: TUI 행 \"Library + Search 패널, ask/inspect 진행 중\" + Quick start
  의 `kebab tui` 코멘트 갱신.
- HANDOFF: 한 줄 요약 + Phase status (P9 1/5 → 2/5) + deviation 한 줄 추가.
- HOTFIXES: P9-2 entry 추가.
- tasks/p9/p9-2 status: completed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 requested changes 2026-05-02 14:41:15 +00:00
claude-reviewer-01 left a comment
Member

회차 1 — 두 actionable: (1) Citation::Page 분기가 마지막 인자 '# page N' 을 push 해서 vim/code 가 두 번째 파일로 해석 = 의도 외 동작 (issue). (2) j/k 가 SHIFT modifier 도 selection 으로 흡수해 "JSON"/"PostgreSQL" 같은 대문자 검색어 입력 깨짐 (suggestion). 그 외 nit 1 (unused width 인자) + 칭찬 (RAII guard, lazy-init, dense format).

회차 1 — 두 actionable: (1) Citation::Page 분기가 마지막 인자 '# page N' 을 push 해서 vim/code 가 두 번째 파일로 해석 = 의도 외 동작 (issue). (2) j/k 가 SHIFT modifier 도 selection 으로 흡수해 "JSON"/"PostgreSQL" 같은 대문자 검색어 입력 깨짐 (suggestion). 그 외 nit 1 (unused width 인자) + 칭찬 (RAII guard, lazy-init, dense format).
@@ -50,4 +81,4 @@
}
};
match outcome {
KeyOutcome::Quit => app.should_quit = true,

(칭찬) KeyOutcome::SwitchPane(p) 에서 if p == Pane::Search && app.search.is_none() { app.search = Some(SearchState::default()); } 의 lazy-init 한 줄. p9-3/4 가 머지되면 같은 패턴으로 한 줄씩만 추가 (각자 자기 module 의 Default 사용) — 머지 conflict 표면 최소.

(칭찬) `KeyOutcome::SwitchPane(p)` 에서 `if p == Pane::Search && app.search.is_none() { app.search = Some(SearchState::default()); }` 의 lazy-init 한 줄. p9-3/4 가 머지되면 같은 패턴으로 한 줄씩만 추가 (각자 자기 module 의 `Default` 사용) — 머지 conflict 표면 최소.
@@ -0,0 +114,4 @@
}
/// §1.5 dense format — 4 lines per hit:
/// 1. `<rank>. <fusion_score> <path#frag>`

(칭찬) §1.5 dense format (4 lines per hit) 정확히 구현. header / path | section / snippet line 1 / snippet line 2 가 spec literal 그대로. fusion_score 4 자리 소수점 ({:.4}) 까지 spec 문구와 일치 — 사용자가 정확한 점수 비교 가능.

(칭찬) §1.5 dense format (4 lines per hit) 정확히 구현. `header / path | section / snippet line 1 / snippet line 2` 가 spec literal 그대로. fusion_score 4 자리 소수점 (`{:.4}`) 까지 spec 문구와 일치 — 사용자가 정확한 점수 비교 가능.
@@ -0,0 +129,4 @@
let hp = if h.heading_path.is_empty() {
String::from("-")
} else {
h.heading_path.join(" / ")

(nit) format_hit_lines_width: usize 인자가 사용 안 됨 — 현재 ratatui 가 자동 truncate 하므로 호출부에서 width 전달 자체가 dead. 제거하거나 width-aware truncation (truncate_to_display_width 의 search 버전) 으로 활용 둘 중 하나.

Why: lib.rs 에 unicode-width dep 가 있는 것은 wide char (한국어) 의 column 계산 위해. Library 의 format_doc_row 는 사용 — search 도 같은 패턴 적용하면 한국어 path / heading 의 정렬이 안 깨짐.

How to apply (둘 중 택일):

  • (A) _width 인자 제거. ratatui 의 자동 truncate 신뢰.
  • (B) path_line / snippet 에 truncate_to_display_width(line, width) 적용 (현재 library.rs 의 helper 가 pub(crate) 로 노출 가능 — 또는 search.rs 에 동일 함수 복제).

Library 와 일관성 위해 (B) 가 약간 더 좋지만 v1 scope 으론 (A) 도 OK.

(nit) `format_hit_lines` 의 `_width: usize` 인자가 사용 안 됨 — 현재 ratatui 가 자동 truncate 하므로 호출부에서 width 전달 자체가 dead. 제거하거나 width-aware truncation (`truncate_to_display_width` 의 search 버전) 으로 활용 둘 중 하나. Why: lib.rs 에 `unicode-width` dep 가 있는 것은 wide char (한국어) 의 column 계산 위해. Library 의 `format_doc_row` 는 사용 — search 도 같은 패턴 적용하면 한국어 path / heading 의 정렬이 안 깨짐. How to apply (둘 중 택일): - (A) `_width` 인자 제거. ratatui 의 자동 truncate 신뢰. - (B) `path_line` / snippet 에 `truncate_to_display_width(line, width)` 적용 (현재 library.rs 의 helper 가 `pub(crate)` 로 노출 가능 — 또는 search.rs 에 동일 함수 복제). Library 와 일관성 위해 (B) 가 약간 더 좋지만 v1 scope 으론 (A) 도 OK.
@@ -0,0 +195,4 @@
}
};
if has_hits {
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".into());

(suggestion / UX) j / k 의 SHIFT modifier 처리. is_typing_mod 가 CTRL/ALT 만 차단, SHIFT 는 typing 으로 간주. 그러면 J (SHIFT+j) 는 is_typing_mod(SHIFT)=false 통과 → move_selection 분기 매치 → 선택 이동. 사용자가 caps lock + 검색어 "JSON" 입력하면 J 가 selection 내려가고 입력에는 안 들어감.

Why: 검색 입력 중 대문자 자주 발생 (영어 약어 "JSON", 고유명사 "PostgreSQL"). caps-lock 우연 발화 시 무한 좌절.

How to apply: j/k/g 분기에 m == KeyModifiers::NONE 추가 (SHIFT 도 차단). j chord 가 의도하는 vim 식 "j is move" 는 modifier 없는 keypress 만이라는 게 vim 컨벤션과 일치.

(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _) => {
    move_selection(s, 1); ...
}

현재 테스트는 KeyModifiers::NONE 만 검증해서 회귀 안 잡힘 — SHIFT+j 케이스 추가 권장.

(suggestion / UX) `j` / `k` 의 SHIFT modifier 처리. `is_typing_mod` 가 CTRL/ALT 만 차단, SHIFT 는 typing 으로 간주. 그러면 `J` (SHIFT+j) 는 `is_typing_mod(SHIFT)=false` 통과 → `move_selection` 분기 매치 → 선택 이동. 사용자가 caps lock + 검색어 "JSON" 입력하면 J 가 selection 내려가고 입력에는 안 들어감. Why: 검색 입력 중 대문자 자주 발생 (영어 약어 "JSON", 고유명사 "PostgreSQL"). caps-lock 우연 발화 시 무한 좌절. How to apply: `j`/`k`/`g` 분기에 `m == KeyModifiers::NONE` 추가 (SHIFT 도 차단). `j` chord 가 의도하는 vim 식 "j is move" 는 modifier 없는 keypress 만이라는 게 vim 컨벤션과 일치. ```rust (KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _) => { move_selection(s, 1); ... } ``` 현재 테스트는 `KeyModifiers::NONE` 만 검증해서 회귀 안 잡힘 — SHIFT+j 케이스 추가 권장.
@@ -0,0 +321,4 @@
// search target. Best-effort.
args.push("+1".to_string());
args.push(path_str);
args.push(format!("# page {page}"));

(issue / 위험) Citation::Page 분기에서 args.push(format!("# page {page}")) 가 마지막 인자로 들어감. vim/nvim/code 모두 이걸 "열어야 할 두 번째 파일" 로 해석합니다 — 즉 의도와 다른 동작 (# page 7 라는 새 빈 파일을 만들거나 not-found 에러).

Why: 이 분기는 "PDF 인용 best-effort" 이지만 잘못된 인자를 추가하는 건 best-effort 가 아니라 actual harm. 사용자가 PDF 인용 누르면 vim 이 두 파일 (실제 PDF + # page 7 빈 파일) 을 split 으로 열거나 code 가 untitled buffer 를 새로 만듬.

How to apply: 마지막 push 제거. PDF 는 line jump 의 자연스러운 의미가 없으므로 단순히 +1 (또는 line jump 없이 path 만) 으로 충분 — 사용자가 어차피 PDF reader 에서 페이지 직접 navigate 해야 함. tracing::info! 로 "PDF citation, opening at page 1" 정도 hint 만 로그에.

Citation::Page { page, .. } => {
    tracing::debug!(target: "kebab-tui", page, "PDF citation — page jump unsupported by editors, opening file only");
    args.push(path_str);
}

또는 KEBAB_EDITOR_JUMP_FORMAT env (spec § Risks 가 이미 거론) 에 PDF reader 명령 ("pdf=evince -p {page} {path}") 을 받을 길을 P+ 로 남기고 v1 은 path-only.

(issue / 위험) `Citation::Page` 분기에서 `args.push(format!("# page {page}"))` 가 마지막 인자로 들어감. vim/nvim/code 모두 이걸 "열어야 할 두 번째 파일" 로 해석합니다 — 즉 의도와 다른 동작 (`# page 7` 라는 새 빈 파일을 만들거나 not-found 에러). Why: 이 분기는 "PDF 인용 best-effort" 이지만 잘못된 인자를 추가하는 건 best-effort 가 아니라 actual harm. 사용자가 PDF 인용 누르면 vim 이 두 파일 (실제 PDF + `# page 7` 빈 파일) 을 split 으로 열거나 `code` 가 untitled buffer 를 새로 만듬. How to apply: 마지막 push 제거. PDF 는 line jump 의 자연스러운 의미가 없으므로 단순히 `+1` (또는 line jump 없이 path 만) 으로 충분 — 사용자가 어차피 PDF reader 에서 페이지 직접 navigate 해야 함. tracing::info! 로 "PDF citation, opening at page 1" 정도 hint 만 로그에. ```rust Citation::Page { page, .. } => { tracing::debug!(target: "kebab-tui", page, "PDF citation — page jump unsupported by editors, opening file only"); args.push(path_str); } ``` 또는 `KEBAB_EDITOR_JUMP_FORMAT` env (spec § Risks 가 이미 거론) 에 PDF reader 명령 (`"pdf=evince -p {page} {path}"`) 을 받을 길을 P+ 로 남기고 v1 은 path-only.
@@ -0,0 +357,4 @@
let _ = enable_raw_mode();
let _ = execute!(std::io::stdout(), EnterAlternateScreen);
}
}

(칭찬) jump_to_citationRawModeRestore RAII guard 가 _restore 로 바인딩되어 함수 끝 또는 panic 시 Drop 이 raw_mode 재진입 + AltScreen 재진입. 이게 빠지면 사용자가 vim 종료 후 prompt 가 깨진 채 남는 UX 재앙. 명시적 RAII + Drop 안의 let _ = 가 errors-during-recovery 를 silent 처리 — 진짜 panic 정보를 가리지 않으면서 cleanup 만 보장.

(칭찬) `jump_to_citation` 의 `RawModeRestore` RAII guard 가 `_restore` 로 바인딩되어 함수 끝 또는 panic 시 `Drop` 이 raw_mode 재진입 + AltScreen 재진입. 이게 빠지면 사용자가 vim 종료 후 prompt 가 깨진 채 남는 UX 재앙. 명시적 RAII + Drop 안의 `let _ =` 가 errors-during-recovery 를 silent 처리 — 진짜 panic 정보를 가리지 않으면서 cleanup 만 보장.
@@ -0,0 +383,4 @@
/// changed).
pub(crate) fn debounce_due(s: &SearchState) -> bool {
let Some(at) = s.input_dirty_at else { return false };
let elapsed = (time::OffsetDateTime::now_utc() - at)

(칭찬) debounce_dueOffsetDateTime - OffsetDateTime → Durationtry_into 결과를 unwrap_or(Duration::ZERO) 로 받음 — 시계 역행 (NTP 보정 등) 시 Negative Duration 으로 panic 안 함. 작은 invariant 지만 사용자 노트북이 절전모드 후 깨어날 때 정확히 트립할 수 있는 케이스를 사전 차단.

(칭찬) `debounce_due` 가 `OffsetDateTime - OffsetDateTime → Duration` 의 `try_into` 결과를 `unwrap_or(Duration::ZERO)` 로 받음 — 시계 역행 (NTP 보정 등) 시 Negative Duration 으로 panic 안 함. 작은 invariant 지만 사용자 노트북이 절전모드 후 깨어날 때 정확히 트립할 수 있는 케이스를 사전 차단.
altair823 added 1 commit 2026-05-02 14:43:11 +00:00
1. **Citation::Page 분기 fix** — `args.push(format!(\"# page {page}\"))` 가
   vim/code/cursor 에 \"두 번째 파일\" 로 해석돼 의도 외 동작 (split / new
   buffer). 마지막 push 제거, path 만 열고 `tracing::debug!` 한 줄.
   PDF 페이지 jump 는 사용자 PDF reader 책임 — `KEBAB_EDITOR_JUMP_FORMAT`
   env hook 은 P+ enhancement.
2. **j/k/g 의 SHIFT modifier 차단** — `is_typing_mod` 가 SHIFT 를 typing
   으로 취급하던 부분이 J/K/G 를 selection 키로 흡수해 \"JSON\" / \"PostgreSQL\"
   / \"Go\" 같은 대문자 검색어 깨짐. arrow 키 (Down/Up) 는 modifier 무관 유지,
   문자 키 (j/k/g) 는 `KeyModifiers::NONE` 만. SHIFT-J / SHIFT-G 회귀 테스트
   2건 추가.
3. **`format_hit_lines` 의 unused `_width` 인자 제거** — ratatui 자동
   truncate 신뢰 (Library 의 한국어 column 정렬은 별도 path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 approved these changes 2026-05-02 14:43:37 +00:00
claude-reviewer-01 left a comment
Member

회차 2 — 회차 1 지적 3건 (PDF citation extra arg / SHIFT modifier 차단 / unused width param) 모두 반영. SHIFT-J / SHIFT-G 회귀 테스트 2개 추가. 25 tests pass + clippy clean. 머지 가능. P9-2 완료 → 사용자 / 키 활성, ask/inspect 패널 후속.

회차 2 — 회차 1 지적 3건 (PDF citation extra arg / SHIFT modifier 차단 / unused width param) 모두 반영. SHIFT-J / SHIFT-G 회귀 테스트 2개 추가. 25 tests pass + clippy clean. 머지 가능. P9-2 완료 → 사용자 / 키 활성, ask/inspect 패널 후속.
@@ -0,0 +197,4 @@
}
};
if has_hits {
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".into());

(칭찬) j/k 와 Down/Up 의 modifier 처리 분리. 문자 키는 KeyModifiers::NONE 만 (vim convention + SHIFT-J 가 "JSON" 의 J 로 들어가게), 화살표 키는 modifier 무관 (typing collision 없음). 두 분기로 나뉘어 의도가 코드 모양에 직접 표현됨.

(칭찬) j/k 와 Down/Up 의 modifier 처리 분리. 문자 키는 `KeyModifiers::NONE` 만 (vim convention + SHIFT-J 가 "JSON" 의 J 로 들어가게), 화살표 키는 modifier 무관 (typing collision 없음). 두 분기로 나뉘어 의도가 코드 모양에 직접 표현됨.
@@ -0,0 +317,4 @@
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| program.clone());

(칭찬) Citation::Page 분기가 path 만 열고 tracing::debug! 한 줄로 의도 명시. 코멘트가 "왜 마지막 push 를 안 하는지" (# page N 가 vim/code/cursor 에서 두 번째 파일로 해석) 를 정확히 박아둠 — 미래 contributor 가 "PDF 페이지 jump 추가하자" 라고 시도하면 코멘트 + KEBAB_EDITOR_JUMP_FORMAT P+ 힌트가 즉시 답.

(칭찬) Citation::Page 분기가 path 만 열고 `tracing::debug!` 한 줄로 의도 명시. 코멘트가 "왜 마지막 push 를 안 하는지" (`# page N` 가 vim/code/cursor 에서 두 번째 파일로 해석) 를 정확히 박아둠 — 미래 contributor 가 "PDF 페이지 jump 추가하자" 라고 시도하면 코멘트 + KEBAB_EDITOR_JUMP_FORMAT P+ 힌트가 즉시 답.
@@ -0,0 +242,4 @@
terminal
.draw(|f| {
let area = Rect::new(0, 0, 80, 20);
render_search(f, area, &app);

(칭찬) shift_j_stays_in_input_does_not_move_selection + shift_g_does_not_trigger_editor_jump 두 회귀 테스트가 "왜 R1 의 SHIFT 차단이 필요한지" 정확히 표현. 미래에 누군가 modifier 처리를 "단순화" 시도하면 즉시 빨개짐. 테스트 이름이 invariant 그대로 — naming 자체가 문서.

(칭찬) `shift_j_stays_in_input_does_not_move_selection` + `shift_g_does_not_trigger_editor_jump` 두 회귀 테스트가 "왜 R1 의 SHIFT 차단이 필요한지" 정확히 표현. 미래에 누군가 modifier 처리를 "단순화" 시도하면 즉시 빨개짐. 테스트 이름이 invariant 그대로 — naming 자체가 문서.
altair823 merged commit 0bcc4c5132 into main 2026-05-02 15:05:47 +00:00
altair823 deleted branch feat/p9-2-tui-search 2026-05-02 15:06:05 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#44