diff --git a/src/lib/syntax.ts b/src/lib/syntax.ts index d74e6491b..261e78611 100644 --- a/src/lib/syntax.ts +++ b/src/lib/syntax.ts @@ -156,12 +156,17 @@ export function lineNumbersGutter(tree: Root) { }); } - // Add a line, and the eol. - lineNumber += 1; - replacement.push(createLine(line, lineNumber), { - type: "text", - value: match[0], + // Add a line, with its eol inside the line's content so that copying + // the line keeps it. The eol is wrapped in a span because the HTML + // parser drops a newline that directly follows a `
` start tag.
+ line.push({
+ type: "element",
+ tagName: "span",
+ properties: {},
+ children: [{ type: "text", value: match[0] }],
});
+ lineNumber += 1;
+ replacement.push(createLine(line, lineNumber));
start = index + 1;
textStart = match.index + match[0].length;
diff --git a/tests/e2e/clipboard.spec.ts b/tests/e2e/clipboard.spec.ts
index 905355dfa..3c0b18390 100644
--- a/tests/e2e/clipboard.spec.ts
+++ b/tests/e2e/clipboard.spec.ts
@@ -87,6 +87,22 @@ test("copy to clipboard", async ({ page, browserName, context }) => {
await expectClipboard("Inline `code` has `back-ticks around` it.\n");
}
+ // Selected source lines keep their newlines, including an empty line.
+ {
+ await page.goto(`${markdownUrl}/tree/main/markdown-code.md#L1`);
+ const from = await page.locator("#L5").getByText("```").boundingBox();
+ const to = await page.locator("#L8").getByText("plain:").boundingBox();
+ if (!from || !to) {
+ throw new Error("source lines not rendered");
+ }
+ await page.mouse.move(from.x, from.y + from.height / 2);
+ await page.mouse.down();
+ await page.mouse.move(to.x, to.y + to.height / 2, { steps: 5 });
+ await page.mouse.up();
+ await page.keyboard.press("ControlOrMeta+C");
+ await expectClipboard("```\n\n```\n");
+ }
+
// Link to a patch comment.
{
await page.goto(`${cobUrl}/patches`);