add DiffEditor

This commit is contained in:
Sidney Alcantara
2021-10-29 18:09:37 +11:00
parent ed3bce3a43
commit bdd7d2aaa2
4 changed files with 339 additions and 207 deletions

View File

@@ -0,0 +1,93 @@
import {
DiffEditor as MonacoDiffEditor,
DiffEditorProps,
EditorProps,
} from "@monaco-editor/react";
import { useTheme, Box, BoxProps } from "@mui/material";
import CircularProgressOptical from "@src/components/CircularProgressOptical";
import ResizeBottomRightIcon from "@src/assets/icons/ResizeBottomRight";
import useMonacoCustomizations, {
IUseMonacoCustomizationsProps,
} from "./useMonacoCustomizations";
export interface IDiffEditorProps
extends Partial<DiffEditorProps>,
IUseMonacoCustomizationsProps {
onChange?: EditorProps["onChange"];
containerProps?: Partial<BoxProps>;
}
export default function DiffEditor({
onChange,
minHeight = 100,
disabled,
error,
containerProps,
extraLibs,
diagnosticsOptions,
onUnmount,
...props
}: IDiffEditorProps) {
const theme = useTheme();
const { boxSx } = useMonacoCustomizations({
minHeight,
disabled,
error,
extraLibs,
diagnosticsOptions,
onUnmount,
});
// Needs manual patch since `onMount` prop is not available in `DiffEditor`
// https://github.com/suren-atoyan/monaco-react/issues/281
const handleEditorMount: DiffEditorProps["onMount"] = (editor, monaco) => {
const modifiedEditor = editor.getModifiedEditor();
modifiedEditor.onDidChangeModelContent((ev) => {
onChange?.(modifiedEditor.getValue(), ev);
});
props.onMount?.(editor, monaco);
};
return (
<Box sx={{ ...boxSx, ...containerProps?.sx }}>
<MonacoDiffEditor
language="javascript"
loading={<CircularProgressOptical size={20} sx={{ m: 2 }} />}
className="editor"
{...props}
onMount={handleEditorMount}
options={
{
readOnly: disabled,
fontFamily: theme.typography.fontFamilyMono,
rulers: [80],
minimap: { enabled: false },
lineNumbersMinChars: 4,
lineDecorationsWidth: "18",
automaticLayout: true,
fixedOverflowWidgets: true,
tabSize: 2,
...props.options,
} as any
}
/>
<ResizeBottomRightIcon
aria-label="Resize code editor"
color="action"
sx={{
position: "absolute",
bottom: 1,
right: 1,
zIndex: 1,
}}
/>
</Box>
);
}

View File

@@ -126,8 +126,8 @@
"editorGutter.modifiedBackground": "#d4a72c66",
"editorGutter.addedBackground": "#4ac26b66",
"editorGutter.deletedBackground": "#ff818266",
"diffEditor.insertedTextBackground": "#dafbe1",
"diffEditor.removedTextBackground": "#ffebe9",
"diffEditor.insertedTextBackground": "#85e89d33",
"diffEditor.removedTextBackground": "#f9758326",
"scrollbar.shadow": "#6a737d33",
"scrollbarSlider.background": "#959da533",
"scrollbarSlider.hoverBackground": "#959da544",

View File

@@ -1,39 +1,26 @@
import React, { useState, useEffect } from "react";
import Editor, { EditorProps, useMonaco } from "@monaco-editor/react";
import type { editor, languages } from "monaco-editor/esm/vs/editor/editor.api";
import githubLightTheme from "./github-light-default.json";
import githubDarkTheme from "./github-dark-default.json";
import { useState } from "react";
import Editor, { EditorProps } from "@monaco-editor/react";
import type { editor } from "monaco-editor/esm/vs/editor/editor.api";
import { useTheme, Box, BoxProps } from "@mui/material";
import CircularProgressOptical from "@src/components/CircularProgressOptical";
import ResizeBottomRightIcon from "@src/assets/icons/ResizeBottomRight";
import { useProjectContext } from "@src/contexts/ProjectContext";
import { getFieldProp } from "@src/components/fields";
import useMonacoCustomizations, {
IUseMonacoCustomizationsProps,
} from "./useMonacoCustomizations";
/* eslint-disable import/no-webpack-loader-syntax */
import firestoreDefs from "!!raw-loader!./firestore.d.ts";
import firebaseAuthDefs from "!!raw-loader!./firebaseAuth.d.ts";
import firebaseStorageDefs from "!!raw-loader!./firebaseStorage.d.ts";
import utilsDefs from "!!raw-loader!./utils.d.ts";
import extensionsDefs from "!!raw-loader!./extensions.d.ts";
export interface ICodeEditorProps extends Partial<EditorProps> {
export interface ICodeEditorProps
extends Partial<EditorProps>,
IUseMonacoCustomizationsProps {
value: string;
minHeight?: number;
disabled?: boolean;
error?: boolean;
containerProps?: Partial<BoxProps>;
extraLibs?: string[];
onValidate?: EditorProps["onValidate"];
onValidStatusUpdate?: (result: {
isValid: boolean;
markers: editor.IMarker[];
}) => void;
diagnosticsOptions?: languages.typescript.DiagnosticsOptions;
onUnmount?: () => void;
}
export default function CodeEditor({
@@ -43,210 +30,44 @@ export default function CodeEditor({
error,
containerProps,
extraLibs,
onValidate,
onValidStatusUpdate,
extraLibs,
diagnosticsOptions,
onUnmount,
...props
}: ICodeEditorProps) {
const theme = useTheme();
const { tableState } = useProjectContext();
// Store editor value to prevent code editor values not being saved when
// Side Drawer is in the middle of a refresh
const [initialEditorValue] = useState(value ?? "");
const monaco = useMonaco();
useEffect(() => {
return () => {
onUnmount?.();
};
}, []);
const { boxSx } = useMonacoCustomizations({
minHeight,
disabled,
error,
extraLibs,
diagnosticsOptions,
onUnmount,
});
const onValidate_: EditorProps["onValidate"] = (markers) => {
if (onValidStatusUpdate)
onValidStatusUpdate({ isValid: markers.length <= 0, markers });
else if (onValidate) onValidate(markers);
onValidStatusUpdate?.({ isValid: markers.length <= 0, markers });
onValidate?.(markers);
};
useEffect(() => {
if (!monaco) {
// useMonaco returns a monaco instance but initialisation is done asynchronously
// dont execute the logic until the instance is initialised
return;
}
setTimeout(() => {
try {
monaco.editor.defineTheme("github-light", githubLightTheme as any);
monaco.editor.defineTheme("github-dark", githubDarkTheme as any);
monaco.editor.setTheme("github-" + theme.palette.mode);
} catch (error) {
console.error("Could not set Monaco theme: ", error);
}
});
}, [monaco, theme.palette.mode]);
useEffect(() => {
if (!monaco) {
// useMonaco returns a monaco instance but initialisation is done asynchronously
// dont execute the logic until the instance is initialised
return;
}
try {
monaco.editor.defineTheme("github-light", githubLightTheme as any);
monaco.editor.defineTheme("github-dark", githubDarkTheme as any);
monaco.languages.typescript.javascriptDefaults.addExtraLib(firestoreDefs);
monaco.languages.typescript.javascriptDefaults.addExtraLib(
firebaseAuthDefs
);
monaco.languages.typescript.javascriptDefaults.addExtraLib(
firebaseStorageDefs
);
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions(
diagnosticsOptions ?? {
noSemanticValidation: true,
noSyntaxValidation: false,
}
);
// compiler options
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES2020,
allowNonTsExtensions: true,
});
if (extraLibs) {
monaco.languages.typescript.javascriptDefaults.addExtraLib(
extraLibs.join("\n"),
"ts:filename/extraLibs.d.ts"
);
}
monaco.languages.typescript.javascriptDefaults.addExtraLib(
utilsDefs,
"ts:filename/utils.d.ts"
);
const rowDefinition =
Object.keys(tableState?.columns!)
.map((columnKey: string) => {
const column = tableState?.columns[columnKey];
return `static ${columnKey}: ${getFieldProp("type", column.type)}`;
})
.join(";\n") + ";";
const availableFields = Object.keys(tableState?.columns!)
.map((columnKey: string) => `"${columnKey}"`)
.join("|\n");
monaco.languages.typescript.javascriptDefaults.addExtraLib(
[
"/**",
" * extensions type configuration",
" */",
"// basic types that are used in all places",
`type Row = {${rowDefinition}};`,
`type Field = ${availableFields} | string | object;`,
`type Fields = Field[];`,
extensionsDefs,
].join("\n"),
"ts:filename/extensions.d.ts"
);
monaco.languages.typescript.javascriptDefaults.addExtraLib(
[
"declare var require: any;",
"declare var Buffer: any;",
"const ref: FirebaseFirestore.DocumentReference;",
"const storage: firebasestorage.Storage;",
"const db: FirebaseFirestore.Firestore;",
"const auth: adminauth.BaseAuth;",
"declare class row {",
" /**",
" * Returns the row fields",
" */",
rowDefinition,
"}",
].join("\n"),
"ts:filename/rowFields.d.ts"
);
} catch (error) {
console.error(
"An error occurred during initialization of Monaco: ",
error
);
}
}, [tableState?.columns, monaco, diagnosticsOptions, extraLibs]);
return (
<Box
sx={{
minWidth: 400,
minHeight,
height: minHeight,
borderRadius: 1,
resize: "vertical",
overflow: "hidden",
position: "relative",
backgroundColor: disabled ? "transparent" : theme.palette.action.input,
"&::after": {
content: '""',
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0,
pointerEvents: "none",
borderRadius: "inherit",
boxShadow: `0 -1px 0 0 ${theme.palette.text.disabled} inset,
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
transition: theme.transitions.create("box-shadow", {
duration: theme.transitions.duration.short,
}),
},
"&:hover::after": {
boxShadow: `0 -1px 0 0 ${theme.palette.text.primary} inset,
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
},
"&:focus-within::after": {
boxShadow: `0 -2px 0 0 ${theme.palette.primary.main} inset,
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
},
...(error
? {
"&::after, &:hover::after, &:focus-within::after": {
boxShadow: `0 -2px 0 0 ${theme.palette.error.main} inset,
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
},
}
: {}),
"& .editor": {
// Overwrite user-select: none that causes editor
// to not be focusable in Safari
userSelect: "auto",
height: "100%",
},
"& .monaco-editor, & .monaco-editor .margin, & .monaco-editor-background":
{
backgroundColor: "transparent",
},
...containerProps?.sx,
}}
>
<Box sx={{ ...boxSx, ...containerProps?.sx }}>
<Editor
defaultLanguage="javascript"
value={initialEditorValue}
onValidate={onValidate_}
loading={<CircularProgressOptical size={20} sx={{ m: 2 }} />}
className="editor"
{...props}
onValidate={onValidate_}
options={{
readOnly: disabled,
fontFamily: theme.typography.fontFamilyMono,
@@ -262,7 +83,7 @@ export default function CodeEditor({
/>
<ResizeBottomRightIcon
aria-label="This code editor is resizable"
aria-label="Resize code editor"
color="action"
sx={{
position: "absolute",

View File

@@ -0,0 +1,218 @@
import { useEffect } from "react";
import { useMonaco } from "@monaco-editor/react";
import type { languages } from "monaco-editor/esm/vs/editor/editor.api";
import githubLightTheme from "./github-light-default.json";
import githubDarkTheme from "./github-dark-default.json";
import { useTheme } from "@mui/material";
import type { SxProps, Theme } from "@mui/system";
import { useProjectContext } from "@src/contexts/ProjectContext";
import { getFieldProp } from "@src/components/fields";
/* eslint-disable import/no-webpack-loader-syntax */
import firestoreDefs from "!!raw-loader!./firestore.d.ts";
import firebaseAuthDefs from "!!raw-loader!./firebaseAuth.d.ts";
import firebaseStorageDefs from "!!raw-loader!./firebaseStorage.d.ts";
import utilsDefs from "!!raw-loader!./utils.d.ts";
import extensionsDefs from "!!raw-loader!./extensions.d.ts";
export interface IUseMonacoCustomizationsProps {
minHeight?: number;
disabled?: boolean;
error?: boolean;
extraLibs?: string[];
diagnosticsOptions?: languages.typescript.DiagnosticsOptions;
onUnmount?: () => void;
}
export default function useMonacoCustomizations({
minHeight,
disabled,
error,
extraLibs,
diagnosticsOptions,
onUnmount,
}: IUseMonacoCustomizationsProps) {
const theme = useTheme();
const { tableState } = useProjectContext();
const monaco = useMonaco();
useEffect(() => {
return () => {
onUnmount?.();
};
}, []);
useEffect(() => {
if (!monaco) {
// useMonaco returns a monaco instance but initialisation is done asynchronously
// dont execute the logic until the instance is initialised
return;
}
setTimeout(() => {
try {
monaco.editor.defineTheme("github-light", githubLightTheme as any);
monaco.editor.defineTheme("github-dark", githubDarkTheme as any);
monaco.editor.setTheme("github-" + theme.palette.mode);
} catch (error) {
console.error("Could not set Monaco theme: ", error);
}
});
}, [monaco, theme.palette.mode]);
useEffect(() => {
if (!monaco) {
// useMonaco returns a monaco instance but initialisation is done asynchronously
// dont execute the logic until the instance is initialised
return;
}
try {
monaco.editor.defineTheme("github-light", githubLightTheme as any);
monaco.editor.defineTheme("github-dark", githubDarkTheme as any);
monaco.languages.typescript.javascriptDefaults.addExtraLib(firestoreDefs);
monaco.languages.typescript.javascriptDefaults.addExtraLib(
firebaseAuthDefs
);
monaco.languages.typescript.javascriptDefaults.addExtraLib(
firebaseStorageDefs
);
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions(
diagnosticsOptions ?? {
noSemanticValidation: true,
noSyntaxValidation: false,
}
);
// compiler options
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES2020,
allowNonTsExtensions: true,
});
if (extraLibs) {
monaco.languages.typescript.javascriptDefaults.addExtraLib(
extraLibs.join("\n"),
"ts:filename/extraLibs.d.ts"
);
}
monaco.languages.typescript.javascriptDefaults.addExtraLib(
utilsDefs,
"ts:filename/utils.d.ts"
);
const rowDefinition =
Object.keys(tableState?.columns!)
.map((columnKey: string) => {
const column = tableState?.columns[columnKey];
return `static ${columnKey}: ${getFieldProp("type", column.type)}`;
})
.join(";\n") + ";";
const availableFields = Object.keys(tableState?.columns!)
.map((columnKey: string) => `"${columnKey}"`)
.join("|\n");
monaco.languages.typescript.javascriptDefaults.addExtraLib(
[
"/**",
" * extensions type configuration",
" */",
"// basic types that are used in all places",
`type Row = {${rowDefinition}};`,
`type Field = ${availableFields} | string | object;`,
`type Fields = Field[];`,
extensionsDefs,
].join("\n"),
"ts:filename/extensions.d.ts"
);
monaco.languages.typescript.javascriptDefaults.addExtraLib(
[
"declare var require: any;",
"declare var Buffer: any;",
"const ref: FirebaseFirestore.DocumentReference;",
"const storage: firebasestorage.Storage;",
"const db: FirebaseFirestore.Firestore;",
"const auth: adminauth.BaseAuth;",
"declare class row {",
" /**",
" * Returns the row fields",
" */",
rowDefinition,
"}",
].join("\n"),
"ts:filename/rowFields.d.ts"
);
} catch (error) {
console.error(
"An error occurred during initialization of Monaco: ",
error
);
}
}, [tableState?.columns, monaco, diagnosticsOptions, extraLibs]);
const boxSx: SxProps<Theme> = {
minWidth: 400,
minHeight,
height: minHeight,
borderRadius: 1,
resize: "vertical",
overflow: "hidden",
position: "relative",
backgroundColor: disabled ? "transparent" : theme.palette.action.input,
"&::after": {
content: '""',
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0,
pointerEvents: "none",
borderRadius: "inherit",
boxShadow: `0 -1px 0 0 ${theme.palette.text.disabled} inset,
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
transition: theme.transitions.create("box-shadow", {
duration: theme.transitions.duration.short,
}),
},
"&:hover::after": {
boxShadow: `0 -1px 0 0 ${theme.palette.text.primary} inset,
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
},
"&:focus-within::after": {
boxShadow: `0 -2px 0 0 ${theme.palette.primary.main} inset,
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
},
...(error
? {
"&::after, &:hover::after, &:focus-within::after": {
boxShadow: `0 -2px 0 0 ${theme.palette.error.main} inset,
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
},
}
: {}),
"& .editor": {
// Overwrite user-select: none that causes editor
// to not be focusable in Safari
userSelect: "auto",
height: "100%",
},
"& .monaco-editor, & .monaco-editor .margin, & .monaco-editor-background": {
backgroundColor: "transparent",
},
};
return { boxSx };
}