{{ message }}
Repository navigation
fix(muya): give a code block's trailing empty line a line box - #5286
Merged
Merged
Conversation
A newline with nothing after it lays out no line. The code block wrapped its final newline in a display:block `mu-line-end` span, which only makes up for that when the text before the newline does not end in one too: `hello\n\n` drew two lines, so Enter twice left the caret on the second line, and Enter in an empty code block did not move the caret at all. Append `<span class="mu-trailing-break"><br></span>` after highlighting whenever the text ends with a newline. It goes in after Prism runs because keep-markup drops an empty element at the very end of the text, and the <br> is wrapped because Chromium puts the caret just before it: a bare <br> yields (content, 1), which the offset mapping reads as text offset 1. Seed the IME composition anchor ahead of the trailing break; appended after it, the composition starts a line below the caret. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QodFHUNmHeC8NhrHSYeXk
) Line boxes and caret painting only exist in a real browser, so the spec measures them in Chromium. A collapsed range right after a newline has no client rects, so the caret is located in a screenshot instead: glyphs hidden, caret painted red with `caret-animation: manual`. It also guards the ways the fix can regress: a click or ArrowDown onto the empty last line must type at the end of the code, and an IME composition there must stay on that line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QodFHUNmHeC8NhrHSYeXk
The code block, its only caller, no longer asks for the `mu-line-end` wrapper. The removed branch also sliced the highlighted slice with offsets into the whole text (`highlightContent.substring(start, end - 1)`), losing highlighted text whenever the highlight did not start at offset 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QodFHUNmHeC8NhrHSYeXk
Content.arrowHandler decides whether the caret is on a block's first or last line by measuring it, and a caret on an empty line has no client rect. The failed measurement read as both first and last line, so ArrowUp from the empty last line, or ArrowDown from an empty middle line, left the code block. In a code block a newline on the caret's side already answers the question, so those keys are left to the browser. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QodFHUNmHeC8NhrHSYeXk
ArrowUp from the empty last line and ArrowDown from an empty middle line must move within the block; ArrowUp on the first line must still leave it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QodFHUNmHeC8NhrHSYeXk
This was referenced Sep 22, 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.

Fixes #5114
In a code block, type a line and press Enter twice: the caret should be on line 3, but it stays on line 2. While testing this branch a second defect on the same empty lines turned up — arrow keys there left the code block — and it is fixed here too.
Root cause
A newline with nothing after it lays out no line. The code block compensated by wrapping its final
\nin adisplay: blockmu-line-endspan (getHighlightHtml(…, handleLineEnding)), and that only works when the text before the newline does not end in one as well. Measured in Chromium:hello\nhello<span.mu-line-end>\n</span>hello\n\nhello\n<span.mu-line-end>\n</span>\n(empty block + Enter)<span.mu-line-end>\n</span>With no line box to sit on, the caret is drawn on the line above. The same gap made ArrowDown onto the empty last line type at the end of the previous line:
hello\nabc+ ArrowDown +qgavehello\nabcq\n.Fix
CodeBlockContent.update()appends<span class="mu-trailing-break"><br></span>whenever the text ends with a newline. The text node — and sotextContentand every offset — is unchanged. Two constraints shape it:<br>in the pre-highlight HTML is dropped.<br>, inside its parent. For a bare<br>that is(content, 1), whichTextSelection.getSelectionreads as text offset 1 — clicking the last line and typing producedhzello. Inside the wrapper the caret is at(wrapper, 0), which resolves to the text length.The IME composition anchor from #5281 is now seeded ahead of the trailing break; appended after it, the composition started a line below the caret.
getHighlightHtmlloses itshandleLineEndingpath, which only the code block used.Measured and rejected
mu-line-endinline +::after { content: '\200B' }<br>Arrow keys on empty lines
In
hello\nworld\nwith the caret on the empty last line, ArrowUp left the code block for the language input instead of moving toworld; from an empty middle line, ArrowDown left it the other way. This predates the branch.Content.arrowHandlermeasures the caret to decide whether it is on the block's first or last line. A caret on an empty line has no client rect, and the failed measurement reads as both. In a code block a newline on the caret's side already answers the question, soCodeBlockContent.arrowHandlerleaves those keys to the browser and defers toContent.arrowHandlerotherwise — including a wrapped first or last line, which still needs the measurement.Tests
e2e/tests/blocks/codeblock-trailing-newline-5114.spec.ts(Chromium)caret-animation: manual;Input.imeSetComposition.codeBlockContent/__tests__/trailingNewline.spec.ts(jsdom) — a trailing break only after a final newline,textContentunchanged, kept on highlighted blocks, the IME anchor seeded ahead of it.codeBlockContent/__tests__/arrowHandler.spec.ts(happy-dom, which like Chromium on an empty line gives the caret no rect) — which arrow keys are left to the browser and which still go throughContent.arrowHandler.Verified
develop: 4 fail--repeat-each=8 --workers=4--repeat-each=8 --workers=4trailingNewline.spec.ts(4)develop: 3 failarrowHandler.spec.ts(4)Content.arrowHandlerprevents both empty-line movesBoth IME guards fail with the anchor change reverted.
tsc --noEmitand ESLint (0 errors) are clean, and the codeBlockContent / base arrow-navigation / table-cell arrow / HTML-block arrow unit suites pass. Locally theblocks,diagrams,typingandoptionsE2E directories pass exceptsearch-replace×2 and theime.spectable-cell case — which fail identically on unmodifieddevelopin the same setup — and the first twooptions/autopaircases, which type into a paragraph and never reach this code.Found along the way, not changed here
codeBlock/index.ts). A keystroke landing before that frame loses its caret to the start of the block, and the rest of the word is typed in front of it:hellobecomeselloh. Seen only in automation under heavy parallel load (22 of 32 runs at 4 workers); not reproduced at typing speed, even with 20× CPU throttling. The E2E spec waits a frame after creating a block.TextSelection.getSelectionreads an element-level boundary(content, n)as text offsetnrather than the length of the text before childn. No normal editing path ondevelopis known to produce such a boundary — the bare<br>above did. To see it: in a code block containinghello world, set the selection to(codeContent, 1)(the end of the text), press Shift, then typeZ— the result ishZello world.🤖 Generated with Claude Code
https://claude.ai/code/session_013QodFHUNmHeC8NhrHSYeXk