diff --git a/docs/incremental-search-framing.md b/docs/incremental-search-framing.md new file mode 100644 index 0000000..58b0561 --- /dev/null +++ b/docs/incremental-search-framing.md @@ -0,0 +1,123 @@ +# Incremental in-buffer search — framing pass + +Date: 2026-06-15. The last deferred GUI item: `SearchMatch` / +`SearchMatchActive` decorations exist on the wire (message.rs) and are +gated to `None` in both frontends "waiting on a search feature." This +builds that feature. Decided up front (user): **incremental isearch** +(highlight live as you type, same key steps to next, Enter accepts, +Esc/C-g restores origin) with **smart-case substring** matching +(case-insensitive unless the query has an uppercase letter). + +## Survey facts (anchors) + +- Greenfield in-buffer search; only `project.search` (cross-file grep) + exists. No `Buffer::find` / rope search. +- `DiagnosticStore` (diag.rs) is a near-exact template: keyed + `Arc` store, sorted entries, `next_after`/`previous_before`, + stale tracking, Lua nav bindings, overlay `_attach_view`. +- Producer `scoped_decorations` (semantic_render.rs) + TUI + `DiagnosticView::render` are the emit/paint templates (viewport clip, + line-start cache, stale-skip). +- GPU `decoration_kind_to_bg_color` already draws bg decorations + through the quad pipeline; SearchMatch/Active just need their arms. +- The minibuffer (minibuffer.rs) is keystroke-driven and pseudo-modal + (dispatch_minibuffer_key intercepts all keys while active) but has + **no live-preview/on_changed hook** — the one missing piece for + incremental highlight. + +## Q#SR1 — store shape & ownership + +**Stance: a per-buffer `SearchStore` mirroring `DiagnosticStore`.** +`by_buffer: HashMap` where `SearchState` holds +the resolved query, the sorted `Vec` matches, and the +active index. Shared `Arc`. The active index lives on the store +(navigation state), not per-window — v1 accepts that two windows on +the same buffer share the active highlight (note it; selection is the +per-window concept, search mirrors diagnostics). Edits mark the +buffer's entry stale (M11.8 model) so matches at pre-edit byte +positions aren't painted until re-search. + +## Q#SR2 — search primitive + +**Stance: smart-case substring over a rope snapshot, regex deferred.** +`find_all(haystack, query) -> Vec`: case-insensitive unless +`query` contains an uppercase char (then exact). Built on +`snapshot_rope().slice` bytes (the diagnostics path's cheap snapshot). +Recomputed on query change; invalidated on edit. No `regex` crate in +v1 (literal-text is the 95% case; regex is a later toggle). Overlapping +matches: advance past each match's start+1 (standard non-overlapping). + +## Q#SR3 — decoration emission + +**Stance: mirror the diagnostics producer.** In `scoped_decorations`, +read the search store for the viewport buffer, emit `SearchMatch` for +every visible match and `SearchMatchActive` for the active one +(emitted last / higher z so it wins the overlap). Reuse the line cache ++ `clip_to_viewport`. Stale-skip exactly like diagnostics. TUI gets a +`SearchView` overlay (mirrors `DiagnosticView`) painting bg, attached +via `pmacs.search._attach_view`. + +## Q#SR4 — colors + +**Stance: a single search palette.** SearchMatch = translucent yellow +wash; SearchMatchActive = stronger amber/orange. GPU: the two +`decoration_kind_to_bg_color` arms. TUI: reverse-ish colored bg in the +`SearchView`. Distinct from selection (blue) and diagnostics +(severity). + +## Q#SR5 — input & modality (incremental) + +**Stance: host the query in the minibuffer + a small `on_changed` +hook + targeted next/prev interception.** Entry opens a minibuffer +search session (prompt `I-search: `); `on_changed` (new optional +session callback, fired after each content mutation in +dispatch_minibuffer_key) recomputes matches → updates the store → +re-decorate. While that session is active, the entry chord again = +`search.next`, its shift/`C-r` variant = `search.prev` (control keys, +not self-insert, so safe to intercept in the search branch of +dispatch_minibuffer_key). `Enter` accepts (close, leave cursor at the +active match); `Esc`/`C-g` cancels (close, restore the origin cursor +saved at entry, clear the store). Reusing the minibuffer's input +editing + prompt avoids reimplementing a modal query line. + +## Q#SR6 — navigation + +**Stance: `search.next`/`search.prev` mirror `diag.next/prev`.** +Advance the active index with wrap, move the active window's cursor to +the active match start, scroll it into view. The active index drives +which match is `SearchMatchActive`. Usable both during the live +session and afterward (matches persist until cleared / next search). + +## Binding (proposal, flagged for veto) + +`C-f` → search (CUA "Find"), rebound from `cursor.right`. Consistent +with the editor's CUA direction (arrows move; Ctrl+F finds); the +Emacs-holdover `C-f = forward-char` is the inconsistent one. Easy to +change — call out in validation. + +## Predicted findings (categorical bets) + +1. **Stale-after-edit linger** (the squiggle lesson again): matches at + pre-edit byte positions paint over shifted text until re-search — + the store's stale gate + re-search-on-change must be right, or + highlights drift during typing. +2. **Minibuffer `on_changed` × completion**: the hook interacts with + the existing per-keystroke candidate recompute; the search session + must opt out of completion cleanly (a session "kind" seam). +3. **Per-buffer active match across windows** surfaces as navigating + in one window moving the active highlight in another — accepted for + v1, but worth eyeballing. +4. **Empty / all-match queries**: empty query → no matches (not all); + a 1-char common letter → many matches → viewport-clipped emission + must stay cheap (line cache + only-visible). + +## Session plan + +Three green commits: +1. Core `SearchStore` + `find_all` smart-case primitive + unit tests. +2. Producer emission + GPU bg colors + TUI `SearchView` + attach. +3. Incremental UX: minibuffer `on_changed`, search session, + next/prev interception + commands, cancel-restores-origin, binding. + +Manual validation gate as usual (type to highlight live, step matches, +edit mid-search, Esc restores).