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>
This commit is contained in:
George·Dong 2025-12-15 15:41:54 +08:00 committed by GitHub
parent a1f0addafb
commit 4d3d5ae4ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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<ContextMenuProps> = ({ children }) => {
const { t } = useTranslation()
@ -45,8 +100,12 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ 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)
}
}