Files
jsonhero-web/app/components/JsonPreview.tsx
T
2022-03-01 09:31:09 +00:00

158 lines
3.8 KiB
TypeScript

import { RangeSetBuilder } from "@codemirror/rangeset";
import { JSONHeroPath } from "@jsonhero/path";
import {
useCodeMirror,
EditorView,
Decoration,
Facet,
ViewPlugin,
Compartment,
TransactionSpec,
} from "@uiw/react-codemirror";
import jsonMap from "json-source-map";
import { useRef, useEffect, useMemo } from "react";
import { getPreviewSetup } from "~/utilities/codeMirrorSetup";
import { lightTheme, darkTheme } from "~/utilities/codeMirrorTheme";
import { useTheme } from "./ThemeProvider";
export type JsonPreviewProps = {
json: unknown;
highlightPath?: string;
};
export function JsonPreview({ json, highlightPath }: JsonPreviewProps) {
const editor = useRef(null);
const jsonMapped = useMemo(() => {
return jsonMap.stringify(json, null, 2);
}, [json]);
const lines: LineRange | undefined = useMemo(() => {
if (!highlightPath) {
return;
}
let path = new JSONHeroPath(highlightPath);
let pointer = path.jsonPointer();
let selectionInfo = jsonMapped.pointers[pointer];
return {
from: selectionInfo.value.line + 1,
to: selectionInfo.valueEnd.line + 1,
};
}, [jsonMapped, highlightPath]);
const extensions = getPreviewSetup();
const highlighting = new Compartment();
if (lines) {
extensions.push(highlighting.of(highlightLineRange(lines)));
}
const [theme] = useTheme();
const { setContainer, view } = useCodeMirror({
container: editor.current,
extensions,
value: jsonMapped.json,
editable: false,
contentEditable: false,
autoFocus: false,
basicSetup: false,
theme: theme === "light" ? lightTheme() : darkTheme(),
});
useEffect(() => {
if (editor.current) {
setContainer(editor.current);
}
}, [editor.current]);
useEffect(() => {
if (!view) {
return;
}
let transactionSpec: TransactionSpec = {
changes: { from: 0, to: view.state.doc.length, insert: jsonMapped.json },
};
let range = lines;
if (range != null) {
transactionSpec.effects = highlighting.reconfigure(
highlightLineRange(range)
);
}
view.dispatch(transactionSpec);
}, [view, highlighting, jsonMapped, highlightPath]);
return (
<div>
<div ref={editor} />
</div>
);
}
interface LineRange {
from: number;
to: number;
}
const baseTheme = EditorView.baseTheme({
"&light .cm-highlighted": { backgroundColor: "#ffee0055" },
"&dark .cm-highlighted": { backgroundColor: "#ffee0055" },
});
const highlightedRange = Facet.define<LineRange, LineRange>({
combine: (values) => (values.length ? values[0] : { from: -1, to: -1 }),
});
function highlightLineRange(range: LineRange | null) {
return [
baseTheme,
range == null ? [] : highlightedRange.of(range),
highlightLineRangePlugin,
];
}
const lineHighlightDecoration = Decoration.line({
attributes: { class: "cm-highlighted" },
});
function highlightLines(view: EditorView) {
let highlightRange = view.state.facet(highlightedRange);
let builder = new RangeSetBuilder();
for (let { from, to } of view.visibleRanges) {
for (let pos = from; pos <= to; ) {
let line = view.state.doc.lineAt(pos);
if (
line.number >= highlightRange.from &&
line.number <= highlightRange.to
) {
builder.add(line.from, line.from, lineHighlightDecoration);
}
pos = line.to + 1;
}
}
return builder.finish();
}
const highlightLineRangePlugin = ViewPlugin.fromClass(
class {
decorations: any;
constructor(view: any) {
this.decorations = highlightLines(view);
}
update(update: { docChanged: any; viewportChanged: any; view: any }) {
if (update.docChanged || update.viewportChanged)
this.decorations = highlightLines(update.view);
}
},
{
decorations: (v) => v.decorations,
}
);