mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
Add result highlighting to code preview
This commit is contained in:
parent
4fc2357fb1
commit
f85c5cef02
3 changed files with 117 additions and 38 deletions
|
|
@ -14,8 +14,9 @@ import { Cross1Icon, FileIcon } from "@radix-ui/react-icons";
|
|||
import { Scrollbar } from "@radix-ui/react-scroll-area";
|
||||
import { vim } from "@replit/codemirror-vim";
|
||||
import CodeMirror, { ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
export interface CodePreviewFile {
|
||||
content: string;
|
||||
|
|
@ -26,16 +27,22 @@ export interface CodePreviewFile {
|
|||
interface CodePreviewProps {
|
||||
file?: CodePreviewFile;
|
||||
keymapType: "default" | "vim";
|
||||
selectedMatchIndex: number;
|
||||
onSelectedMatchIndexChange: (index: number) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const CodePreview = ({
|
||||
file,
|
||||
keymapType,
|
||||
selectedMatchIndex,
|
||||
onSelectedMatchIndexChange,
|
||||
onClose,
|
||||
}: CodePreviewProps) => {
|
||||
const editorRef = useRef<ReactCodeMirrorRef>(null);
|
||||
|
||||
const { theme: _theme, systemTheme } = useTheme();
|
||||
|
||||
const theme = useMemo(() => {
|
||||
if (_theme === "system") {
|
||||
return systemTheme ?? "light";
|
||||
|
|
@ -59,16 +66,42 @@ export const CodePreview = ({
|
|||
[keymapType]
|
||||
);
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
return [
|
||||
keymapExtension,
|
||||
gutterWidthExtension,
|
||||
javascript(),
|
||||
searchResultHighlightExtension(),
|
||||
search({
|
||||
top: true,
|
||||
}),
|
||||
EditorView.updateListener.of(update => {
|
||||
const width = update.view.plugin(gutterWidthExtension)?.width;
|
||||
if (width) {
|
||||
setGutterWidth(width);
|
||||
}
|
||||
}),
|
||||
];
|
||||
}, [keymapExtension]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!file || !editorRef.current?.view) {
|
||||
return;
|
||||
}
|
||||
|
||||
markMatches(file.matches, editorRef.current.view);
|
||||
}, [file?.matches]);
|
||||
markMatches(selectedMatchIndex, file.matches, editorRef.current.view);
|
||||
}, [file?.matches, selectedMatchIndex]);
|
||||
|
||||
const onUpClicked = useCallback(() => {
|
||||
onSelectedMatchIndexChange(selectedMatchIndex - 1);
|
||||
}, [selectedMatchIndex]);
|
||||
|
||||
const onDownClicked = useCallback(() => {
|
||||
onSelectedMatchIndexChange(selectedMatchIndex + 1);
|
||||
}, [selectedMatchIndex]);
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-row bg-cyan-200 dark:bg-cyan-900 items-center justify-between pr-3">
|
||||
<div className="flex flex-row">
|
||||
<div
|
||||
|
|
@ -79,34 +112,43 @@ export const CodePreview = ({
|
|||
</div>
|
||||
<span>{file?.filepath}</span>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6">
|
||||
<Cross1Icon
|
||||
className="h-4 w-4"
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
<p className="text-sm">{`${selectedMatchIndex + 1} of ${file?.matches.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 === file?.matches.length - 1 : true}
|
||||
>
|
||||
<ArrowDown className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Button>
|
||||
>
|
||||
<Cross1Icon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="h-full overflow-y-auto">
|
||||
<ScrollArea className="h-full overflow-auto flex-1">
|
||||
<CodeMirror
|
||||
ref={editorRef}
|
||||
readOnly={true}
|
||||
value={file?.content}
|
||||
theme={theme === "dark" ? "dark" : "light"}
|
||||
extensions={[
|
||||
keymapExtension,
|
||||
gutterWidthExtension,
|
||||
javascript(),
|
||||
searchResultHighlightExtension(),
|
||||
search({
|
||||
top: true,
|
||||
}),
|
||||
EditorView.updateListener.of(update => {
|
||||
const width = update.view.plugin(gutterWidthExtension)?.width;
|
||||
if (width) {
|
||||
setGutterWidth(width);
|
||||
}
|
||||
})
|
||||
]}
|
||||
extensions={extensions}
|
||||
/>
|
||||
<Scrollbar orientation="vertical" />
|
||||
</ScrollArea>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export default function Home() {
|
|||
|
||||
const [isCodePanelOpen, setIsCodePanelOpen] = useState(false);
|
||||
const [previewFile, setPreviewFile] = useState<CodePreviewFile | undefined>(undefined);
|
||||
const [selectedMatchIndex, setSelectedMatchIndex] = useState(0);
|
||||
|
||||
const [fileMatches, setFileMatches] = useState<ZoektFileMatch[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
|
@ -104,11 +105,12 @@ export default function Home() {
|
|||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then((body: GetSourceResponse) => {
|
||||
setSelectedMatchIndex(0);
|
||||
setPreviewFile({
|
||||
content: body.content,
|
||||
filepath: match.FileName,
|
||||
matches: match.Matches,
|
||||
})
|
||||
});
|
||||
setIsCodePanelOpen(true);
|
||||
});
|
||||
}}
|
||||
|
|
@ -122,6 +124,8 @@ export default function Home() {
|
|||
<CodePreview
|
||||
file={previewFile}
|
||||
onClose={() => setIsCodePanelOpen(false)}
|
||||
selectedMatchIndex={selectedMatchIndex}
|
||||
onSelectedMatchIndexChange={setSelectedMatchIndex}
|
||||
keymapType={keymapType}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,27 @@
|
|||
import { Extension, StateEffect, StateField, Transaction } from "@codemirror/state";
|
||||
import { EditorSelection, Extension, StateEffect, StateField, Text, Transaction } from "@codemirror/state";
|
||||
import { Decoration, DecorationSet, EditorView } from "@codemirror/view";
|
||||
import { ZoektMatch } from "../types";
|
||||
|
||||
const matchMark = Decoration.mark({
|
||||
class: "cm-searchMatch"
|
||||
});
|
||||
const selectedMatchMark = Decoration.mark({
|
||||
class: "cm-searchMatch cm-searchMatch-selected"
|
||||
});
|
||||
|
||||
const setMatchState = StateEffect.define<{
|
||||
selectedMatchIndex: number,
|
||||
matches: ZoektMatch[],
|
||||
}>();
|
||||
|
||||
const getMatchRange = (match: ZoektMatch, document: Text) => {
|
||||
const line = document.line(match.LineNum);
|
||||
const fragment = match.Fragments[0];
|
||||
const from = line.from + fragment.Pre.length;
|
||||
const to = from + fragment.Match.length;
|
||||
return { from, to };
|
||||
}
|
||||
|
||||
const setMatches = StateEffect.define<ZoektMatch[]>();
|
||||
|
||||
const matchHighlighter = StateField.define<DecorationSet>({
|
||||
create () {
|
||||
|
|
@ -16,13 +31,13 @@ const matchHighlighter = StateField.define<DecorationSet>({
|
|||
highlights = highlights.map(transaction.changes);
|
||||
|
||||
for (const effect of transaction.effects) {
|
||||
if (effect.is(setMatches)) {
|
||||
const decorations = effect.value.map(match => {
|
||||
const line = transaction.newDoc.line(match.LineNum);
|
||||
const fragment = match.Fragments[0];
|
||||
const from = line.from + fragment.Pre.length;
|
||||
const to = from + fragment.Match.length;
|
||||
return matchMark.range(from, to);
|
||||
if (effect.is(setMatchState)) {
|
||||
const { matches, selectedMatchIndex } = effect.value;
|
||||
|
||||
const decorations = matches.map((match, index) => {
|
||||
const { from, to } = getMatchRange(match, transaction.newDoc);
|
||||
const mark = index === selectedMatchIndex ? selectedMatchMark : matchMark;
|
||||
return mark.range(from, to);
|
||||
});
|
||||
|
||||
highlights = Decoration.set(decorations)
|
||||
|
|
@ -37,11 +52,29 @@ const matchHighlighter = StateField.define<DecorationSet>({
|
|||
const highlightTheme = EditorView.baseTheme({
|
||||
"&light .cm-searchMatch": { backgroundColor: "#ffff0054" },
|
||||
"&dark .cm-searchMatch": { backgroundColor: "#00ffff8a" },
|
||||
"&light .cm-searchMatch-selected": { backgroundColor: "#ff6a0054" },
|
||||
"&dark .cm-searchMatch-selected": { backgroundColor: "#ff00ff8a" }
|
||||
});
|
||||
|
||||
export const markMatches = (matches: ZoektMatch[], view: EditorView) => {
|
||||
const effect: StateEffect<ZoektMatch[]> = setMatches.of(matches);
|
||||
view.dispatch({ effects: [effect] });
|
||||
export const markMatches = (selectedMatchIndex: number, matches: ZoektMatch[], view: EditorView) => {
|
||||
const setState = setMatchState.of({
|
||||
selectedMatchIndex,
|
||||
matches,
|
||||
});
|
||||
|
||||
const effects = []
|
||||
effects.push(setState);
|
||||
|
||||
if (selectedMatchIndex >= 0 && selectedMatchIndex < matches.length) {
|
||||
const match = matches[selectedMatchIndex];
|
||||
const { from, to } = getMatchRange(match, view.state.doc);
|
||||
const selection = EditorSelection.range(from, to);
|
||||
effects.push(EditorView.scrollIntoView(selection, {
|
||||
y: "start",
|
||||
}));
|
||||
};
|
||||
|
||||
view.dispatch({ effects });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue