mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 12:25:22 +00:00
fix: Improve symbol reference/definition list perf (#327)
This commit is contained in:
parent
81a9ea1e59
commit
91e803d7a6
3 changed files with 118 additions and 45 deletions
|
|
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### 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 slow rendering issue with large reference/definition lists. [#327](https://github.com/sourcebot-dev/sourcebot/pull/327)
|
||||
|
||||
## [4.1.0] - 2025-06-02
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ interface LightweightCodeHighlighter {
|
|||
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.
|
||||
* 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');
|
||||
}, [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 highlightStyle = useCodeMirrorHighlighter();
|
||||
|
||||
useEffect(() => {
|
||||
if (isFileTooLargeToDisplay) {
|
||||
return;
|
||||
}
|
||||
|
||||
measure(() => Promise.all(
|
||||
unhighlightedLines
|
||||
.map(async (line, index) => {
|
||||
|
|
@ -103,12 +113,21 @@ export const LightweightCodeHighlighter = memo<LightweightCodeHighlighter>((prop
|
|||
const lineNumberDigits = String(lineCount).length;
|
||||
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 (
|
||||
<div
|
||||
style={{
|
||||
fontFamily: tailwind.theme.fontFamily.editor,
|
||||
fontSize: tailwind.theme.fontSize.editor,
|
||||
whiteSpace: renderWhitespace ? 'pre-wrap' : 'none',
|
||||
wordBreak: 'break-all',
|
||||
}}
|
||||
>
|
||||
{(highlightedLines ?? unhighlightedLines).map((line, index) => (
|
||||
|
|
|
|||
|
|
@ -3,18 +3,21 @@
|
|||
import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation";
|
||||
import { FileHeader } from "@/app/[domain]/components/fileHeader";
|
||||
import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { FindRelatedSymbolsResponse } from "@/features/codeNav/types";
|
||||
import { RepositoryInfo, SourceRange } from "@/features/search/types";
|
||||
import { base64Decode } from "@/lib/utils";
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useRef } from "react";
|
||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
|
||||
interface ReferenceListProps {
|
||||
data: FindRelatedSymbolsResponse;
|
||||
revisionName: string;
|
||||
}
|
||||
|
||||
const ESTIMATED_LINE_HEIGHT_PX = 30;
|
||||
const ESTIMATED_MATCH_CONTAINER_HEIGHT_PX = 30;
|
||||
|
||||
export const ReferenceList = ({
|
||||
data,
|
||||
revisionName,
|
||||
|
|
@ -29,52 +32,102 @@ export const ReferenceList = ({
|
|||
const { navigateToPath } = useBrowseNavigation();
|
||||
const captureEvent = useCaptureEvent();
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
{data.files.map((file, index) => {
|
||||
const repoInfo = repoInfoMap[file.repositoryId];
|
||||
// Virtualization setup
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const virtualizer = useVirtualizer({
|
||||
count: data.files.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: (index) => {
|
||||
const file = data.files[index];
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
<div className="bg-accent py-1 px-2 flex flex-row sticky top-0">
|
||||
<FileHeader
|
||||
repo={{
|
||||
name: repoInfo.name,
|
||||
displayName: repoInfo.displayName,
|
||||
codeHostType: repoInfo.codeHostType,
|
||||
webUrl: repoInfo.webUrl,
|
||||
const estimatedSize =
|
||||
file.matches.length * ESTIMATED_LINE_HEIGHT_PX +
|
||||
ESTIMATED_MATCH_CONTAINER_HEIGHT_PX;
|
||||
|
||||
return estimatedSize;
|
||||
},
|
||||
overscan: 5,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
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 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>
|
||||
)
|
||||
})}
|
||||
</ScrollArea>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue