fix: pair a delimiter run typed into literal text with the run that text opens - #346
Merged
Merged
Conversation
This was referenced Sep 1, 2026
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
A
*or_run typed into literal text now pairs with the run that same text opens, over as many delimiters as the shorter of the two spells, leaving the surplus literal.***text*reads as two literal asterisks before italic text,_**text**as a literal underscore before bold text, and_**text**_as italic bold.The preset input rules read one content group between delimiters of equal length, so a run closed against an unequal or differently spelled one matched nothing and stayed literal.
_**text**failed for a second reason: the strong rule's guard counts_as a word character, where CommonMark counts it as punctuation.The pass that already answers a delimiter typed beside a mark now answers one typed beside literal text as well. It acts only where the preset declined, because a rule that matches consumes the typed character before the plugin sees it, so the preset's word, colon, and slash guard is reproduced rather than replaced and
lead**text**still types literal.Where the closing run is the shorter one, the pair is read once the caret leaves that run rather than as the character lands. The rows this issue lists are continuations of each other —
_**text*is a prefix of_**text**— and an input rule reads only the text before the caret, so firing on the keystroke serves one row at the cost of the other. Deferring serves both, and is how typed link source already reaches the object it spells.Three constraints the pairing had to meet, each measured rather than assumed:
_**text**_committed as_**text_**.**a*b**reached the projection commit path and lost two typed delimiters, which is worse than the literal text it replaced.Related Issue
Closes #232
Verification
attentionPairing.test.tsgains atyping a delimiter run into literal textblock. Ten rows type a run into an empty paragraph and assert the document, the saved Markdown, and the document the saved file reopens as; each row also asserts that stripping the backslashes from the saved Markdown reproduces exactly the characters typed, which is whereNo input inserts or duplicates a delimiter character that was not typedis pinned. Four further tests cover the behaviors that carry the fix: a run held until the caret leaves it and then extended into bold, a run closed against a letter left literal, a run following a word left literal, and an unequal tilde run left literal.Measured by driving the editor mount across a twenty-six input matrix and comparing the document and saved bytes before and after: three inputs beyond the issue's table change behavior, and each moves to the reading CommonMark gives the same source —
****text*,____text_, and***text* rest. Nothing else in the matrix moves, including**text**,__text__,***text**,*__text__,***text***,~~text~~,a**b**, andsnake_case_name.pnpm check:frontendpasses. The backend is untouched, sopnpm check:backendwas not run.Not verified: the manual pass over
corpus/in the running application, which covers rendering, interaction, and navigator behavior beyond the automated round-trip.Notes
Reading a pair once the caret leaves the run is new behavior rather than a narrower rule, and is recorded in
docs/specification.mdanddocs/decisions.md. It qualifies the conservatism recorded for the strikethrough input rule rather than contradicting it: that decision reasons from an input rule not being able to know another delimiter is coming, which is the limitation deferring removes. The conservatism itself is kept where deferring cannot help, which is why a closer that could also open is refused rather than held.A run still open to growth when the document is saved is written as the literal text it is. What the file holds matches what the editor shows, and it reopens as the same document, so the two agree; it does mean the same keystrokes save differently depending on whether the caret moved first. No save-time finalize was added for it.
A tilde is left out of this pass. A strikethrough is spelled only by runs of equal length, so an unequal tilde run has nothing to pair, and the input rule Leafdown owns for it already holds a run literal until the author closes it.