Improve rendering performance of search results (#52)

This commit is contained in:
Brendan Kellam 2024-10-30 09:32:05 -07:00 committed by GitHub
parent 8a619b7145
commit e913b22324
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 362 additions and 151 deletions

5
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"recommendations": [
"dbaeumer.vscode-eslint"
]
}

View file

@ -2,5 +2,10 @@
"files.associations": { "files.associations": {
"*.json": "jsonc", "*.json": "jsonc",
"index.json": "json" "index.json": "json"
} },
"eslint.workingDirectories": [
{
"pattern": "./packages/*/"
}
]
} }

View file

@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fixed issue with GitLab sub-projects not being included recursively. ([#54](https://github.com/sourcebot-dev/sourcebot/pull/54)) - 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 ## [2.1.1] - 2024-10-25

View file

@ -38,6 +38,7 @@
"@replit/codemirror-vim": "^6.2.1", "@replit/codemirror-vim": "^6.2.1",
"@tanstack/react-query": "^5.53.3", "@tanstack/react-query": "^5.53.3",
"@tanstack/react-table": "^8.20.5", "@tanstack/react-table": "^8.20.5",
"@tanstack/react-virtual": "^3.10.8",
"@uiw/react-codemirror": "^4.23.0", "@uiw/react-codemirror": "^4.23.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"client-only": "^0.0.1", "client-only": "^0.0.1",

View file

@ -59,5 +59,4 @@ export const CodePreviewPanel = ({
onSelectedMatchIndexChange={onSelectedMatchIndexChange} onSelectedMatchIndexChange={onSelectedMatchIndexChange}
/> />
) )
} }

View file

@ -93,7 +93,7 @@ export const FilterPanel = ({
); );
onFilterChanged(filteredMatches); onFilterChanged(filteredMatches);
}, [matches, repos, languages]); }, [matches, repos, languages, onFilterChanged]);
return ( return (
<div className="p-3 flex flex-col gap-3"> <div className="p-3 flex flex-col gap-3">

View file

@ -1,12 +1,15 @@
'use client'; 'use client';
import { useExtensionWithDependency } from "@/hooks/useExtensionWithDependency"; import { getSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension";
import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension";
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
import { lineOffsetExtension } from "@/lib/extensions/lineOffsetExtension"; import { lineOffsetExtension } from "@/lib/extensions/lineOffsetExtension";
import { SearchResultRange } from "@/lib/types"; 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 { useMemo, useRef } from "react";
import { LightweightCodeMirror, CodeMirrorRef } from "./lightweightCodeMirror";
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
const markDecoration = Decoration.mark({ const markDecoration = Decoration.mark({
class: "cm-searchMatch-selected" class: "cm-searchMatch-selected"
@ -25,13 +28,22 @@ export const CodePreview = ({
ranges, ranges,
lineOffset, lineOffset,
}: CodePreviewProps) => { }: CodePreviewProps) => {
const editorRef = useRef<ReactCodeMirrorRef>(null); const editorRef = useRef<CodeMirrorRef>(null);
const { theme } = useThemeNormalized(); const { theme } = useThemeNormalized();
const syntaxHighlighting = useSyntaxHighlightingExtension(language, editorRef.current?.view); const extensions = useMemo(() => {
const rangeHighlighting = useExtensionWithDependency(editorRef.current?.view ?? null, () => {
return [ return [
EditorView.editable.of(false),
...(theme === 'dark' ? [
syntaxHighlighting(oneDarkHighlightStyle),
oneDarkTheme,
] : [
syntaxHighlighting(defaultHighlightStyle),
defaultLightThemeOption,
]),
lineNumbers(),
lineOffsetExtension(lineOffset),
getSyntaxHighlightingExtension(language),
StateField.define<DecorationSet>({ StateField.define<DecorationSet>({
create(editorState: EditorState) { create(editorState: EditorState) {
const document = editorState.doc; const document = editorState.doc;
@ -61,7 +73,8 @@ export const CodePreview = ({
const from = document.line(startLine).from + Start.Column - 1; const from = document.line(startLine).from + Start.Column - 1;
const to = document.line(endLine).from + End.Column - 1; const to = document.line(endLine).from + End.Column - 1;
return markDecoration.range(from, to); return markDecoration.range(from, to);
}); })
.sort((a, b) => a.from - b.from);
return Decoration.set(decorations); return Decoration.set(decorations);
}, },
@ -70,56 +83,15 @@ export const CodePreview = ({
}, },
provide: (field) => EditorView.decorations.from(field), provide: (field) => EditorView.decorations.from(field),
}), }),
]; ]
}, [ranges, lineOffset]); }, [language, lineOffset, ranges, theme]);
const extensions = useMemo(() => {
return [
syntaxHighlighting,
EditorView.lineWrapping,
lineOffsetExtension(lineOffset),
rangeHighlighting,
];
}, [syntaxHighlighting, lineOffset, rangeHighlighting]);
return ( return (
<CodeMirror <LightweightCodeMirror
ref={editorRef} ref={editorRef}
readOnly={true}
editable={false}
value={content} value={content}
theme={theme === "dark" ? "dark" : "light"}
basicSetup={{
lineNumbers: true,
syntaxHighlighting: true,
// Disable all this other stuff...
... {
foldGutter: false,
highlightActiveLineGutter: false,
highlightSpecialChars: false,
history: false,
drawSelection: false,
dropCursor: false,
allowMultipleSelections: false,
indentOnInput: false,
bracketMatching: false,
closeBrackets: false,
autocompletion: false,
rectangularSelection: false,
crosshairCursor: false,
highlightActiveLine: false,
highlightSelectionMatches: false,
closeBracketsKeymap: false,
defaultKeymap: false,
searchKeymap: false,
historyKeymap: false,
foldKeymap: false,
completionKeymap: false,
lintKeymap: false,
}
}}
extensions={extensions} extensions={extensions}
/> />
) )
} }

View file

@ -29,7 +29,7 @@ export const FileMatch = ({
return ( return (
<div <div
tabIndex={0} tabIndex={0}
className="cursor-pointer p-1 focus:ring-inset focus:ring-4 bg-white dark:bg-[#282c34]" className="cursor-pointer focus:ring-inset focus:ring-4 bg-white dark:bg-[#282c34]"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key !== "Enter") { if (e.key !== "Enter") {
return; return;

View file

@ -1,7 +1,7 @@
'use client'; 'use client';
import { getRepoCodeHostInfo } from "@/lib/utils"; import { getRepoCodeHostInfo } from "@/lib/utils";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo } from "react";
import Image from "next/image"; import Image from "next/image";
import { DoubleArrowDownIcon, DoubleArrowUpIcon, FileIcon } from "@radix-ui/react-icons"; import { DoubleArrowDownIcon, DoubleArrowUpIcon, FileIcon } from "@radix-ui/react-icons";
import clsx from "clsx"; import clsx from "clsx";
@ -9,21 +9,24 @@ import { Separator } from "@/components/ui/separator";
import { SearchResultFile } from "@/lib/types"; import { SearchResultFile } from "@/lib/types";
import { FileMatch } from "./fileMatch"; import { FileMatch } from "./fileMatch";
const MAX_MATCHES_TO_PREVIEW = 3; export const MAX_MATCHES_TO_PREVIEW = 3;
interface FileMatchContainerProps { interface FileMatchContainerProps {
file: SearchResultFile; file: SearchResultFile;
onOpenFile: () => void; onOpenFile: () => void;
onMatchIndexChanged: (matchIndex: number) => void; onMatchIndexChanged: (matchIndex: number) => void;
showAllMatches: boolean;
onShowAllMatchesButtonClicked: () => void;
} }
export const FileMatchContainer = ({ export const FileMatchContainer = ({
file, file,
onOpenFile, onOpenFile,
onMatchIndexChanged, onMatchIndexChanged,
showAllMatches,
onShowAllMatchesButtonClicked,
}: FileMatchContainerProps) => { }: FileMatchContainerProps) => {
const [showAll, setShowAll] = useState(false);
const matchCount = useMemo(() => { const matchCount = useMemo(() => {
return file.ChunkMatches.length; return file.ChunkMatches.length;
}, [file]); }, [file]);
@ -33,12 +36,12 @@ export const FileMatchContainer = ({
return a.ContentStart.LineNumber - b.ContentStart.LineNumber; return a.ContentStart.LineNumber - b.ContentStart.LineNumber;
}); });
if (!showAll) { if (!showAllMatches) {
return sortedMatches.slice(0, MAX_MATCHES_TO_PREVIEW); return sortedMatches.slice(0, MAX_MATCHES_TO_PREVIEW);
} }
return sortedMatches; return sortedMatches;
}, [file, showAll]); }, [file, showAllMatches]);
const fileNameRange = useMemo(() => { const fileNameRange = useMemo(() => {
for (const match of matches) { for (const match of matches) {
@ -79,10 +82,6 @@ export const FileMatchContainer = ({
return matchCount > MAX_MATCHES_TO_PREVIEW; return matchCount > MAX_MATCHES_TO_PREVIEW;
}, [matchCount]); }, [matchCount]);
const onShowMoreMatches = useCallback(() => {
setShowAll(!showAll);
}, [showAll]);
const onOpenMatch = useCallback((index: number) => { const onOpenMatch = useCallback((index: number) => {
const matchIndex = matches.slice(0, index).reduce((acc, match) => { const matchIndex = matches.slice(0, index).reduce((acc, match) => {
return acc + match.Ranges.length; return acc + match.Ranges.length;
@ -94,8 +93,9 @@ export const FileMatchContainer = ({
return ( return (
<div> <div>
{/* Title */}
<div <div
className="sticky top-0 bg-cyan-200 dark:bg-cyan-900 primary-foreground px-2 py-0.5 flex flex-row items-center justify-between cursor-pointer z-10" className="top-0 bg-cyan-200 dark:bg-cyan-900 primary-foreground px-2 py-0.5 flex flex-row items-center justify-between cursor-pointer"
onClick={() => { onClick={() => {
onOpenFile(); onOpenFile();
}} }}
@ -132,6 +132,8 @@ export const FileMatchContainer = ({
</div> </div>
</div> </div>
</div> </div>
{/* Matches */}
{matches.map((match, index) => ( {matches.map((match, index) => (
<div <div
key={index} key={index}
@ -148,6 +150,8 @@ export const FileMatchContainer = ({
)} )}
</div> </div>
))} ))}
{/* Show more button */}
{isMoreContentButtonVisible && ( {isMoreContentButtonVisible && (
<div <div
tabIndex={0} tabIndex={0}
@ -156,15 +160,15 @@ export const FileMatchContainer = ({
if (e.key !== "Enter") { if (e.key !== "Enter") {
return; return;
} }
onShowMoreMatches(); onShowAllMatchesButtonClicked();
}} }}
onClick={onShowMoreMatches} onClick={onShowAllMatchesButtonClicked}
> >
<p <p
className="text-blue-500 cursor-pointer text-sm flex flex-row items-center gap-2" className="text-blue-500 cursor-pointer text-sm flex flex-row items-center gap-2"
> >
{showAll ? <DoubleArrowUpIcon className="w-3 h-3" /> : <DoubleArrowDownIcon className="w-3 h-3" />} {showAllMatches ? <DoubleArrowUpIcon className="w-3 h-3" /> : <DoubleArrowDownIcon className="w-3 h-3" />}
{showAll ? `Show fewer matches` : `Show ${matchCount - MAX_MATCHES_TO_PREVIEW} more matches`} {showAllMatches ? `Show fewer matches` : `Show ${matchCount - MAX_MATCHES_TO_PREVIEW} more matches`}
</p> </p>
</div> </div>
)} )}

View file

@ -1,29 +1,164 @@
'use client'; 'use client';
import { SearchResultFile } from "@/lib/types"; 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 { interface SearchResultsPanelProps {
fileMatches: SearchResultFile[]; fileMatches: SearchResultFile[];
onOpenFileMatch: (fileMatch: SearchResultFile) => void; onOpenFileMatch: (fileMatch: SearchResultFile) => void;
onMatchIndexChanged: (matchIndex: number) => 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 = ({ export const SearchResultsPanel = ({
fileMatches, fileMatches,
onOpenFileMatch, onOpenFileMatch,
onMatchIndexChanged, onMatchIndexChanged,
isLoadMoreButtonVisible,
onLoadMoreButtonClicked,
}: SearchResultsPanelProps) => { }: SearchResultsPanelProps) => {
return fileMatches.map((fileMatch, index) => ( const parentRef = useRef<HTMLDivElement>(null);
<FileMatchContainer const [showAllMatchesStates, setShowAllMatchesStates] = useState(Array(fileMatches.length).fill(false));
key={index} const [lastShowAllMatchesButtonClickIndex, setLastShowAllMatchesButtonClickIndex] = useState(-1);
file={fileMatch}
onOpenFile={() => { const virtualizer = useVirtualizer({
onOpenFileMatch(fileMatch); 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 (
<div
ref={parentRef}
style={{
width: "100%",
height: "100%",
overflowY: 'auto',
contain: 'strict',
}} }}
onMatchIndexChanged={(matchIndex) => { >
onMatchIndexChanged(matchIndex); <div
}} style={{
/> height: virtualizer.getTotalSize(),
)) width: "100%",
position: "relative",
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
data-index={virtualRow.index}
ref={virtualizer.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
}}
>
<FileMatchContainer
file={fileMatches[virtualRow.index]}
onOpenFile={() => {
onOpenFileMatch(fileMatches[virtualRow.index]);
}}
onMatchIndexChanged={(matchIndex) => {
onMatchIndexChanged(matchIndex);
}}
showAllMatches={showAllMatchesStates[virtualRow.index]}
onShowAllMatchesButtonClicked={() => {
onShowAllMatchesButtonClicked(virtualRow.index);
}}
/>
</div>
))}
</div>
{isLoadMoreButtonVisible && (
<div className="p-3">
<span
className="cursor-pointer text-blue-500 hover:underline"
onClick={onLoadMoreButtonClicked}
>
Load more results
</span>
</div>
)}
</div>
)
} }

View file

@ -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<CodeMirrorRef, CodeMirrorProps>(({
value,
extensions,
className,
}, ref) => {
const editor = useRef<HTMLDivElement | null>(null);
const [view, setView] = useState<EditorView>();
const [state, setState] = useState<EditorState>();
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 (
<div
className={className}
ref={editor}
/>
)
});
LightweightCodeMirror.displayName = "LightweightCodeMirror";
export { LightweightCodeMirror };

View file

@ -5,14 +5,12 @@ import {
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
} from "@/components/ui/resizable"; } from "@/components/ui/resizable";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import useCaptureEvent from "@/hooks/useCaptureEvent"; import useCaptureEvent from "@/hooks/useCaptureEvent";
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam"; import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
import { SearchQueryParams, SearchResultFile } from "@/lib/types"; import { SearchQueryParams, SearchResultFile } from "@/lib/types";
import { createPathWithQueryParams } from "@/lib/utils"; import { createPathWithQueryParams } from "@/lib/utils";
import { SymbolIcon } from "@radix-ui/react-icons"; import { SymbolIcon } from "@radix-ui/react-icons";
import { Scrollbar } from "@radix-ui/react-scroll-area";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@ -27,7 +25,7 @@ import { FilterPanel } from "./components/filterPanel";
import { SearchResultsPanel } from "./components/searchResultsPanel"; import { SearchResultsPanel } from "./components/searchResultsPanel";
import { ImperativePanelHandle } from "react-resizable-panels"; import { ImperativePanelHandle } from "react-resizable-panels";
const DEFAULT_MAX_MATCH_DISPLAY_COUNT = 200; const DEFAULT_MAX_MATCH_DISPLAY_COUNT = 10000;
export default function SearchPage() { export default function SearchPage() {
const router = useRouter(); const router = useRouter();
@ -212,6 +210,10 @@ const PanelGroup = ({
} }
}, [selectedFile]); }, [selectedFile]);
const onFilterChanged = useCallback((matches: SearchResultFile[]) => {
setFilteredFileMatches(matches);
}, []);
return ( return (
<ResizablePanelGroup <ResizablePanelGroup
direction="horizontal" direction="horizontal"
@ -227,9 +229,7 @@ const PanelGroup = ({
> >
<FilterPanel <FilterPanel
matches={fileMatches} matches={fileMatches}
onFilterChanged={(filteredFileMatches) => { onFilterChanged={onFilterChanged}
setFilteredFileMatches(filteredFileMatches)
}}
/> />
</ResizablePanel> </ResizablePanel>
<ResizableHandle <ResizableHandle
@ -243,30 +243,17 @@ const PanelGroup = ({
order={2} order={2}
> >
{filteredFileMatches.length > 0 ? ( {filteredFileMatches.length > 0 ? (
<ScrollArea <SearchResultsPanel
className="h-full" fileMatches={filteredFileMatches}
> onOpenFileMatch={(fileMatch) => {
<SearchResultsPanel setSelectedFile(fileMatch);
fileMatches={filteredFileMatches} }}
onOpenFileMatch={(fileMatch) => { onMatchIndexChanged={(matchIndex) => {
setSelectedFile(fileMatch); setSelectedMatchIndex(matchIndex);
}} }}
onMatchIndexChanged={(matchIndex) => { isLoadMoreButtonVisible={!!isMoreResultsButtonVisible}
setSelectedMatchIndex(matchIndex); onLoadMoreButtonClicked={onLoadMoreResults}
}} />
/>
{isMoreResultsButtonVisible && (
<div className="p-3">
<span
className="cursor-pointer text-blue-500 hover:underline"
onClick={onLoadMoreResults}
>
Load more results
</span>
</div>
)}
<Scrollbar orientation="vertical" />
</ScrollArea>
) : ( ) : (
<div className="flex flex-col items-center justify-center h-full"> <div className="flex flex-col items-center justify-center h-full">
<p className="text-sm text-muted-foreground">No results found</p> <p className="text-sm text-muted-foreground">No results found</p>

View file

@ -21,46 +21,50 @@ export const useSyntaxHighlightingExtension = (language: string, view: EditorVie
const extension = useExtensionWithDependency( const extension = useExtensionWithDependency(
view ?? null, view ?? null,
() => { () => {
switch (language.toLowerCase()) { return getSyntaxHighlightingExtension(language);
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 [];
}
}, },
[language] [language]
); );
return extension; 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 [];
}
}

View file

@ -1305,11 +1305,23 @@
dependencies: dependencies:
"@tanstack/table-core" "8.20.5" "@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": "@tanstack/table-core@8.20.5":
version "8.20.5" version "8.20.5"
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d" resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d"
integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg== 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": "@types/argparse@^2.0.16":
version "2.0.16" version "2.0.16"
resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-2.0.16.tgz#3bb7ccd2844b3a8bcd6efbd217f6c0ea06a80d22" resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-2.0.16.tgz#3bb7ccd2844b3a8bcd6efbd217f6c0ea06a80d22"