Fix multi-line selection creating separate code blocks per line#912
Fix multi-line selection creating separate code blocks per line#912samuelpecher wants to merge 2 commits intomainfrom
Conversation
Selecting multiple lines and pressing the Code block button was creating one code block per line instead of a single code block containing all selected lines. The old implementation used $setBlocksType which wraps each block individually. Rewrite toggleCodeBlock() to collect all top-level elements in the selection and merge them into a single CodeNode separated by line breaks, mirroring how toggleBlockquote() already works. Toggle-off splits the code block content at line breaks back into separate paragraphs.
There was a problem hiding this comment.
Pull request overview
This PR fixes block-level code formatting so multi-line selections produce a single multiline code block (instead of one code block per line), and toggling the code block off restores separate paragraphs.
Changes:
- Update
toggleCodeBlockbehavior to merge multiple selected blocks into a singleCodeNode, and to unwrap multiline code blocks back into paragraphs. - Add browser tests covering multiline code-block apply + toggle-off behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/editor/contents.js |
Reworks code-block toggling to merge selected top-level blocks into one code block and unwrap back to paragraphs. |
test/browser/tests/formatting/inline_formatting.test.js |
Adds Playwright coverage for multiline code-block creation and toggle-off restoration. |
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| editor, | ||
| "<p>Line one</p><p>Line two</p><p>Line three</p>", | ||
| ) | ||
| }) |
There was a problem hiding this comment.
The new multiline code-block tests cover plain paragraphs, but they don’t exercise selections inside nested block structures (e.g., multiple paragraphs within a blockquote, or multiple list items). Given the new code path builds the code block from topLevelElements, adding at least one regression test for a multiline selection inside a blockquote and/or list would help ensure line boundaries are preserved for those common structures.
| }) | |
| }) | |
| test("selecting multiple paragraphs inside a blockquote and toggling code block creates a single code block", async ({ page, editor }) => { | |
| await editor.setValue("<blockquote><p>Line one</p><p>Line two</p></blockquote>") | |
| await editor.selectAll() | |
| await page.getByRole("button", { name: "Code" }).click() | |
| await assertEditorHtml( | |
| editor, | |
| '<pre data-language="plain" data-highlight-language="plain">Line one<br>Line two</pre>', | |
| ) | |
| }) |
| children.forEach(child => { | ||
| if ($isTextNode(child)) { | ||
| codeNode.append($createTextNode(child.getTextContent())) | ||
| } else if ($isLineBreakNode(child)) { | ||
| codeNode.append($createLineBreakNode()) | ||
| } else { |
There was a problem hiding this comment.
#applyCodeBlockFormat assumes each entry in topLevelElements is a leaf block (e.g., a paragraph). If the selection is inside a container top-level element (e.g., a <blockquote> / QuoteNode or a list), element.getChildren() returns nested block nodes and the code currently appends each child’s getTextContent() back-to-back without inserting line breaks between them. That collapses multiple paragraphs/list items into a single line in the resulting code block. Consider flattening the selection to the actual block nodes you want to convert (paragraphs/list items/etc.) and inserting $createLineBreakNode() between those blocks, or apply $setBlocksType first and then merge adjacent CodeNodes so nested structures preserve line boundaries.
| children.forEach(child => { | |
| if ($isTextNode(child)) { | |
| codeNode.append($createTextNode(child.getTextContent())) | |
| } else if ($isLineBreakNode(child)) { | |
| codeNode.append($createLineBreakNode()) | |
| } else { | |
| children.forEach((child, childIndex) => { | |
| if ($isTextNode(child)) { | |
| codeNode.append($createTextNode(child.getTextContent())) | |
| } else if ($isLineBreakNode(child)) { | |
| codeNode.append($createLineBreakNode()) | |
| } else { | |
| if (childIndex > 0) { | |
| codeNode.append($createLineBreakNode()) | |
| } |
When toggling a code block on a blockquote (or list) containing multiple paragraphs, the content was collapsed into a single line because #applyCodeBlockFormat only iterated top-level elements without descending into containers. Two fixes: - In toggleCodeBlock, unwrap non-paragraph/non-code container elements (using the existing #unwrap method) and re-gather top-level elements before merging into a code block. - In hasSelectedWordsInSingleLine, detect when anchor and focus are in different block-level children of the same top-level element (e.g. two paragraphs inside a blockquote) so the code button correctly triggers code block mode instead of inline code.
Summary
Fixes Fizzy card #5029: Making multiple lines a code block not working as expected