From 4d3d5ae4cee601a44326c0a94ee62f78b481a77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?George=C2=B7Dong?= <98630204+GeorgeDong32@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:41:54 +0800 Subject: [PATCH] fix/line-number-wrongly-copied (#11857) * fix(code-viewer): copy selected code without line numbers * fix(context-menu): strip line numbers from code selection * style(codeviewer): fix format * fix: preserve indentation and format when copying mixed content (text + code blocks) - Replace regex-based extraction with DOM structure-based approach - Remove line number elements while preserving all other content - Use TreeWalker to handle mixed content (text paragraphs + code blocks) - Preserve indentation and newlines in code blocks - Simplify CodeViewer.tsx by removing duplicate context menu logic Fixes #11790 * style: remove unused comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * refactor: optimize TreeWalker performance --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/components/ContextMenu/index.tsx | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/ContextMenu/index.tsx b/src/renderer/src/components/ContextMenu/index.tsx index 719cd14133..8db88955bd 100644 --- a/src/renderer/src/components/ContextMenu/index.tsx +++ b/src/renderer/src/components/ContextMenu/index.tsx @@ -6,6 +6,61 @@ interface ContextMenuProps { children: React.ReactNode } +/** + * Extract text content from selection, filtering out line numbers in code viewers. + * Preserves all content including plain text and code blocks, only removing line numbers. + * This ensures right-click copy in code blocks doesn't include line numbers while preserving indentation. + */ +function extractSelectedText(selection: Selection): string { + // Validate selection + if (selection.rangeCount === 0 || selection.isCollapsed) { + return '' + } + + const range = selection.getRangeAt(0) + const fragment = range.cloneContents() + + // Check if the selection contains code viewer elements + const hasLineNumbers = fragment.querySelectorAll('.line-number').length > 0 + + // If no line numbers, return the original text (preserves formatting) + if (!hasLineNumbers) { + return selection.toString() + } + + // Remove all line number elements + fragment.querySelectorAll('.line-number').forEach((el) => el.remove()) + + // Handle all content using optimized TreeWalker with precise node filtering + // This approach handles mixed content correctly while improving performance + const walker = document.createTreeWalker(fragment, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, null) + + let result = '' + let node = walker.nextNode() + + while (node) { + if (node.nodeType === Node.TEXT_NODE) { + // Preserve text content including whitespace + result += node.textContent + } else if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as Element + + // Add newline after block elements and code lines to preserve structure + if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(element.tagName)) { + result += '\n' + } else if (element.classList.contains('line')) { + // Add newline after code lines to preserve code structure + result += '\n' + } + } + + node = walker.nextNode() + } + + // Clean up excessive newlines but preserve code structure + return result.trim() +} + // FIXME: Why does this component name look like a generic component but is not customizable at all? const ContextMenu: React.FC = ({ children }) => { const { t } = useTranslation() @@ -45,8 +100,12 @@ const ContextMenu: React.FC = ({ children }) => { const onOpenChange = (open: boolean) => { if (open) { - const selectedText = window.getSelection()?.toString() - setSelectedText(selectedText) + const selection = window.getSelection() + if (!selection || selection.rangeCount === 0 || selection.isCollapsed) { + setSelectedText(undefined) + return + } + setSelectedText(extractSelectedText(selection) || undefined) } }