framing: incremental in-buffer search (smart-case isearch)
Decided: incremental isearch (live highlight, same key steps next, Enter accepts, Esc restores origin) + smart-case substring. Q#SR1 per-buffer SearchStore mirroring DiagnosticStore; Q#SR2 smart-case find_all over a rope snapshot (regex deferred); Q#SR3 producer + TUI SearchView mirror the diagnostics path; Q#SR4 yellow/amber palette; Q#SR5 minibuffer-hosted query + new on_changed hook + next/prev interception; Q#SR6 navigation mirrors diag.next/prev. Four bets (stale-after-edit linger the headline). Three-commit arc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4d63f21905
commit
d45b064ab2
|
|
@ -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<Mutex>` 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<BufferId, SearchState>` where `SearchState` holds
|
||||
the resolved query, the sorted `Vec<ByteRange>` matches, and the
|
||||
active index. Shared `Arc<Mutex>`. 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<ByteRange>`: 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).
|
||||
Loading…
Reference in New Issue