Skip to content

Commit 4313d4c

Browse files
committed
fix: address multiple memory leaks in CodeBlock component (CodeBlock_247, CodeBlock_459, CodeBlock_694)
1 parent c673569 commit 4313d4c

File tree

1 file changed

+68
-16
lines changed

1 file changed

+68
-16
lines changed

webview-ui/src/components/common/CodeBlock.tsx

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,10 @@ const CodeBlock = memo(
232232
const copyButtonWrapperRef = useRef<HTMLDivElement>(null)
233233
const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
234234
const { t } = useAppTranslation()
235+
const isMountedRef = useRef(true)
236+
const buttonPositionTimeoutRef = useRef<NodeJS.Timeout | null>(null)
237+
const collapseTimeout1Ref = useRef<NodeJS.Timeout | null>(null)
238+
const collapseTimeout2Ref = useRef<NodeJS.Timeout | null>(null)
235239

236240
// Update current language when prop changes, but only if user hasn't
237241
// made a selection.
@@ -243,17 +247,40 @@ const CodeBlock = memo(
243247
}
244248
}, [language, currentLanguage])
245249

250+
// Manage mounted state
251+
useEffect(() => {
252+
isMountedRef.current = true
253+
return () => {
254+
isMountedRef.current = false
255+
}
256+
}, [])
257+
258+
// Cleanup for collapse/expand timeouts
259+
useEffect(() => {
260+
return () => {
261+
if (collapseTimeout1Ref.current) {
262+
clearTimeout(collapseTimeout1Ref.current)
263+
}
264+
if (collapseTimeout2Ref.current) {
265+
clearTimeout(collapseTimeout2Ref.current)
266+
}
267+
}
268+
}, [])
269+
246270
// Syntax highlighting with cached Shiki instance.
247271
useEffect(() => {
248272
const fallback = `<pre style="padding: 0; margin: 0;"><code class="hljs language-${currentLanguage || "txt"}">${source || ""}</code></pre>`
249273

250274
const highlight = async () => {
251275
// Show plain text if language needs to be loaded.
252276
if (currentLanguage && !isLanguageLoaded(currentLanguage)) {
253-
setHighlightedCode(fallback)
277+
if (isMountedRef.current) {
278+
setHighlightedCode(fallback)
279+
}
254280
}
255281

256282
const highlighter = await getHighlighter(currentLanguage)
283+
if (!isMountedRef.current) return
257284

258285
const html = await highlighter.codeToHtml(source || "", {
259286
lang: currentLanguage || "txt",
@@ -277,13 +304,18 @@ const CodeBlock = memo(
277304
},
278305
] as ShikiTransformer[],
279306
})
307+
if (!isMountedRef.current) return
280308

281-
setHighlightedCode(html)
309+
if (isMountedRef.current) {
310+
setHighlightedCode(html)
311+
}
282312
}
283313

284314
highlight().catch((e) => {
285315
console.error("[CodeBlock] Syntax highlighting error:", e, "\nStack trace:", e.stack)
286-
setHighlightedCode(fallback)
316+
if (isMountedRef.current) {
317+
setHighlightedCode(fallback)
318+
}
287319
})
288320
}, [source, currentLanguage, collapsedHeight])
289321

@@ -455,8 +487,15 @@ const CodeBlock = memo(
455487
// Update button position and scroll when highlightedCode changes
456488
useEffect(() => {
457489
if (highlightedCode) {
490+
// Clear any existing timeout before setting a new one
491+
if (buttonPositionTimeoutRef.current) {
492+
clearTimeout(buttonPositionTimeoutRef.current)
493+
}
458494
// Update button position
459-
setTimeout(updateCodeBlockButtonPosition, 0)
495+
buttonPositionTimeoutRef.current = setTimeout(() => {
496+
updateCodeBlockButtonPosition()
497+
buttonPositionTimeoutRef.current = null // Optional: Clear ref after execution
498+
}, 0)
460499

461500
// Scroll to bottom if needed (immediately after Shiki updates)
462501
if (shouldScrollAfterHighlightRef.current) {
@@ -479,6 +518,12 @@ const CodeBlock = memo(
479518
shouldScrollAfterHighlightRef.current = false
480519
}
481520
}
521+
// Cleanup function for this effect
522+
return () => {
523+
if (buttonPositionTimeoutRef.current) {
524+
clearTimeout(buttonPositionTimeoutRef.current)
525+
}
526+
}
482527
}, [highlightedCode, updateCodeBlockButtonPosition])
483528

484529
// Advanced inertial scroll chaining
@@ -682,23 +727,30 @@ const CodeBlock = memo(
682727
{showCollapseButton && (
683728
<CodeBlockButton
684729
onClick={() => {
685-
// Get the current code block element and scrollable container
686-
const codeBlock = codeBlockRef.current
687-
const scrollContainer = document.querySelector('[data-virtuoso-scroller="true"]')
688-
if (!codeBlock || !scrollContainer) return
689-
730+
// Get the current code block element
731+
const codeBlock = codeBlockRef.current // Capture ref early
690732
// Toggle window shade state
691733
setWindowShade(!windowShade)
692734

735+
// Clear any previous timeouts
736+
if (collapseTimeout1Ref.current) clearTimeout(collapseTimeout1Ref.current)
737+
if (collapseTimeout2Ref.current) clearTimeout(collapseTimeout2Ref.current)
738+
693739
// After UI updates, ensure code block is visible and update button position
694-
setTimeout(
740+
collapseTimeout1Ref.current = setTimeout(
695741
() => {
696-
codeBlock.scrollIntoView({ behavior: "smooth", block: "nearest" })
697-
698-
// Wait for scroll to complete before updating button position
699-
setTimeout(() => {
700-
updateCodeBlockButtonPosition()
701-
}, 50)
742+
if (codeBlock) {
743+
// Check if codeBlock element still exists
744+
codeBlock.scrollIntoView({ behavior: "smooth", block: "nearest" })
745+
746+
// Wait for scroll to complete before updating button position
747+
collapseTimeout2Ref.current = setTimeout(() => {
748+
// updateCodeBlockButtonPosition itself should also check for refs if needed
749+
updateCodeBlockButtonPosition()
750+
collapseTimeout2Ref.current = null
751+
}, 50)
752+
}
753+
collapseTimeout1Ref.current = null
702754
},
703755
WINDOW_SHADE_SETTINGS.transitionDelayS * 1000 + 50,
704756
)

0 commit comments

Comments
 (0)