docs: GPU minibuffer framing + as-built (Q#MB)
Framing + as-built record: the render-only architecture, the v12 wire, the band prompt line + caret, the candidate dropdown, opening-chord forwarding, the delivered phasing, and an "As-built divergences" section (monospace caret, a third TextRenderer, best-at-top dropdown ordering) plus the categorical bets that held. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
This commit is contained in:
parent
b9bd231e64
commit
e0f7ff6eca
|
|
@ -0,0 +1,161 @@
|
||||||
|
# GPU minibuffer — framing + as-built
|
||||||
|
|
||||||
|
pmacs-gpu couldn't render the minibuffer, so `M-x`, find-file,
|
||||||
|
switch-buffer, and the LSP rename prompt were invisible in the GUI —
|
||||||
|
both prior arcs (search, context menu) had to route *around* this gap.
|
||||||
|
This arc closed it. The build landed close to the framing (the third
|
||||||
|
surface in the `SearchPrompt` / `MenuPrompt` family); the "As-built"
|
||||||
|
notes record where it differed.
|
||||||
|
|
||||||
|
Decided (with the user):
|
||||||
|
|
||||||
|
- **Render-only.** The minibuffer logic already lived entirely in the
|
||||||
|
core; the GPU *already round-tripped keys* while one was open
|
||||||
|
(`dispatch_idle` goes false). So this was a wire-message + GPU
|
||||||
|
render-surface arc — no new input logic.
|
||||||
|
- **Vertical dropdown** for candidates (Vertico / Telescope style), not
|
||||||
|
the TUI's inline `[selected]`. The GPU leads the TUI here; the
|
||||||
|
cross-frontend divergence is accepted (discussed against how Emacs and
|
||||||
|
Neovim converged on a vertical list).
|
||||||
|
|
||||||
|
## What the core already exposed
|
||||||
|
|
||||||
|
A single **global** `EditorCore::minibuffer` (not per-buffer). While a
|
||||||
|
prompt is open, `MinibufferSession` carries everything a renderer needs:
|
||||||
|
`prompt` (e.g. `"M-x "`), the typed `input` (`minibuffer.contents()`),
|
||||||
|
the `cursor` (byte offset), `candidates: Vec<String>` (already
|
||||||
|
filtered + fuzzy-sorted best-first, plain strings), and
|
||||||
|
`selected: Option<usize>`. Input is fully handled by
|
||||||
|
`MinibufferAction::from_chord` (RET accept, TAB complete, `C-g`/Esc
|
||||||
|
cancel, Up/Down history, `M-n`/`M-p` cycle candidates, motion,
|
||||||
|
self-insert). These fields are all public, so the producer reads them
|
||||||
|
directly — **the core was untouched**, exactly the family bet.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Q#MB1 — A `MinibufferPrompt` semantic message, mirroring the family
|
||||||
|
|
||||||
|
`InstanceMessage::MinibufferPrompt` (protocol v12), produced by
|
||||||
|
`semantic_render::minibuffer_prompt_msg` with the same cached-compare
|
||||||
|
suppression as `search_prompt_msg` / `menu_prompt_msg`, daemon-gated
|
||||||
|
`>= 12`. The GPU mirrors it into a `MinibufferLocal` and renders. The
|
||||||
|
minibuffer is global, so the message is **bufferless** — the producer
|
||||||
|
caches a single value (not a per-buffer `HashMap`) and emits only from
|
||||||
|
the active-buffer viewport so the bufferless message ships once per
|
||||||
|
frame.
|
||||||
|
|
||||||
|
### Q#MB2 — The prompt line lives in the bottom band
|
||||||
|
|
||||||
|
When the minibuffer is open, `compose_status_left` returns exactly
|
||||||
|
`prompt + input`, taking over the band ahead of the search prompt and
|
||||||
|
status. The buffer caret is hidden; a **band caret** quad draws at the
|
||||||
|
input cursor. **As-built:** the caret x uses the band font's monospace
|
||||||
|
advance — the shaped status-left width ÷ its char count, times
|
||||||
|
`prompt_chars + cursor` — rather than a per-glyph measurement (Q#MB4).
|
||||||
|
Exact for the ASCII command names / filenames that dominate; a
|
||||||
|
multibyte-exact caret is deferred.
|
||||||
|
|
||||||
|
### Q#MB3 — The candidate dropdown floats above the band
|
||||||
|
|
||||||
|
A vertical popup anchored just **above** the band at the input's left
|
||||||
|
edge, **growing upward, best match at the top**, the `selected` row
|
||||||
|
highlighted. **As-built:** it's a *third* `TextRenderer`
|
||||||
|
(`mb_text_renderer`) over bg/selection quads — the menu's popup pattern,
|
||||||
|
reusing the menu's colors — not "a second" renderer (the menu already
|
||||||
|
owns one). It only appears when there are candidates, so free-form
|
||||||
|
prompts (`project.search`, rename) stay a single line. The list is a
|
||||||
|
**scrolled window**: the producer sends a bounded slice
|
||||||
|
(≤ `MB_VISIBLE` = 10) around `selected` plus `total`, so a
|
||||||
|
1000-command `M-x` ships ~10 strings per keystroke, not 1000, and the
|
||||||
|
selected row stays in view as you cycle.
|
||||||
|
|
||||||
|
### Q#MB4 — Cursor as a codepoint offset
|
||||||
|
|
||||||
|
The message carries `cursor` as the count of codepoints before the
|
||||||
|
cursor in `input` (computed in the producer via `char_indices`). See
|
||||||
|
Q#MB2 for how the GPU turns it into the band caret x.
|
||||||
|
|
||||||
|
### Q#MB5 — Forwarding the chords that *open* the minibuffer
|
||||||
|
|
||||||
|
The GPU withholds command chords by default, so `M-x` and the `C-x`
|
||||||
|
prefix never reached the daemon — the minibuffer couldn't even be
|
||||||
|
opened. `is_minibuffer_open_chord` now forwards them: `M-x`
|
||||||
|
(→ execute-command, from which any command incl. find-file /
|
||||||
|
switch-buffer is reachable by name) and the **`C-x` prefix** (so the
|
||||||
|
bound `C-x b` / `C-x C-f` work). Both flip the daemon into a state
|
||||||
|
(`minibuffer active` / `pending prefix`) that makes `dispatch_idle` go
|
||||||
|
false, after which the intercept gate round-trips every key. Mirrors
|
||||||
|
`is_search_entry_chord` / `is_clipboard_chord`; no optimistic local flip
|
||||||
|
(the search precedent). General Emacs-chord forwarding stays a separate
|
||||||
|
thread; this arc forwarded only what opens a prompt.
|
||||||
|
|
||||||
|
## Wire (protocol v12)
|
||||||
|
|
||||||
|
Additive over v11; `SUPPORTED = [6..12]`:
|
||||||
|
|
||||||
|
```
|
||||||
|
InstanceMessage::MinibufferPrompt {
|
||||||
|
prompt: Option<String>, // None = minibuffer closed (clears the GUI)
|
||||||
|
input: String,
|
||||||
|
cursor: u32, // codepoints before the cursor in `input`
|
||||||
|
candidates: Vec<String>, // windowed slice (≤ MB_VISIBLE)
|
||||||
|
selected: Option<u32>, // highlighted row *within* `candidates`
|
||||||
|
total: u32, // full candidate count
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Cached-compare suppressed like `SearchPrompt`; first sight while closed
|
||||||
|
stays silent. Daemon-gated `>= 12`. The TUI ignores the variant (it
|
||||||
|
paints the minibuffer via its own bottom row). The whole shape (incl.
|
||||||
|
candidates) landed at v12, so the dropdown phase needed no second bump.
|
||||||
|
|
||||||
|
## Phasing (delivered; each commit binary-build-green)
|
||||||
|
|
||||||
|
1. **Wire + prompt line** — protocol v12 + producer + daemon gate + GPU
|
||||||
|
handler + the band prompt line (prompt + input + caret) + the
|
||||||
|
opening-chord forwarding (`M-x`, `C-x`). `M-x` opens, typing works,
|
||||||
|
RET runs. User-validated.
|
||||||
|
2. **Candidate dropdown** — GPU-render-only: the vertical popup above the
|
||||||
|
band, consuming the candidates already on the wire. User-validated.
|
||||||
|
3. **Docs** — this consolidation.
|
||||||
|
|
||||||
|
Phase 1 made the minibuffer *work* in the GUI; phase 2 made it
|
||||||
|
*pleasant*. The v12 bump means the daemon + pmacs-gpu must both be
|
||||||
|
rebuilt to negotiate it.
|
||||||
|
|
||||||
|
## Categorical bets (all held)
|
||||||
|
|
||||||
|
- **The family pattern generalized a third time.** `MinibufferPrompt` +
|
||||||
|
the band/popup surfaces dropped in as the same shape as search and the
|
||||||
|
menu; the producer/cached-compare/gate machinery reused cleanly and
|
||||||
|
the core stayed untouched.
|
||||||
|
- **A windowed candidate slice was enough.** ~10 around the selection
|
||||||
|
keeps the wire cheap and the dropdown correct as you cycle.
|
||||||
|
- **Forwarding only the opening chords was the right cut.** `M-x` + `C-x`
|
||||||
|
make the minibuffer reachable without the general chord-forwarding can
|
||||||
|
of worms.
|
||||||
|
|
||||||
|
## As-built divergences from the framing
|
||||||
|
|
||||||
|
1. **Caret via monospace advance** (Q#MB2/Q#MB4), not a per-glyph width
|
||||||
|
measurement — simpler, exact for the ASCII case that dominates.
|
||||||
|
2. **A third `TextRenderer`** for the dropdown, not "a second" — the menu
|
||||||
|
already owns its own popup renderer; the dropdown got a dedicated one
|
||||||
|
and reuses the menu's colors.
|
||||||
|
3. **Dropdown ordering pinned to best-at-top, growing upward** — the
|
||||||
|
framing left it unspecified; top-to-bottom reading with `M-n` moving
|
||||||
|
the highlight down was the least surprising.
|
||||||
|
4. **`total` ships but isn't rendered** — the "i/total" hint is deferred
|
||||||
|
(below).
|
||||||
|
|
||||||
|
## Deferred (named, not silently dropped)
|
||||||
|
|
||||||
|
- General Emacs-chord forwarding in the GPU (every binding, not just the
|
||||||
|
prompt-openers) — the separate thread this arc brushes against.
|
||||||
|
- The "i/total" count hint and a Telescope-style preview pane beside the
|
||||||
|
list.
|
||||||
|
- Candidate annotations (kind / docstring) — the core's candidates are
|
||||||
|
bare strings today.
|
||||||
|
- Bringing the TUI's inline `[selected]` up to the same dropdown (unify
|
||||||
|
the frontends); for now the GPU leads.
|
||||||
|
- Multibyte-exact caret positioning in the band.
|
||||||
Loading…
Reference in New Issue