fix: pair a delimiter typed beside a mark with the literal one across it - #345
Merged
Merged
Conversation
The typed character reaches the emphasis input rule while the source projection is open, so the rule matches the projected literal and the projection's later commit discards what it built. The pairing is therefore decided once the projection has settled and the run exists, which is a dispatch later than the character arrives in, so the typed character is held in plugin state until then. It is driven by the character typed in this session rather than by the shape of the document: escapes do not survive a load, so a file holding a delimiter on each side of a run opens as the same three siblings, and only the typed character tells the two apart.
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~typed against a marked run now pairs with the matching literal run on the other side of it, so the two become the construct the line spells instead of both staying as content.Typing the closing
_of_**text**reached a document of three siblings — a literal_, the strong run, and another literal_— which the file recorded faithfully as\_**text**\_. Opening a file holding_**text**_gives emphasis wrapping strong emphasis, so the parser and the editing path disagreed about the same characters.The character reaches Milkdown's emphasis input rule while the source projection is open, so the rule matches the projected literal
_**text**_and offers emphasis over a literal**text**. The projection then commits: its session covers only the mark's own source, a character typed outside it leaves that source unchanged, and the clean finalize restores the mark's original content over the range, discarding what the rule built. Both answers are wrong on their own, and the commit lands a dispatch later than the character arrives in.attentionPairingdecides the pairing once the document has settled and the run exists rather than its projection. The typed character is held in plugin state untilhasActiveSourceProjectionis false, then mapped forward and answered against the real marks.Three constraints it holds:
\_**text**\_opens as the same three siblings a paired document would have started from; only the typed character separates them.a_**text**keeps its markers literal. The run always faces the mark's own delimiters, which are punctuation, so both CommonMark tests collapse to the character on the outer side and the intraword rule_adds falls out of the same neighbour.Related Issue
Closes #344
Verification
attentionPairing.test.tscovers the four rows the issue measures — both delimiter characters, closing after the run and opening before it — asserting the document once the projection settles, the bytes, and the document the saved file reloads as. Reading the projection instead of the document is what produced that issue's previous table, so the assertions park the caret in a following paragraph first.The rows that must not pair are covered beside them: a file that already escapes a delimiter on each side of a run, a run that cannot open (
a_**text**), one that cannot close (_**text**a), runs of unequal length, a span already carrying the mark it would apply, and an ordinary_text_with no mark between, which the existing input rule still owns.sourceProjection.test.tsheld one expectation encoding the behavior this replaces, for the issue's second row, and it was updated rather than worked around. What it exists to prove is unchanged and still asserted: the input rule must not swallow the run into literal text, whichgetEditorTextContentnow pins astextrather than__text__. Its caret moves to a following paragraph because the pairing carries the run to the end of its own.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
The saved nesting order is the schema's, not the order the delimiters were typed in:
__*text*__is written*__text__*, and~~**text**~~is written**~~text~~**. Measured with this plugin disabled, a plain open of either form produces one identical document and the same output, so the order is lost when the file is read and no serializer change reaches it. It is the cross-type sibling of the same-type collapse #251 already excludes, and that issue now records it there. The tests assert the mark set and the reload rather than a nesting order.A delimiter separated from the run by text, such as
_a**b**_, is not covered. That is general delimiter recognition, which #232 owns.Typing the delimiter on the inner side of a closing run remains out of scope, as #344 and #232 both record: it becomes part of the run's content, and CommonMark does not require the editor to relocate it.