Skip to content

Commit b011ea0

Browse files
authored
Simplify deferred codeblock highlighting and fix streaming (#2977)
1 parent d33171b commit b011ea0

File tree

2 files changed

+64
-30
lines changed

2 files changed

+64
-30
lines changed

.changeset/old-dragons-crash.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gitbook": patch
3+
---
4+
5+
Fix rendering of code blocks in Ask AI when being streamed
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
'use client';
22

33
import type { DocumentBlockCode } from '@gitbook/api';
4-
import { useEffect, useRef, useState } from 'react';
4+
import { useEffect, useMemo, useRef, useState } from 'react';
55

66
import { useInViewportListener } from '@/components/hooks/useInViewportListener';
77
import { useScrollListener } from '@/components/hooks/useScrollListener';
8-
import { useDebounceCallback, useEventCallback } from 'usehooks-ts';
8+
import { useDebounceCallback } from 'usehooks-ts';
99
import type { BlockProps } from '../Block';
1010
import { CodeBlockRenderer } from './CodeBlockRenderer';
1111
import type { HighlightLine, RenderedInline } from './highlight';
@@ -22,53 +22,82 @@ type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> &
2222
export function ClientCodeBlock(props: ClientBlockProps) {
2323
const { block, style, inlines } = props;
2424
const blockRef = useRef<HTMLDivElement>(null);
25-
const processedRef = useRef(false);
26-
const isInViewportRef = useRef<boolean | null>(null);
27-
const [lines, setLines] = useState<HighlightLine[]>(() => plainHighlight(block, []));
25+
const isInViewportRef = useRef(false);
26+
const [isInViewport, setIsInViewport] = useState(false);
27+
const plainLines = useMemo(() => plainHighlight(block, []), [block]);
28+
const [lines, setLines] = useState<null | HighlightLine[]>(null);
2829

2930
// Preload the highlighter when the block is mounted.
3031
useEffect(() => {
3132
import('./highlight').then(({ preloadHighlight }) => preloadHighlight(block));
3233
}, [block]);
3334

34-
const runHighlight = useEventCallback(() => {
35-
if (processedRef.current) {
36-
return;
37-
}
38-
if (typeof window !== 'undefined') {
39-
import('./highlight').then(({ highlight }) => {
40-
highlight(block, inlines).then((lines) => {
41-
setLines(lines);
42-
processedRef.current = true;
43-
});
44-
});
35+
// When user scrolls, we need to wait for the scroll to finish before running the highlight
36+
const isScrollingRef = useRef(false);
37+
const onFinishScrolling = useDebounceCallback(() => {
38+
isScrollingRef.current = false;
39+
40+
// If the block is in the viewport after the scroll, we need to run the highlight
41+
if (isInViewportRef.current) {
42+
setIsInViewport(true);
4543
}
46-
});
47-
const debouncedRunHighlight = useDebounceCallback(runHighlight, 1000);
44+
}, 100);
45+
useScrollListener(
46+
() => {
47+
isScrollingRef.current = true;
48+
onFinishScrolling();
49+
},
50+
useRef(typeof window !== 'undefined' ? window : null)
51+
);
4852

53+
// Detect when the block is in viewport
4954
useInViewportListener(
5055
blockRef,
5156
(isInViewport, disconnect) => {
52-
// Disconnect once in viewport
57+
isInViewportRef.current = isInViewport;
58+
if (isScrollingRef.current) {
59+
// If the user is scrolling, we don't want to run the highlight
60+
// because it will be run when the scroll is finished
61+
return;
62+
}
63+
5364
if (isInViewport) {
65+
// Disconnect once in viewport
5466
disconnect();
55-
// If it's initially in viewport, we need to run the highlight
56-
if (isInViewportRef.current === null) {
57-
runHighlight();
58-
}
67+
setIsInViewport(true);
5968
}
60-
isInViewportRef.current = isInViewport;
6169
},
6270
{ rootMargin: '200px' }
6371
);
6472

65-
const handleScroll = useDebounceCallback(() => {
66-
if (isInViewportRef.current) {
67-
debouncedRunHighlight();
73+
// When the block is visible or updated, we need to re-run the highlight
74+
useEffect(() => {
75+
if (isInViewport) {
76+
// If the block is in viewport, we need to run the highlight
77+
let cancelled = false;
78+
79+
if (typeof window !== 'undefined') {
80+
import('./highlight').then(({ highlight }) => {
81+
highlight(block, inlines).then((lines) => {
82+
if (cancelled) {
83+
return;
84+
}
85+
86+
setLines(lines);
87+
});
88+
});
89+
}
90+
91+
return () => {
92+
cancelled = true;
93+
};
6894
}
69-
}, 80);
7095

71-
useScrollListener(handleScroll, useRef(typeof window !== 'undefined' ? window : null));
96+
// Otherwise if the block is not in viewport, we reset to the plain lines
97+
setLines(null);
98+
}, [isInViewport, block, inlines]);
7299

73-
return <CodeBlockRenderer ref={blockRef} block={block} style={style} lines={lines} />;
100+
return (
101+
<CodeBlockRenderer ref={blockRef} block={block} style={style} lines={lines ?? plainLines} />
102+
);
74103
}

0 commit comments

Comments
 (0)