Incremental buffer search for escriba — vim's / and ?.
What this is
The whole search feature as a pure function of (text, pattern, cursor).
There is no I/O here: no buffer handle, no renderer, no clock. That is
deliberate and is what the ★★ default delivery method asks for — a pure core
split from side effects. Search happens to need no Environment seam at
all, because it genuinely has no effects to inject, so every behaviour below
is provable by a plain unit test with no mock and no fixture.
The caller owns the buffer and the cursor; this crate answers "where should the cursor go" and "which ranges should light up".
Feature parity with vim
| vim | here |
|---|---|
/pat ?pat |
[SearchState::open] + [SearchState::accept] |
| incremental highlight while typing | [SearchState::preview] |
n / N |
[SearchState::repeat] |
* / # |
[SearchState::search_word] |
hlsearch / :noh |
[SearchState::highlights] / [SearchState::clear_highlight] |
ignorecase / smartcase |
[CaseMode] |
| wrap + "hit BOTTOM" message | [Wrapped] |
bare /<CR> repeats last |
[Accepted::ReusedPrevious] |
| search history + arrows | [SearchState::history_step] |
| regex patterns | [SearchPattern::compile] |
Two invariants worth knowing
Offsets are chars, never bytes. regex reports bytes; escriba's buffer
is char-addressed. The conversion happens once, inside [find_all], so no
consumer can mix them. A byte/char mix-up is invisible in ASCII tests and
wrong on the first accented character — [engine] tests it directly.
A prompt and a committed search are different things. Cancelling /xyz
must not disturb the foo you were already searching. That falls out of the
type — the prompt is an Option<Prompt> holding its own text and origin, so
dropping it cannot touch the committed pattern.