sourcebot/src/app/search/codePreviewPanel.tsx

181 lines
6.6 KiB
TypeScript
Raw Normal View History

'use client';
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
2024-08-30 00:47:35 +00:00
import { useExtensionWithDependency } from "@/hooks/useExtensionWithDependency";
import { useKeymapType } from "@/hooks/useKeymapType";
import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension";
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
2024-08-30 00:47:35 +00:00
import { gutterWidthExtension } from "@/lib/extensions/gutterWidthExtension";
import { highlightRanges, searchResultHighlightExtension } from "@/lib/extensions/searchResultHighlightExtension";
import { SearchResultFileMatch } from "@/lib/schemas";
import { defaultKeymap } from "@codemirror/commands";
import { javascript } from "@codemirror/lang-javascript";
2024-08-30 00:47:35 +00:00
import { search } from "@codemirror/search";
import { EditorView, keymap } from "@codemirror/view";
import { Cross1Icon, FileIcon } from "@radix-ui/react-icons";
import { Scrollbar } from "@radix-ui/react-scroll-area";
2024-08-30 00:47:35 +00:00
import { vim } from "@replit/codemirror-vim";
import CodeMirror, { ReactCodeMirrorRef } from '@uiw/react-codemirror';
2024-09-07 01:24:39 +00:00
import clsx from "clsx";
import { ArrowDown, ArrowUp } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2024-08-30 00:47:35 +00:00
export interface CodePreviewFile {
content: string;
filepath: string;
2024-09-07 01:24:39 +00:00
link?: string;
matches: SearchResultFileMatch[];
language: string;
2024-08-30 00:47:35 +00:00
}
interface CodePreviewPanelProps {
2024-08-30 00:47:35 +00:00
file?: CodePreviewFile;
selectedMatchIndex: number;
onSelectedMatchIndexChange: (index: number) => void;
onClose: () => void;
}
export const CodePreviewPanel = ({
2024-08-30 00:47:35 +00:00
file,
selectedMatchIndex,
onSelectedMatchIndexChange,
onClose,
}: CodePreviewPanelProps) => {
2024-08-30 00:47:35 +00:00
const editorRef = useRef<ReactCodeMirrorRef>(null);
const [ keymapType ] = useKeymapType();
const { theme } = useThemeNormalized();
const [gutterWidth, setGutterWidth] = useState(0);
2024-08-30 00:47:35 +00:00
const keymapExtension = useExtensionWithDependency(
editorRef.current?.view ?? null,
() => {
switch (keymapType) {
case "default":
return keymap.of(defaultKeymap);
case "vim":
return vim();
}
2024-08-30 00:47:35 +00:00
},
[keymapType]
);
const syntaxHighlighting = useSyntaxHighlightingExtension(file?.language ?? '', editorRef.current?.view);
const extensions = useMemo(() => {
return [
keymapExtension,
gutterWidthExtension,
javascript(),
syntaxHighlighting,
searchResultHighlightExtension(),
search({
top: true,
}),
EditorView.updateListener.of(update => {
const width = update.view.plugin(gutterWidthExtension)?.width;
if (width) {
setGutterWidth(width);
}
}),
];
}, [keymapExtension, syntaxHighlighting]);
const ranges = useMemo(() => {
if (!file || !file.matches.length) {
return [];
}
return file.matches.flatMap((match) => {
return match.Ranges;
})
}, [file]);
2024-08-30 00:47:35 +00:00
useEffect(() => {
if (!file || !editorRef.current?.view) {
return;
}
highlightRanges(selectedMatchIndex, ranges, editorRef.current.view);
2024-09-11 05:23:40 +00:00
}, [ranges, selectedMatchIndex, file]);
const onUpClicked = useCallback(() => {
onSelectedMatchIndexChange(selectedMatchIndex - 1);
2024-08-30 04:38:48 +00:00
}, [onSelectedMatchIndexChange, selectedMatchIndex]);
const onDownClicked = useCallback(() => {
onSelectedMatchIndexChange(selectedMatchIndex + 1);
2024-08-30 04:38:48 +00:00
}, [onSelectedMatchIndexChange, selectedMatchIndex]);
return (
<div className="flex flex-col h-full">
2024-09-10 19:24:47 +00:00
<div className="flex flex-row bg-cyan-200 dark:bg-cyan-900 items-center justify-between pr-3 py-0.5">
<div className="flex flex-row">
<div
style={{ width: `${gutterWidth}px` }}
className="flex justify-center items-center"
>
<FileIcon className="h-4 w-4" />
</div>
2024-09-07 01:24:39 +00:00
<span
className={clsx("", {
"cursor-pointer text-blue-500 hover:underline" : file?.link
})}
onClick={() => {
if (file?.link) {
window.open(file.link, "_blank");
}
}}
>
{file?.filepath}
</span>
</div>
<div className="flex flex-row gap-1 items-center">
2024-09-10 19:24:47 +00:00
{file && file.matches.length > 0 && (
<>
<p className="text-sm">{`${selectedMatchIndex + 1} of ${ranges.length}`}</p>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
disabled={selectedMatchIndex === 0}
onClick={onUpClicked}
>
<ArrowUp className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={onDownClicked}
disabled={file ? selectedMatchIndex === ranges.length - 1 : true}
>
<ArrowDown className="h-4 w-4" />
</Button>
</>
)}
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={onClose}
>
<Cross1Icon className="h-4 w-4" />
</Button>
</div>
</div>
<ScrollArea className="h-full overflow-auto flex-1">
<CodeMirror
2024-08-30 00:47:35 +00:00
ref={editorRef}
readOnly={true}
2024-08-30 00:47:35 +00:00
value={file?.content}
theme={theme === "dark" ? "dark" : "light"}
extensions={extensions}
/>
<Scrollbar orientation="vertical" />
<Scrollbar orientation="horizontal" />
</ScrollArea>
</div>
)
}