mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-11 20:05:25 +00:00
Improve rendering performance of search results (#52)
This commit is contained in:
parent
8a619b7145
commit
e913b22324
14 changed files with 362 additions and 151 deletions
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
}
|
||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
|
|
@ -2,5 +2,10 @@
|
|||
"files.associations": {
|
||||
"*.json": "jsonc",
|
||||
"index.json": "json"
|
||||
}
|
||||
},
|
||||
"eslint.workingDirectories": [
|
||||
{
|
||||
"pattern": "./packages/*/"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -59,5 +59,4 @@ export const CodePreviewPanel = ({
|
|||
onSelectedMatchIndexChange={onSelectedMatchIndexChange}
|
||||
/>
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ export const FilterPanel = ({
|
|||
);
|
||||
|
||||
onFilterChanged(filteredMatches);
|
||||
}, [matches, repos, languages]);
|
||||
}, [matches, repos, languages, onFilterChanged]);
|
||||
|
||||
return (
|
||||
<div className="p-3 flex flex-col gap-3">
|
||||
|
|
|
|||
|
|
@ -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<ReactCodeMirrorRef>(null);
|
||||
const editorRef = useRef<CodeMirrorRef>(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<DecorationSet>({
|
||||
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 (
|
||||
<CodeMirror
|
||||
<LightweightCodeMirror
|
||||
ref={editorRef}
|
||||
readOnly={true}
|
||||
editable={false}
|
||||
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}
|
||||
/>
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ export const FileMatch = ({
|
|||
return (
|
||||
<div
|
||||
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) => {
|
||||
if (e.key !== "Enter") {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div>
|
||||
{/* Title */}
|
||||
<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={() => {
|
||||
onOpenFile();
|
||||
}}
|
||||
|
|
@ -132,6 +132,8 @@ export const FileMatchContainer = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Matches */}
|
||||
{matches.map((match, index) => (
|
||||
<div
|
||||
key={index}
|
||||
|
|
@ -148,6 +150,8 @@ export const FileMatchContainer = ({
|
|||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Show more button */}
|
||||
{isMoreContentButtonVisible && (
|
||||
<div
|
||||
tabIndex={0}
|
||||
|
|
@ -156,15 +160,15 @@ export const FileMatchContainer = ({
|
|||
if (e.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
onShowMoreMatches();
|
||||
onShowAllMatchesButtonClicked();
|
||||
}}
|
||||
onClick={onShowMoreMatches}
|
||||
onClick={onShowAllMatchesButtonClicked}
|
||||
>
|
||||
<p
|
||||
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" />}
|
||||
{showAll ? `Show fewer matches` : `Show ${matchCount - MAX_MATCHES_TO_PREVIEW} more matches`}
|
||||
{showAllMatches ? <DoubleArrowUpIcon className="w-3 h-3" /> : <DoubleArrowDownIcon className="w-3 h-3" />}
|
||||
{showAllMatches ? `Show fewer matches` : `Show ${matchCount - MAX_MATCHES_TO_PREVIEW} more matches`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
<FileMatchContainer
|
||||
key={index}
|
||||
file={fileMatch}
|
||||
onOpenFile={() => {
|
||||
onOpenFileMatch(fileMatch);
|
||||
const parentRef = useRef<HTMLDivElement>(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 (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 (
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
|
|
@ -227,9 +229,7 @@ const PanelGroup = ({
|
|||
>
|
||||
<FilterPanel
|
||||
matches={fileMatches}
|
||||
onFilterChanged={(filteredFileMatches) => {
|
||||
setFilteredFileMatches(filteredFileMatches)
|
||||
}}
|
||||
onFilterChanged={onFilterChanged}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle
|
||||
|
|
@ -243,30 +243,17 @@ const PanelGroup = ({
|
|||
order={2}
|
||||
>
|
||||
{filteredFileMatches.length > 0 ? (
|
||||
<ScrollArea
|
||||
className="h-full"
|
||||
>
|
||||
<SearchResultsPanel
|
||||
fileMatches={filteredFileMatches}
|
||||
onOpenFileMatch={(fileMatch) => {
|
||||
setSelectedFile(fileMatch);
|
||||
}}
|
||||
onMatchIndexChanged={(matchIndex) => {
|
||||
setSelectedMatchIndex(matchIndex);
|
||||
}}
|
||||
/>
|
||||
{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>
|
||||
<SearchResultsPanel
|
||||
fileMatches={filteredFileMatches}
|
||||
onOpenFileMatch={(fileMatch) => {
|
||||
setSelectedFile(fileMatch);
|
||||
}}
|
||||
onMatchIndexChanged={(matchIndex) => {
|
||||
setSelectedMatchIndex(matchIndex);
|
||||
}}
|
||||
isLoadMoreButtonVisible={!!isMoreResultsButtonVisible}
|
||||
onLoadMoreButtonClicked={onLoadMoreResults}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<p className="text-sm text-muted-foreground">No results found</p>
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
12
yarn.lock
12
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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue