mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-14 21:35:25 +00:00
150 lines
5.2 KiB
TypeScript
150 lines
5.2 KiB
TypeScript
'use client';
|
|
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { useKeymapExtension } from "@/hooks/useKeymapExtension";
|
|
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
|
|
import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension";
|
|
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
|
|
import { search } from "@codemirror/search";
|
|
import CodeMirror, { Decoration, DecorationSet, EditorSelection, EditorView, ReactCodeMirrorRef, SelectionRange, StateField, ViewUpdate } from "@uiw/react-codemirror";
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
import { EditorContextMenu } from "../../components/editorContextMenu";
|
|
|
|
interface CodePreviewProps {
|
|
path: string;
|
|
repoName: string;
|
|
revisionName: string;
|
|
source: string;
|
|
language: string;
|
|
}
|
|
|
|
export const CodePreview = ({
|
|
source,
|
|
language,
|
|
path,
|
|
repoName,
|
|
revisionName,
|
|
}: CodePreviewProps) => {
|
|
const editorRef = useRef<ReactCodeMirrorRef>(null);
|
|
const syntaxHighlighting = useSyntaxHighlightingExtension(language, editorRef.current?.view);
|
|
const [currentSelection, setCurrentSelection] = useState<SelectionRange>();
|
|
const keymapExtension = useKeymapExtension(editorRef.current?.view);
|
|
const [isEditorCreated, setIsEditorCreated] = useState(false);
|
|
|
|
const highlightRangeQuery = useNonEmptyQueryParam('highlightRange');
|
|
const highlightRange = useMemo(() => {
|
|
if (!highlightRangeQuery) {
|
|
return;
|
|
}
|
|
|
|
const rangeRegex = /^\d+:\d+,\d+:\d+$/;
|
|
if (!rangeRegex.test(highlightRangeQuery)) {
|
|
return;
|
|
}
|
|
|
|
const [start, end] = highlightRangeQuery.split(',').map((range) => {
|
|
return range.split(':').map((val) => parseInt(val, 10));
|
|
});
|
|
|
|
return {
|
|
start: {
|
|
line: start[0],
|
|
character: start[1],
|
|
},
|
|
end: {
|
|
line: end[0],
|
|
character: end[1],
|
|
}
|
|
}
|
|
}, [highlightRangeQuery]);
|
|
|
|
const extensions = useMemo(() => {
|
|
const highlightDecoration = Decoration.mark({
|
|
class: "cm-searchMatch-selected",
|
|
});
|
|
|
|
return [
|
|
syntaxHighlighting,
|
|
EditorView.lineWrapping,
|
|
keymapExtension,
|
|
search({
|
|
top: true,
|
|
}),
|
|
EditorView.updateListener.of((update: ViewUpdate) => {
|
|
if (update.selectionSet) {
|
|
setCurrentSelection(update.state.selection.main);
|
|
}
|
|
}),
|
|
StateField.define<DecorationSet>({
|
|
create(state) {
|
|
if (!highlightRange) {
|
|
return Decoration.none;
|
|
}
|
|
|
|
const { start, end } = highlightRange;
|
|
const from = state.doc.line(start.line).from + start.character - 1;
|
|
const to = state.doc.line(end.line).from + end.character - 1;
|
|
|
|
return Decoration.set([
|
|
highlightDecoration.range(from, to),
|
|
]);
|
|
},
|
|
update(deco, tr) {
|
|
return deco.map(tr.changes);
|
|
},
|
|
provide: (field) => EditorView.decorations.from(field),
|
|
}),
|
|
];
|
|
}, [keymapExtension, syntaxHighlighting, highlightRange]);
|
|
|
|
useEffect(() => {
|
|
if (!highlightRange || !editorRef.current || !editorRef.current.state) {
|
|
return;
|
|
}
|
|
|
|
const doc = editorRef.current.state.doc;
|
|
const { start, end } = highlightRange;
|
|
const from = doc.line(start.line).from + start.character - 1;
|
|
const to = doc.line(end.line).from + end.character - 1;
|
|
const selection = EditorSelection.range(from, to);
|
|
|
|
editorRef.current.view?.dispatch({
|
|
effects: [
|
|
EditorView.scrollIntoView(selection, { y: "center" }),
|
|
]
|
|
});
|
|
// @note: we need to include `isEditorCreated` in the dependency array since
|
|
// a race-condition can happen if the `highlightRange` is resolved before the
|
|
// editor is created.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [highlightRange, isEditorCreated]);
|
|
|
|
const { theme } = useThemeNormalized();
|
|
|
|
return (
|
|
<ScrollArea className="h-full overflow-auto flex-1">
|
|
<CodeMirror
|
|
className="relative"
|
|
ref={editorRef}
|
|
onCreateEditor={() => {
|
|
setIsEditorCreated(true);
|
|
}}
|
|
value={source}
|
|
extensions={extensions}
|
|
readOnly={true}
|
|
theme={theme === "dark" ? "dark" : "light"}
|
|
>
|
|
{editorRef.current && editorRef.current.view && currentSelection && (
|
|
<EditorContextMenu
|
|
view={editorRef.current.view}
|
|
selection={currentSelection}
|
|
repoName={repoName}
|
|
path={path}
|
|
revisionName={revisionName}
|
|
/>
|
|
)}
|
|
</CodeMirror>
|
|
</ScrollArea>
|
|
)
|
|
}
|
|
|