fix: Improve symbol reference/definition list perf (#327)

This commit is contained in:
Brendan Kellam 2025-06-02 14:49:51 -07:00 committed by GitHub
parent 81a9ea1e59
commit 91e803d7a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 118 additions and 45 deletions

View file

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fixed issue with the symbol hover popover clipping at the top of the page. [#326](https://github.com/sourcebot-dev/sourcebot/pull/326) - Fixed issue with the symbol hover popover clipping at the top of the page. [#326](https://github.com/sourcebot-dev/sourcebot/pull/326)
- Fixed slow rendering issue with large reference/definition lists. [#327](https://github.com/sourcebot-dev/sourcebot/pull/327)
## [4.1.0] - 2025-06-02 ## [4.1.0] - 2025-06-02

View file

@ -27,6 +27,9 @@ interface LightweightCodeHighlighter {
renderWhitespace?: boolean; renderWhitespace?: boolean;
} }
// The maximum number of characters per line that we will display in the preview.
const MAX_NUMBER_OF_CHARACTER_PER_LINE = 1000;
/** /**
* Lightweight code highlighter that uses the Lezer parser to highlight code. * Lightweight code highlighter that uses the Lezer parser to highlight code.
* This is helpful in scenarios where we need to highlight a ton of code snippets * This is helpful in scenarios where we need to highlight a ton of code snippets
@ -49,12 +52,19 @@ export const LightweightCodeHighlighter = memo<LightweightCodeHighlighter>((prop
return code.trimEnd().split('\n'); return code.trimEnd().split('\n');
}, [code]); }, [code]);
const isFileTooLargeToDisplay = useMemo(() => {
return unhighlightedLines.some(line => line.length > MAX_NUMBER_OF_CHARACTER_PER_LINE);
}, [code]);
const [highlightedLines, setHighlightedLines] = useState<React.ReactNode[] | null>(null); const [highlightedLines, setHighlightedLines] = useState<React.ReactNode[] | null>(null);
const highlightStyle = useCodeMirrorHighlighter(); const highlightStyle = useCodeMirrorHighlighter();
useEffect(() => { useEffect(() => {
if (isFileTooLargeToDisplay) {
return;
}
measure(() => Promise.all( measure(() => Promise.all(
unhighlightedLines unhighlightedLines
.map(async (line, index) => { .map(async (line, index) => {
@ -103,12 +113,21 @@ export const LightweightCodeHighlighter = memo<LightweightCodeHighlighter>((prop
const lineNumberDigits = String(lineCount).length; const lineNumberDigits = String(lineCount).length;
const lineNumberWidth = `${lineNumberDigits + 2}ch`; // +2 for padding const lineNumberWidth = `${lineNumberDigits + 2}ch`; // +2 for padding
if (isFileTooLargeToDisplay) {
return (
<div className="font-mono text-sm px-2">
File too large to display in preview.
</div>
);
}
return ( return (
<div <div
style={{ style={{
fontFamily: tailwind.theme.fontFamily.editor, fontFamily: tailwind.theme.fontFamily.editor,
fontSize: tailwind.theme.fontSize.editor, fontSize: tailwind.theme.fontSize.editor,
whiteSpace: renderWhitespace ? 'pre-wrap' : 'none', whiteSpace: renderWhitespace ? 'pre-wrap' : 'none',
wordBreak: 'break-all',
}} }}
> >
{(highlightedLines ?? unhighlightedLines).map((line, index) => ( {(highlightedLines ?? unhighlightedLines).map((line, index) => (

View file

@ -3,18 +3,21 @@
import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation"; import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation";
import { FileHeader } from "@/app/[domain]/components/fileHeader"; import { FileHeader } from "@/app/[domain]/components/fileHeader";
import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter"; import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter";
import { ScrollArea } from "@/components/ui/scroll-area";
import { FindRelatedSymbolsResponse } from "@/features/codeNav/types"; import { FindRelatedSymbolsResponse } from "@/features/codeNav/types";
import { RepositoryInfo, SourceRange } from "@/features/search/types"; import { RepositoryInfo, SourceRange } from "@/features/search/types";
import { base64Decode } from "@/lib/utils"; import { base64Decode } from "@/lib/utils";
import { useMemo } from "react"; import { useMemo, useRef } from "react";
import useCaptureEvent from "@/hooks/useCaptureEvent"; import useCaptureEvent from "@/hooks/useCaptureEvent";
import { useVirtualizer } from "@tanstack/react-virtual";
interface ReferenceListProps { interface ReferenceListProps {
data: FindRelatedSymbolsResponse; data: FindRelatedSymbolsResponse;
revisionName: string; revisionName: string;
} }
const ESTIMATED_LINE_HEIGHT_PX = 30;
const ESTIMATED_MATCH_CONTAINER_HEIGHT_PX = 30;
export const ReferenceList = ({ export const ReferenceList = ({
data, data,
revisionName, revisionName,
@ -29,52 +32,102 @@ export const ReferenceList = ({
const { navigateToPath } = useBrowseNavigation(); const { navigateToPath } = useBrowseNavigation();
const captureEvent = useCaptureEvent(); const captureEvent = useCaptureEvent();
return ( // Virtualization setup
<ScrollArea className="h-full"> const parentRef = useRef<HTMLDivElement>(null);
{data.files.map((file, index) => { const virtualizer = useVirtualizer({
const repoInfo = repoInfoMap[file.repositoryId]; count: data.files.length,
getScrollElement: () => parentRef.current,
estimateSize: (index) => {
const file = data.files[index];
const estimatedSize =
file.matches.length * ESTIMATED_LINE_HEIGHT_PX +
ESTIMATED_MATCH_CONTAINER_HEIGHT_PX;
return ( return estimatedSize;
<div key={index}> },
<div className="bg-accent py-1 px-2 flex flex-row sticky top-0"> overscan: 5,
<FileHeader enabled: true,
repo={{ });
name: repoInfo.name,
displayName: repoInfo.displayName, return (
codeHostType: repoInfo.codeHostType, <div
webUrl: repoInfo.webUrl, ref={parentRef}
style={{
width: "100%",
height: "100%",
overflowY: "auto",
contain: "strict",
}}
>
<div
style={{
height: virtualizer.getTotalSize(),
width: "100%",
position: "relative",
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => {
const file = data.files[virtualRow.index];
const repoInfo = repoInfoMap[file.repositoryId];
return (
<div
key={virtualRow.key}
data-index={virtualRow.index}
ref={virtualizer.measureElement}
style={{
position: "absolute",
transform: `translateY(${virtualRow.start}px)`,
top: 0,
left: 0,
width: "100%",
}}
>
<div
className="bg-accent py-1 px-2 flex flex-row sticky top-0 z-10"
style={{
top: `-${virtualRow.start}px`,
}} }}
fileName={file.fileName} >
branchDisplayName={revisionName === "HEAD" ? undefined : revisionName} <FileHeader
/> repo={{
name: repoInfo.name,
displayName: repoInfo.displayName,
codeHostType: repoInfo.codeHostType,
webUrl: repoInfo.webUrl,
}}
fileName={file.fileName}
branchDisplayName={revisionName === "HEAD" ? undefined : revisionName}
/>
</div>
<div className="divide-y">
{file.matches
.sort((a, b) => a.range.start.lineNumber - b.range.start.lineNumber)
.map((match, index) => (
<ReferenceListItem
key={index}
lineContent={match.lineContent}
range={match.range}
language={file.language}
onClick={() => {
captureEvent('wa_explore_menu_reference_clicked', {});
navigateToPath({
repoName: file.repository,
revisionName,
path: file.fileName,
pathType: 'blob',
highlightRange: match.range,
})
}}
/>
))}
</div>
</div> </div>
<div className="divide-y"> );
{file.matches })}
.sort((a, b) => a.range.start.lineNumber - b.range.start.lineNumber) </div>
.map((match, index) => ( </div>
<ReferenceListItem );
key={index}
lineContent={match.lineContent}
range={match.range}
language={file.language}
onClick={() => {
captureEvent('wa_explore_menu_reference_clicked', {});
navigateToPath({
repoName: file.repository,
revisionName,
path: file.fileName,
pathType: 'blob',
highlightRange: match.range,
})
}}
/>
))}
</div>
</div>
)
})}
</ScrollArea>
)
} }