feat: add opt-in automatic pair insertion, with a highlighter veto - #1135
Merged
kronberger-droid merged 16 commits intoSep 1, 2026
Conversation
Auto-pairing had no way to consult the surrounding syntax, so language specific rules such as "do not pair a quote inside an unterminated string" could not be expressed and downstream projects had to work around it at the keybinding layer. Add `Highlighter::should_auto_pair`, a defaulted veto that mirrors the existing `should_expand_abbr` hook: consumers already own their parser in the highlighter, and reusing that seam avoids a second, parallel policy channel with its own precedence rules. The veto receives an `AutoPairContext` carrying the buffer, the insertion point, the pair being acted on, the selection and which of the three actions (`Open`, `SkipExistingCloser`, `BackspacePair`) is pending. Returning `false` runs the originally typed command verbatim. Resolving the action now happens before any conversion, so all three actions pass through the same gate. Previously skip-over short circuited first and could not be suppressed at all. `AutoPairs` keeps its shape and derives, and no new builder is added. Behaviour is unchanged for existing users: the default implementation returns true, and a highlighter that does not override it auto-pairs exactly as before.
Extend the `with_auto_pairs` documentation the way `with_abbreviations` points at `should_expand_abbr`: say that auto-pairing applies everywhere by default and that `Highlighter::should_auto_pair` is how to suppress it. Also document two things that were previously implicit: for `InsertChar` the closer is looked up before the opener, so a character registered on both sides of two different pairs resolves by what sits at the cursor rather than by registration order; and this builder deliberately does not touch the terminal's bracketed paste setting, which matters because without bracketed paste a pasted opener is auto-paired like a typed one. Rewrite the example around a highlighter that implements the rules an R console actually uses, so it shows a real policy rather than a toy one.
The context derived its selection by comparing the selection anchor against the insertion point. Under a forward vi-normal block selection those two disagree: the caret sits one grapheme behind the widened head, so the range handed to the veto dropped the last selected grapheme while `insert_pair` went on to wrap the full range from `Editor::get_selection`. Use `get_selection` for the context as well, so a policy sees exactly the range the edit will affect.
The example's quote detector scanned for one quote character at a time, so a quote sitting inside a string of the other kind counted as a delimiter: after `'foo"` it reported an unclosed double-quoted string and suppressed pairing another `"`. Track both kinds at once, the way `ExampleHighlighter::should_expand_abbr` already does, so each kind is inert while the other string is open.
It is minimal on purpose and carries no tests; a consumer should answer this from the parser it already owns, which is why the veto sits on the highlighter in the first place.
eitsupi
marked this pull request as ready for review
August 23, 2026 05:05
Contributor
Author
|
I've replaced arf's auto-pair feature in eitsupi/arf#330. |
Collaborator
|
Nice I will take a look and test it out. |
…text-veto # Conflicts: # src/engine.rs
The `helix` feature flag is gone from the crate, so gating this test on it silently excluded it from every build. Drop the gate.
Collaborator
|
@eitsupi
Thanks again, nice work! |
Collaborator
|
Also helix seems to be missing the no veto test:
|
Contributor
Author
|
Updated.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This adds opt-in automatic pair insertion to reedline, continuing PR #1114 by @altacountbabi. Its two commits are preserved unchanged so the original author keeps authorship.
with_auto_pairsenables it; otherwise behaviour is unchanged. Once enabled:The feature needs context-aware vetoes. Character-only logic cannot handle consumer-specific cases such as quotes inside unterminated strings. Downstream applications otherwise need keybinding-layer workarounds based on shadow editor state, which can drift from reedline's real buffer after:
The veto is on
Highlighter, following theshould_expand_abbrprecedent from #1094. Highlighters already own consumer parser state, so this avoids another policy channel. The API and bundled example remain consumer-neutral; language-specific policy stays downstream. If maintainers prefer a separate hook or builder, I can move it.Public API. All of this is new and unreleased.
should_auto_pairdefaults totrue, so existing highlighters compile unchanged. The new commands also expand the strum discriminants used for keybinding configuration.Before
reedline has no automatic pair insertion.
After
Auto-pairing runs in the engine against the real buffer and cursor. Pair insertion, closer skip-over, and empty-pair deletion are each resolved before
should_auto_pair; returningfalseexecutes the original command unchanged.Additional notes
How this differs from #1114 as it stands
Highlighter::should_auto_pairgates all three actionswith_auto_pairsdocuments the bracketed-paste guardInsertCharchecks the closer before the openerTests
The focused engine-level suites cover independent vetoes and exact fallback commands, decision order, selections, undo grouping, same-character and overlapping pairs, multiline and CRLF buffers, vi and helix replays, balanced or unbalanced
Event::Paste, and reverse history search. The context-policy suite also covers allowing non-quote pairs inside an unterminated region and vetoing the matching quote delimiter.The latest regression additions cover two state transitions that previously caused downstream auto-pair bugs:
They assert behaviour from the live editor buffer and cursor; they do not track which characters auto-pairing inserted and contain no downstream language policy.
Local focused checks after the latest upstream
mainmerge:cargo test --all-features auto_pair --lib— 33 passedcargo test --all-features context_policy --lib— 2 passedcargo fmt --checkcargo clippy --all-targets --all-features -- -D warningsenv -u NO_COLOR cargo test --all-features -- --test-threads=1— 1635 unit tests passed, 1 ignored; 30 doc tests passedThe Helix insert-mode positive path is covered alongside the existing Vi test, and the Helix veto test runs unconditionally now that the crate no longer has a
helixfeature flag.Downstream validation
arf PR #330 now uses this API from the pushed git revision. Its R-specific decision remains in arf's highlighter; reedline contains no arf-specific behavior.
The downstream migration removes the shadow-buffer keybinding workaround. The arf PTY suite covering history replacement, pair deletion, newline deletion, raw strings, pasting, and history menus passes with the new implementation (18 passed). The new history/newline regressions were also added on a branch based on arf
mainand passed against the old implementation before being merged into the feature branch, so they pin existing user-visible behaviour rather than defining behaviour around this implementation.Pasting
with_auto_pairsdoes not change terminal settings. With bracketed paste, paste arrives asEditCommand::InsertStringand is not auto-paired. Without it, pasted input is indistinguishable from typing, so an unmatched opener can gain a closer.The example enables bracketed paste except on Windows, matching nushell. Stock crossterm on Windows has no
Event::Pastepath, so this remains unsolved there. I would rather not havewith_auto_pairsenable bracketed paste implicitly: reedline cannot tell at compile time whether the terminal supports it.Two behaviours worth your opinion
UndoBehavior::MoveCursor, so it creates no undo boundary. Undoing after typing(then)returns to an empty buffer.Follow-up
@fdncred asked on #1114 for a nushell-side toggle. I can add one once this API shape is settled.
Not included
HistoryCompletercan leave trailing text behind when a history suggestion is selected with the cursor mid-buffer. This is independent of auto-pairing and will be a separate PR.