From b9b137dc914139194a6f0debfb2d1552c79bf525 Mon Sep 17 00:00:00 2001 From: ji ruo <79234113+RuoJi6@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:30:44 +0800 Subject: [PATCH] fix: preserve blank lines when copying HTTP packets --- web/src/components/http-code-block.tsx | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/web/src/components/http-code-block.tsx b/web/src/components/http-code-block.tsx index 6370b54..0a24de1 100644 --- a/web/src/components/http-code-block.tsx +++ b/web/src/components/http-code-block.tsx @@ -111,6 +111,40 @@ function HighlightedBody({ body, format }: { body: string; format: BodyFormat }) return body; } +// Empty grid rows have no text, so native selection copying drops their newlines. +// Map the selected DOM boundaries back to the raw packet instead. +function selectedPacketText(container: HTMLElement, value: string, lines: string[]): string | null { + const selection = window.getSelection(); + if (!selection || selection.isCollapsed || selection.rangeCount !== 1) return null; + const range = selection.getRangeAt(0); + if (!container.contains(range.startContainer) || !container.contains(range.endContainer)) return null; + + let start: number | null = null; + let end: number | null = null; + let offset = 0; + container.querySelectorAll("[data-line] code").forEach((code, index) => { + if (range.intersectsNode(code)) { + const selected = document.createRange(); + selected.selectNodeContents(code); + if (range.compareBoundaryPoints(Range.START_TO_START, selected) > 0) { + selected.setStart(range.startContainer, range.startOffset); + } + if (range.compareBoundaryPoints(Range.END_TO_END, selected) < 0) { + selected.setEnd(range.endContainer, range.endOffset); + } + const prefix = document.createRange(); + prefix.selectNodeContents(code); + prefix.setEnd(selected.startContainer, selected.startOffset); + start ??= offset + prefix.toString().length; + prefix.setEnd(selected.endContainer, selected.endOffset); + end = offset + prefix.toString().length; + } + const lineEnd = offset + lines[index].length; + offset = lineEnd + (value[lineEnd] === "\r" ? 2 : 1); + }); + return start !== null && end !== null ? value.slice(start, end) : null; +} + export function HttpCodeBlock({ raw }: { raw: string }) { const [wrapLines, setWrapLines] = React.useState(true); const [copied, setCopied] = React.useState(false); @@ -183,6 +217,12 @@ export function HttpCodeBlock({ raw }: { raw: string }) { aria-readonly="true" tabIndex={0} className="max-h-[calc(100vh-15rem)] min-w-0 overflow-auto bg-background py-3 font-mono text-xs outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring" + onCopy={(event) => { + const text = selectedPacketText(event.currentTarget, value, lines); + if (text === null) return; + event.preventDefault(); + event.clipboardData.setData("text/plain", text); + }} onKeyDown={(event) => { if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== "a") return; event.preventDefault();