diff --git a/CHANGELOG.md b/CHANGELOG.md index ced89e9e..ececd111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/web/src/app/[domain]/components/lightweightCodeHighlighter.tsx b/packages/web/src/app/[domain]/components/lightweightCodeHighlighter.tsx index 1cb01719..dbb6c00f 100644 --- a/packages/web/src/app/[domain]/components/lightweightCodeHighlighter.tsx +++ b/packages/web/src/app/[domain]/components/lightweightCodeHighlighter.tsx @@ -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((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(null); const highlightStyle = useCodeMirrorHighlighter(); useEffect(() => { + if (isFileTooLargeToDisplay) { + return; + } + measure(() => Promise.all( unhighlightedLines .map(async (line, index) => { @@ -103,12 +113,21 @@ export const LightweightCodeHighlighter = memo((prop const lineNumberDigits = String(lineCount).length; const lineNumberWidth = `${lineNumberDigits + 2}ch`; // +2 for padding + if (isFileTooLargeToDisplay) { + return ( +
+ File too large to display in preview. +
+ ); + } + return (
{(highlightedLines ?? unhighlightedLines).map((line, index) => ( diff --git a/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx b/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx index 8b3acd80..c82782dd 100644 --- a/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx +++ b/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx @@ -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 ( - - {data.files.map((file, index) => { - const repoInfo = repoInfoMap[file.repositoryId]; + // Virtualization setup + const parentRef = useRef(null); + const virtualizer = useVirtualizer({ + 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 ( -
-
- +
+ {virtualizer.getVirtualItems().map((virtualRow) => { + const file = data.files[virtualRow.index]; + const repoInfo = repoInfoMap[file.repositoryId]; + return ( +
+
+ > + +
+
+ {file.matches + .sort((a, b) => a.range.start.lineNumber - b.range.start.lineNumber) + .map((match, index) => ( + { + captureEvent('wa_explore_menu_reference_clicked', {}); + navigateToPath({ + repoName: file.repository, + revisionName, + path: file.fileName, + pathType: 'blob', + highlightRange: match.range, + }) + }} + /> + ))} +
-
- {file.matches - .sort((a, b) => a.range.start.lineNumber - b.range.start.lineNumber) - .map((match, index) => ( - { - captureEvent('wa_explore_menu_reference_clicked', {}); - navigateToPath({ - repoName: file.repository, - revisionName, - path: file.fileName, - pathType: 'blob', - highlightRange: match.range, - }) - }} - /> - ))} -
-
- ) - })} - - ) + ); + })} +
+
+ ); }