diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 00000000..897af65d
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,5 @@
+{
+ "recommendations": [
+ "dbaeumer.vscode-eslint"
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 5f65bac5..463b90ca 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -2,5 +2,10 @@
"files.associations": {
"*.json": "jsonc",
"index.json": "json"
- }
+ },
+ "eslint.workingDirectories": [
+ {
+ "pattern": "./packages/*/"
+ }
+ ]
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3c008bf6..a8e6cff6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed issue with GitLab sub-projects not being included recursively. ([#54](https://github.com/sourcebot-dev/sourcebot/pull/54))
+- Fixed slow rendering performance when rendering a large number of results. ([#52](https://github.com/sourcebot-dev/sourcebot/pull/52))
## [2.1.1] - 2024-10-25
diff --git a/packages/web/package.json b/packages/web/package.json
index cc523f8e..672758d2 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -38,6 +38,7 @@
"@replit/codemirror-vim": "^6.2.1",
"@tanstack/react-query": "^5.53.3",
"@tanstack/react-table": "^8.20.5",
+ "@tanstack/react-virtual": "^3.10.8",
"@uiw/react-codemirror": "^4.23.0",
"class-variance-authority": "^0.7.0",
"client-only": "^0.0.1",
diff --git a/packages/web/src/app/search/components/codePreviewPanel/index.tsx b/packages/web/src/app/search/components/codePreviewPanel/index.tsx
index 6921663f..40df1ff9 100644
--- a/packages/web/src/app/search/components/codePreviewPanel/index.tsx
+++ b/packages/web/src/app/search/components/codePreviewPanel/index.tsx
@@ -59,5 +59,4 @@ export const CodePreviewPanel = ({
onSelectedMatchIndexChange={onSelectedMatchIndexChange}
/>
)
-
}
\ No newline at end of file
diff --git a/packages/web/src/app/search/components/filterPanel/index.tsx b/packages/web/src/app/search/components/filterPanel/index.tsx
index 1a220fe3..d459799a 100644
--- a/packages/web/src/app/search/components/filterPanel/index.tsx
+++ b/packages/web/src/app/search/components/filterPanel/index.tsx
@@ -93,7 +93,7 @@ export const FilterPanel = ({
);
onFilterChanged(filteredMatches);
- }, [matches, repos, languages]);
+ }, [matches, repos, languages, onFilterChanged]);
return (
diff --git a/packages/web/src/app/search/components/searchResultsPanel/codePreview.tsx b/packages/web/src/app/search/components/searchResultsPanel/codePreview.tsx
index e7ebb7ed..d1759401 100644
--- a/packages/web/src/app/search/components/searchResultsPanel/codePreview.tsx
+++ b/packages/web/src/app/search/components/searchResultsPanel/codePreview.tsx
@@ -1,12 +1,15 @@
'use client';
-import { useExtensionWithDependency } from "@/hooks/useExtensionWithDependency";
-import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension";
-import { useThemeNormalized } from "@/hooks/useThemeNormalized";
+import { getSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension";
import { lineOffsetExtension } from "@/lib/extensions/lineOffsetExtension";
import { SearchResultRange } from "@/lib/types";
-import CodeMirror, { Decoration, DecorationSet, EditorState, EditorView, ReactCodeMirrorRef, StateField, Transaction } from "@uiw/react-codemirror";
+import { defaultHighlightStyle, syntaxHighlighting } from "@codemirror/language";
+import { EditorState, StateField, Transaction } from "@codemirror/state";
+import { defaultLightThemeOption, oneDarkHighlightStyle, oneDarkTheme } from "@uiw/react-codemirror";
+import { Decoration, DecorationSet, EditorView, lineNumbers } from "@codemirror/view";
import { useMemo, useRef } from "react";
+import { LightweightCodeMirror, CodeMirrorRef } from "./lightweightCodeMirror";
+import { useThemeNormalized } from "@/hooks/useThemeNormalized";
const markDecoration = Decoration.mark({
class: "cm-searchMatch-selected"
@@ -25,13 +28,22 @@ export const CodePreview = ({
ranges,
lineOffset,
}: CodePreviewProps) => {
- const editorRef = useRef
(null);
+ const editorRef = useRef(null);
const { theme } = useThemeNormalized();
- const syntaxHighlighting = useSyntaxHighlightingExtension(language, editorRef.current?.view);
-
- const rangeHighlighting = useExtensionWithDependency(editorRef.current?.view ?? null, () => {
+ const extensions = useMemo(() => {
return [
+ EditorView.editable.of(false),
+ ...(theme === 'dark' ? [
+ syntaxHighlighting(oneDarkHighlightStyle),
+ oneDarkTheme,
+ ] : [
+ syntaxHighlighting(defaultHighlightStyle),
+ defaultLightThemeOption,
+ ]),
+ lineNumbers(),
+ lineOffsetExtension(lineOffset),
+ getSyntaxHighlightingExtension(language),
StateField.define({
create(editorState: EditorState) {
const document = editorState.doc;
@@ -61,7 +73,8 @@ export const CodePreview = ({
const from = document.line(startLine).from + Start.Column - 1;
const to = document.line(endLine).from + End.Column - 1;
return markDecoration.range(from, to);
- });
+ })
+ .sort((a, b) => a.from - b.from);
return Decoration.set(decorations);
},
@@ -70,56 +83,15 @@ export const CodePreview = ({
},
provide: (field) => EditorView.decorations.from(field),
}),
- ];
- }, [ranges, lineOffset]);
-
- const extensions = useMemo(() => {
- return [
- syntaxHighlighting,
- EditorView.lineWrapping,
- lineOffsetExtension(lineOffset),
- rangeHighlighting,
- ];
- }, [syntaxHighlighting, lineOffset, rangeHighlighting]);
+ ]
+ }, [language, lineOffset, ranges, theme]);
return (
-
)
+
}
\ No newline at end of file
diff --git a/packages/web/src/app/search/components/searchResultsPanel/fileMatch.tsx b/packages/web/src/app/search/components/searchResultsPanel/fileMatch.tsx
index 635fb5b9..981ad746 100644
--- a/packages/web/src/app/search/components/searchResultsPanel/fileMatch.tsx
+++ b/packages/web/src/app/search/components/searchResultsPanel/fileMatch.tsx
@@ -29,7 +29,7 @@ export const FileMatch = ({
return (
{
if (e.key !== "Enter") {
return;
diff --git a/packages/web/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx b/packages/web/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx
index 02951f49..b7c7370f 100644
--- a/packages/web/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx
+++ b/packages/web/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx
@@ -1,7 +1,7 @@
'use client';
import { getRepoCodeHostInfo } from "@/lib/utils";
-import { useCallback, useMemo, useState } from "react";
+import { useCallback, useMemo } from "react";
import Image from "next/image";
import { DoubleArrowDownIcon, DoubleArrowUpIcon, FileIcon } from "@radix-ui/react-icons";
import clsx from "clsx";
@@ -9,21 +9,24 @@ import { Separator } from "@/components/ui/separator";
import { SearchResultFile } from "@/lib/types";
import { FileMatch } from "./fileMatch";
-const MAX_MATCHES_TO_PREVIEW = 3;
+export const MAX_MATCHES_TO_PREVIEW = 3;
interface FileMatchContainerProps {
file: SearchResultFile;
onOpenFile: () => void;
onMatchIndexChanged: (matchIndex: number) => void;
+ showAllMatches: boolean;
+ onShowAllMatchesButtonClicked: () => void;
}
export const FileMatchContainer = ({
file,
onOpenFile,
onMatchIndexChanged,
+ showAllMatches,
+ onShowAllMatchesButtonClicked,
}: FileMatchContainerProps) => {
- const [showAll, setShowAll] = useState(false);
const matchCount = useMemo(() => {
return file.ChunkMatches.length;
}, [file]);
@@ -33,12 +36,12 @@ export const FileMatchContainer = ({
return a.ContentStart.LineNumber - b.ContentStart.LineNumber;
});
- if (!showAll) {
+ if (!showAllMatches) {
return sortedMatches.slice(0, MAX_MATCHES_TO_PREVIEW);
}
return sortedMatches;
- }, [file, showAll]);
+ }, [file, showAllMatches]);
const fileNameRange = useMemo(() => {
for (const match of matches) {
@@ -79,10 +82,6 @@ export const FileMatchContainer = ({
return matchCount > MAX_MATCHES_TO_PREVIEW;
}, [matchCount]);
- const onShowMoreMatches = useCallback(() => {
- setShowAll(!showAll);
- }, [showAll]);
-
const onOpenMatch = useCallback((index: number) => {
const matchIndex = matches.slice(0, index).reduce((acc, match) => {
return acc + match.Ranges.length;
@@ -94,8 +93,9 @@ export const FileMatchContainer = ({
return (
+ {/* Title */}
{
onOpenFile();
}}
@@ -132,6 +132,8 @@ export const FileMatchContainer = ({
+
+ {/* Matches */}
{matches.map((match, index) => (
))}
+
+ {/* Show more button */}
{isMoreContentButtonVisible && (
- {showAll ? : }
- {showAll ? `Show fewer matches` : `Show ${matchCount - MAX_MATCHES_TO_PREVIEW} more matches`}
+ {showAllMatches ? : }
+ {showAllMatches ? `Show fewer matches` : `Show ${matchCount - MAX_MATCHES_TO_PREVIEW} more matches`}
)}
diff --git a/packages/web/src/app/search/components/searchResultsPanel/index.tsx b/packages/web/src/app/search/components/searchResultsPanel/index.tsx
index 128ab9b2..717c335b 100644
--- a/packages/web/src/app/search/components/searchResultsPanel/index.tsx
+++ b/packages/web/src/app/search/components/searchResultsPanel/index.tsx
@@ -1,29 +1,164 @@
'use client';
import { SearchResultFile } from "@/lib/types";
-import { FileMatchContainer } from "./fileMatchContainer";
+import { FileMatchContainer, MAX_MATCHES_TO_PREVIEW } from "./fileMatchContainer";
+import { useVirtualizer } from "@tanstack/react-virtual";
+import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
interface SearchResultsPanelProps {
fileMatches: SearchResultFile[];
onOpenFileMatch: (fileMatch: SearchResultFile) => void;
onMatchIndexChanged: (matchIndex: number) => void;
+ isLoadMoreButtonVisible: boolean;
+ onLoadMoreButtonClicked: () => void;
}
+const ESTIMATED_LINE_HEIGHT_PX = 20;
+const ESTIMATED_NUMBER_OF_LINES_PER_CODE_CELL = 10;
+const ESTIMATED_MATCH_CONTAINER_HEIGHT_PX = 30;
+
export const SearchResultsPanel = ({
fileMatches,
onOpenFileMatch,
onMatchIndexChanged,
+ isLoadMoreButtonVisible,
+ onLoadMoreButtonClicked,
}: SearchResultsPanelProps) => {
- return fileMatches.map((fileMatch, index) => (
-
{
- onOpenFileMatch(fileMatch);
- }}
- onMatchIndexChanged={(matchIndex) => {
- onMatchIndexChanged(matchIndex);
+ const parentRef = useRef(null);
+ const [showAllMatchesStates, setShowAllMatchesStates] = useState(Array(fileMatches.length).fill(false));
+ const [lastShowAllMatchesButtonClickIndex, setLastShowAllMatchesButtonClickIndex] = useState(-1);
+
+ const virtualizer = useVirtualizer({
+ count: fileMatches.length,
+ getScrollElement: () => parentRef.current,
+ estimateSize: (index) => {
+ const fileMatch = fileMatches[index];
+ const showAllMatches = showAllMatchesStates[index];
+
+ // Quick guesstimation ;) This needs to be quick since the virtualizer will
+ // run this upfront for all items in the list.
+ const numCodeCells = fileMatch.ChunkMatches
+ .filter(match => !match.FileName)
+ .slice(0, showAllMatches ? fileMatch.ChunkMatches.length : MAX_MATCHES_TO_PREVIEW)
+ .length;
+
+ const estimatedSize =
+ numCodeCells * ESTIMATED_NUMBER_OF_LINES_PER_CODE_CELL * ESTIMATED_LINE_HEIGHT_PX +
+ ESTIMATED_MATCH_CONTAINER_HEIGHT_PX;
+
+ return estimatedSize;
+ },
+ measureElement: (element, _entry, instance) => {
+ // @note : Stutters were appearing when scrolling upwards. The workaround is
+ // to use the cached height of the element when scrolling up.
+ // @see : https://github.com/TanStack/virtual/issues/659
+ const isCacheDirty = element.hasAttribute("data-cache-dirty");
+ element.removeAttribute("data-cache-dirty");
+ const direction = instance.scrollDirection;
+ if (direction === "forward" || direction === null || isCacheDirty) {
+ return element.scrollHeight;
+ } else {
+ const indexKey = Number(element.getAttribute("data-index"));
+ // Unfortunately, the cache is a private property, so we need to
+ // hush the TS compiler.
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ const cacheMeasurement = instance.itemSizeCache.get(indexKey);
+ return cacheMeasurement;
+ }
+ },
+ enabled: true,
+ overscan: 10,
+ debug: false,
+ });
+
+ const onShowAllMatchesButtonClicked = useCallback((index: number) => {
+ const states = [...showAllMatchesStates];
+ states[index] = !states[index];
+ setShowAllMatchesStates(states);
+ setLastShowAllMatchesButtonClickIndex(index);
+ }, [showAllMatchesStates]);
+
+ // After the "show N more/less matches" button is clicked, the FileMatchContainer's
+ // size can change considerably. In cases where N > 3 or 4 cells when collapsing,
+ // a visual artifact can appear where there is a large gap between the now collapsed
+ // container and the next container. This is because the container's height was not
+ // re-calculated. To get arround this, we force a re-measure of the element AFTER
+ // it was re-rendered (hence the useLayoutEffect).
+ useLayoutEffect(() => {
+ if (lastShowAllMatchesButtonClickIndex < 0) {
+ return;
+ }
+
+ const element = virtualizer.elementsCache.get(lastShowAllMatchesButtonClickIndex);
+ element?.setAttribute('data-cache-dirty', 'true');
+ virtualizer.measureElement(element);
+
+ setLastShowAllMatchesButtonClickIndex(-1);
+ }, [lastShowAllMatchesButtonClickIndex, virtualizer]);
+
+ // Reset some state when the file matches change.
+ useEffect(() => {
+ setShowAllMatchesStates(Array(fileMatches.length).fill(false));
+ virtualizer.scrollToIndex(0);
+ }, [fileMatches, virtualizer]);
+
+ return (
+
- ))
+ >
+
+ {virtualizer.getVirtualItems().map((virtualRow) => (
+
+ {
+ onOpenFileMatch(fileMatches[virtualRow.index]);
+ }}
+ onMatchIndexChanged={(matchIndex) => {
+ onMatchIndexChanged(matchIndex);
+ }}
+ showAllMatches={showAllMatchesStates[virtualRow.index]}
+ onShowAllMatchesButtonClicked={() => {
+ onShowAllMatchesButtonClicked(virtualRow.index);
+ }}
+ />
+
+ ))}
+
+ {isLoadMoreButtonVisible && (
+
+
+ Load more results
+
+
+ )}
+
+ )
}
\ No newline at end of file
diff --git a/packages/web/src/app/search/components/searchResultsPanel/lightweightCodeMirror.tsx b/packages/web/src/app/search/components/searchResultsPanel/lightweightCodeMirror.tsx
new file mode 100644
index 00000000..5cc774b4
--- /dev/null
+++ b/packages/web/src/app/search/components/searchResultsPanel/lightweightCodeMirror.tsx
@@ -0,0 +1,86 @@
+'use client';
+
+import { EditorState, Extension, StateEffect } from "@codemirror/state";
+import { EditorView } from "@codemirror/view";
+import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
+
+interface CodeMirrorProps {
+ value?: string;
+ extensions?: Extension[];
+ className?: string;
+}
+
+export interface CodeMirrorRef {
+ editor: HTMLDivElement | null;
+ state?: EditorState;
+ view?: EditorView;
+}
+
+/**
+ * This component provides a lightweight CodeMirror component that has been optimized to
+ * render quickly in the search results panel. Why not use react-codemirror? For whatever reason,
+ * react-codemirror issues many StateEffects when first rendering, causing a stuttery scroll
+ * experience as new cells load. This component is a workaround for that issue and provides
+ * a minimal react wrapper around CodeMirror that avoids this issue.
+ */
+const LightweightCodeMirror = forwardRef(({
+ value,
+ extensions,
+ className,
+}, ref) => {
+ const editor = useRef(null);
+ const [view, setView] = useState();
+ const [state, setState] = useState();
+
+ useImperativeHandle(ref, () => ({
+ editor: editor.current,
+ state,
+ view,
+ }), [editor, state, view]);
+
+ useEffect(() => {
+ if (!editor.current) {
+ return;
+ }
+
+ const state = EditorState.create({
+ extensions: [], /* extensions are explicitly left out here */
+ doc: value,
+ });
+ setState(state);
+
+ const view = new EditorView({
+ state,
+ parent: editor.current,
+ });
+ setView(view);
+
+ // console.debug(`[CM] Editor created.`);
+
+ return () => {
+ view.destroy();
+ setView(undefined);
+ setState(undefined);
+ // console.debug(`[CM] Editor destroyed.`);
+ }
+
+ }, [value]);
+
+ useEffect(() => {
+ if (view) {
+ view.dispatch({ effects: StateEffect.reconfigure.of(extensions ?? []) });
+ // console.debug(`[CM] Editor reconfigured.`);
+ }
+ }, [extensions, view]);
+
+ return (
+
+ )
+});
+
+LightweightCodeMirror.displayName = "LightweightCodeMirror";
+
+export { LightweightCodeMirror };
\ No newline at end of file
diff --git a/packages/web/src/app/search/page.tsx b/packages/web/src/app/search/page.tsx
index 09316ac3..5f8563af 100644
--- a/packages/web/src/app/search/page.tsx
+++ b/packages/web/src/app/search/page.tsx
@@ -5,14 +5,12 @@ import {
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
-import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import useCaptureEvent from "@/hooks/useCaptureEvent";
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
import { SearchQueryParams, SearchResultFile } from "@/lib/types";
import { createPathWithQueryParams } from "@/lib/utils";
import { SymbolIcon } from "@radix-ui/react-icons";
-import { Scrollbar } from "@radix-ui/react-scroll-area";
import { useQuery } from "@tanstack/react-query";
import Image from "next/image";
import { useRouter } from "next/navigation";
@@ -27,7 +25,7 @@ import { FilterPanel } from "./components/filterPanel";
import { SearchResultsPanel } from "./components/searchResultsPanel";
import { ImperativePanelHandle } from "react-resizable-panels";
-const DEFAULT_MAX_MATCH_DISPLAY_COUNT = 200;
+const DEFAULT_MAX_MATCH_DISPLAY_COUNT = 10000;
export default function SearchPage() {
const router = useRouter();
@@ -212,6 +210,10 @@ const PanelGroup = ({
}
}, [selectedFile]);
+ const onFilterChanged = useCallback((matches: SearchResultFile[]) => {
+ setFilteredFileMatches(matches);
+ }, []);
+
return (
{
- setFilteredFileMatches(filteredFileMatches)
- }}
+ onFilterChanged={onFilterChanged}
/>
{filteredFileMatches.length > 0 ? (
-
- {
- setSelectedFile(fileMatch);
- }}
- onMatchIndexChanged={(matchIndex) => {
- setSelectedMatchIndex(matchIndex);
- }}
- />
- {isMoreResultsButtonVisible && (
-
-
- Load more results
-
-
- )}
-
-
+ {
+ setSelectedFile(fileMatch);
+ }}
+ onMatchIndexChanged={(matchIndex) => {
+ setSelectedMatchIndex(matchIndex);
+ }}
+ isLoadMoreButtonVisible={!!isMoreResultsButtonVisible}
+ onLoadMoreButtonClicked={onLoadMoreResults}
+ />
) : (
No results found
diff --git a/packages/web/src/hooks/useSyntaxHighlightingExtension.ts b/packages/web/src/hooks/useSyntaxHighlightingExtension.ts
index af6acd6f..8318e654 100644
--- a/packages/web/src/hooks/useSyntaxHighlightingExtension.ts
+++ b/packages/web/src/hooks/useSyntaxHighlightingExtension.ts
@@ -21,46 +21,50 @@ export const useSyntaxHighlightingExtension = (language: string, view: EditorVie
const extension = useExtensionWithDependency(
view ?? null,
() => {
- switch (language.toLowerCase()) {
- case "c":
- case "c++":
- return cpp();
- case "c#":
- return csharp();
- case "json":
- return json();
- case "java":
- return java();
- case "rust":
- return rust();
- case "go":
- return go();
- case "sql":
- return sql();
- case "php":
- return php();
- case "html":
- return html();
- case "css":
- return css();
- case "jsx":
- case "tsx":
- case "typescript":
- case "javascript":
- return javascript({
- jsx: true,
- typescript: true,
- });
- case "python":
- return python();
- case "markdown":
- return markdown();
- default:
- return [];
- }
+ return getSyntaxHighlightingExtension(language);
},
[language]
);
return extension;
+}
+
+export const getSyntaxHighlightingExtension = (language: string) => {
+ switch (language.toLowerCase()) {
+ case "c":
+ case "c++":
+ return cpp();
+ case "c#":
+ return csharp();
+ case "json":
+ return json();
+ case "java":
+ return java();
+ case "rust":
+ return rust();
+ case "go":
+ return go();
+ case "sql":
+ return sql();
+ case "php":
+ return php();
+ case "html":
+ return html();
+ case "css":
+ return css();
+ case "jsx":
+ case "tsx":
+ case "typescript":
+ case "javascript":
+ return javascript({
+ jsx: true,
+ typescript: true,
+ });
+ case "python":
+ return python();
+ case "markdown":
+ return markdown();
+ default:
+ return [];
+ }
}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 6ffb2bc5..ddca8307 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1305,11 +1305,23 @@
dependencies:
"@tanstack/table-core" "8.20.5"
+"@tanstack/react-virtual@^3.10.8":
+ version "3.10.8"
+ resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz#bf4b06f157ed298644a96ab7efc1a2b01ab36e3c"
+ integrity sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==
+ dependencies:
+ "@tanstack/virtual-core" "3.10.8"
+
"@tanstack/table-core@8.20.5":
version "8.20.5"
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d"
integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==
+"@tanstack/virtual-core@3.10.8":
+ version "3.10.8"
+ resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz#975446a667755222f62884c19e5c3c66d959b8b4"
+ integrity sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==
+
"@types/argparse@^2.0.16":
version "2.0.16"
resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-2.0.16.tgz#3bb7ccd2844b3a8bcd6efbd217f6c0ea06a80d22"