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