edamame 0.1.3

A fast TUI Markdown editor and viewer
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
# Live `:s` substitution preview (vim `inccommand`)

> Part of the edamame contributor deep-dives. Index and project-wide conventions: [`AGENTS.md`](../../AGENTS.md). Sibling docs live in [`docs/dev/`](.).


- **The preview transiently rewrites the real buffer — through raw `Buffer` edits only.** While the vim `:` line holds a complete-enough `:s`/`:%s`/`:'<,'>s`, `vim_ops::preview::update_substitute_preview` applies it via raw `Buffer::insert`/`remove` (never `apply_delta`), so no undo delta is recorded and `dirty` is untouched. Every keystroke reverts the previous preview and recomputes against the pristine buffer — never diff two previews. On Enter the reducer reverts *before* `submit_ex`, so commit semantics stay byte-identical to a preview-less submit. `SubstitutePreview` lives on `EditorState` (like `search` / `yank_flash`) so painters read it off `&EditorState`.
- **The revert delta is version-stamped as a fail-safe** — a revert on a mismatched version silently drops the preview instead of corrupting text, and `replace_buffer` drops any preview. Don't rely on the stamp: any new mutation path reachable while the cmdline is open must be gated.
- **Three gates hold while `substitute_preview.is_some()`.** (1) `tick_autosave` / `autosave_deadline` skip entirely — raw preview edits bump `version` without touching `dirty`, so on an already-dirty buffer the debounce would write preview text to disk. (2) `dispatch_mouse_event` blocks everything but wheel scroll and pointer moves (shares the capturing-search gate) — a click or checkbox toggle would mutate text that is about to revert. (3) `prepare_viewport` skips `ensure_search_fresh` and both search-overlay painters early-out, since a coexisting hlsearch session's ranges are stale against preview text (the session survives and repaints after the revert). Keyboard needs no gate — the cmdline captures every key. Additionally `cursor_block_revealed()` returns false while a preview is active: it parks the cursor on the first affected line, and the reveal delay would elapse mid-typing.
- **A `:s` pattern sees the whole resolved range at once, not one line at a time.** `ex::region_haystack` materializes lines `first..=last` as one string, and `ex::for_each_region_match` is the *single* match-finding implementation driving the commit path, the replacement preview and the highlight-only preview — which makes "what the preview highlights" and "what Enter replaces" the same set by construction. Four load-bearing details: (1) **`multi_line(true)` at both compile sites** (`execute_substitute` and `compute_preview_plan`) keeps `^`/`$` anchoring per line — setting it on one and not the other makes the preview lie; `dot_matches_new_line` stays off so `.` refuses to cross a break. (2) **The region excludes the last line's own break**, which is the entire enforcement of the range bound: a `\n` pattern can never reach past the named lines. That costs one deliberate divergence from vim (a single-line `:s/\n//` can't join with the next line); `:%s` resolves `last` to ropey's phantom line, so it *can* consume the file's final newline. (3) **The non-`g` walk replaces the first match *starting on* each line**, resuming at the line after the last line the match covered, with a correction for a match ending exactly at a line start (any pattern ending in `\n`) — without which every other line is skipped. (4) **`match_cap` stops on a match boundary, not a line boundary**, so the preview's `removed` stays a verbatim prefix of the region. Don't reintroduce a per-line `substitute_line`.
- **Compute is pure and shared with the commit path.** `ex::build_substitution` produces the single `EditDelta` plus post-apply ranges of each inserted segment; `preview::compute_preview_plan` is a pure function of `(Buffer, cursor_line, Substitution, visual_range)` — the unit-test seam. `Substitution::replacement_present` (did the user type the second delimiter?) distinguishes highlight-only `:%s/foo` (ranges, first per line without `g`, no edit) from deletion preview `:%s/foo/` (edit applied, zero-width ranges filtered).
- **The preview regex is bounded; the commit regex is not.** The preview compiles `fancy-regex` with `backtrack_limit(100_000)` and caps the walk at 1 000 matches (later lines stay original until submit), so a pathological half-typed pattern fails fast per keystroke. Parse/regex errors and matchless patterns silently end the session — never flash an error mid-typing. Painting reuses the search walk (`paint_substitute_preview_overlays`, an inline branch in `RawView`), all ranges in `theme.selection` — no focus concept, matching nvim's one `Substitute` group.