mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 14:41:24 +08:00
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:
parent
a1f0addafb
commit
4d3d5ae4ce
@ -6,6 +6,61 @@ interface ContextMenuProps {
|
|||||||
children: React.ReactNode
|
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?
|
// FIXME: Why does this component name look like a generic component but is not customizable at all?
|
||||||
const ContextMenu: React.FC<ContextMenuProps> = ({ children }) => {
|
const ContextMenu: React.FC<ContextMenuProps> = ({ children }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -45,8 +100,12 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ children }) => {
|
|||||||
|
|
||||||
const onOpenChange = (open: boolean) => {
|
const onOpenChange = (open: boolean) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
const selectedText = window.getSelection()?.toString()
|
const selection = window.getSelection()
|
||||||
setSelectedText(selectedText)
|
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
|
||||||
|
setSelectedText(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSelectedText(extractSelectedText(selection) || undefined)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user