Files
rowy/src/components/CodeEditor/useMonacoCustomizations.ts
Bobby Wang 8445081365 Merge branch 'develop' into feature/code-editor-package-typing
# Conflicts:
#	src/components/CodeEditor/useMonacoCustomizations.ts
#	yarn.lock
2023-08-29 20:19:27 +08:00

407 lines
12 KiB
TypeScript

import { useEffect } from "react";
import { useAtom } from "jotai";
import { matchSorter } from "match-sorter";
import {
tableScope,
tableRowsAtom,
tableColumnsOrderedAtom,
} from "@src/atoms/tableScope";
import { useMonaco } from "@monaco-editor/react";
import type { languages } from "monaco-editor/esm/vs/editor/editor.api";
import { useTheme } from "@mui/material";
import type { SystemStyleObject, Theme } from "@mui/system";
import firestoreDefs from "./firestore.d.ts?raw";
import firebaseAuthDefs from "./firebaseAuth.d.ts?raw";
import firebaseStorageDefs from "./firebaseStorage.d.ts?raw";
import utilsDefs from "./utils.d.ts?raw";
import rowyUtilsDefs from "./rowy.d.ts?raw";
import extensionsDefs from "./extensions.d.ts?raw";
import { projectScope, secretNamesAtom } from "@src/atoms/projectScope";
import { getFieldProp } from "@src/components/fields";
export interface IUseMonacoCustomizationsProps {
minHeight?: number;
disabled?: boolean;
error?: boolean;
extraLibs?: string[];
diagnosticsOptions?: languages.typescript.DiagnosticsOptions;
onUnmount?: () => void;
// Internal only
fullScreen?: boolean;
}
export default function useMonacoCustomizations({
minHeight,
disabled,
error,
extraLibs,
diagnosticsOptions = {
noSemanticValidation: true,
noSyntaxValidation: false,
},
onUnmount,
fullScreen,
}: IUseMonacoCustomizationsProps) {
const theme = useTheme();
const monaco = useMonaco();
const [tableRows] = useAtom(tableRowsAtom, tableScope);
const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope);
const [secretNames] = useAtom(secretNamesAtom, projectScope);
useEffect(() => {
return () => {
onUnmount?.();
};
}, []);
useEffect(() => {
if (!monaco) return;
try {
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
moduleResolution:
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.CommonJS,
target: monaco.languages.typescript.ScriptTarget.ES2020,
allowNonTsExtensions: true,
typeRoots: ["node_modules/@types"],
});
monaco.languages.typescript.typescriptDefaults.addExtraLib(firestoreDefs);
monaco.languages.typescript.typescriptDefaults.addExtraLib(
firebaseAuthDefs
);
monaco.languages.typescript.typescriptDefaults.addExtraLib(
firebaseStorageDefs
);
monaco.languages.typescript.typescriptDefaults.addExtraLib(
utilsDefs,
"ts:filename/utils.d.ts"
);
monaco.languages.typescript.typescriptDefaults.addExtraLib(rowyUtilsDefs);
} catch (error) {
console.error(
"An error occurred during initialization of Monaco: ",
error
);
}
}, [monaco]);
// Initialize extraLibs from props
useEffect(() => {
if (!monaco) return;
if (!extraLibs) return;
try {
monaco.languages.typescript.typescriptDefaults.addExtraLib(
extraLibs.join("\n"),
"ts:filename/extraLibs.d.ts"
);
} catch (error) {
console.error("Could not add extraLibs from props: ", error);
}
}, [monaco, extraLibs]);
// Set diagnostics options
const stringifiedDiagnosticsOptions = JSON.stringify(diagnosticsOptions);
useEffect(() => {
if (!monaco) return;
try {
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
...JSON.parse(stringifiedDiagnosticsOptions),
diagnosticCodesToIgnore: [
1323, // remove dynamic import error
2307, // silence type declarations not found for dynamic import
],
});
} catch (error) {
console.error("Could not set diagnostics options: ", error);
}
}, [monaco, stringifiedDiagnosticsOptions]);
const setReplacementActions = () => {
if (!monaco) return;
const { dispose } = monaco.languages.registerCodeActionProvider(
"javascript",
{
provideCodeActions: (model, range, context, token) => {
const consoleLogReplacements = context.markers
.filter((error) => {
return error.message.includes("Rowy Cloud Logging");
})
.map((error) => {
// first sentence of the message is "Replace with logging.[log/warn/error]"
const firstSentence = error.message.split(":")[0];
const replacement = firstSentence.split("with ")[1];
return {
title: firstSentence,
diagnostics: [error],
kind: "quickfix",
edit: {
edits: [
{
resource: model.uri,
edit: {
range: error,
text: replacement,
},
},
],
},
isPreferred: true,
};
});
const secretNameReplacements = context.markers
.filter((error) => {
return error.message.includes(
"is not assignable to parameter of type 'SecretNames'"
);
})
.map((error) => {
const typoSecretName = model
.getLineContent(error.startLineNumber)
.slice(error.startColumn, error.endColumn - 2);
const similarSecretNames =
matchSorter(secretNames.secretNames ?? [], typoSecretName) ??
[];
const otherSecretNames =
secretNames.secretNames?.filter(
(secretName) => !similarSecretNames.includes(secretName)
) ?? [];
return [
...similarSecretNames.map((secretName) => ({
title: `Replace with "${secretName}"`,
diagnostics: [error],
kind: "quickfix",
edit: {
edits: [
{
resource: model.uri,
edit: {
range: error,
text: `"${secretName}"`,
},
},
],
},
isPreferred: true,
})),
...otherSecretNames.map((secretName) => ({
title: `Replace with "${secretName}"`,
diagnostics: [error],
kind: "quickfix",
edit: {
edits: [
{
resource: model.uri,
edit: {
range: error,
text: `"${secretName}"`,
},
},
],
},
isPreferred: false,
})),
];
})
.flat();
return {
actions: [...consoleLogReplacements, ...secretNameReplacements],
dispose: () => {},
};
},
}
);
monaco.editor.onWillDisposeModel((model) => {
// dispose code action provider when model is disposed
// this makes sure code actions are not displayed multiple times
dispose();
});
};
const addJsonFieldDefinition = async (
columnKey: string,
interfaceName: string
) => {
const samples = tableRows
.map((row) => row[columnKey])
.filter((entry) => entry !== undefined)
.map((entry) => JSON.stringify(entry));
monaco?.languages.typescript.typescriptDefaults.addExtraLib(
`type ${interfaceName} = any;`
);
// if (!samples || samples.length === 0) {
// monaco?.languages.typescript.typescriptDefaults.addExtraLib(
// `type ${interfaceName} = any;`
// );
// return;
// } else {
// const jsonInput = jsonInputForTargetLanguage("typescript");
// await jsonInput.addSource({
// name: interfaceName,
// samples,
// });
// const inputData = new InputData();
// inputData.addInput(jsonInput);
// const result = await quicktype({
// inputData,
// lang: "typescript",
// rendererOptions: { "just-types": "true" },
// });
// const newLib = result.lines.join("\n").replaceAll("export ", "");
// monaco?.languages.typescript.typescriptDefaults.addExtraLib(newLib);
//}
};
//TODO: types
const setBaseDefinitions = () => {
const rowDefinition =
tableColumnsOrdered
.map((column) => {
const { type, key } = column;
if (type === "JSON") {
const interfaceName = key[0].toUpperCase() + key.slice(1);
addJsonFieldDefinition(key, interfaceName);
const def = `static "${key}": ${interfaceName}`;
return def;
}
return `static "${key}": ${getFieldProp("dataType", type)}`;
})
.join(";\n") + ";";
const availableFields = tableColumnsOrdered
.map((key) => `"${key}"`)
.join("|\n");
monaco?.languages.typescript.typescriptDefaults.addExtraLib(
["/**", " * extensions type configuration", " */", extensionsDefs].join(
"\n"
),
"ts:filename/extensions.d.ts"
);
monaco?.languages.typescript.typescriptDefaults.addExtraLib(
[
"// basic types that are used in all places",
"declare var require: any;",
"declare var Buffer: any;",
"const ref: FirebaseFirestore.DocumentReference;",
"const storage: firebasestorage.Storage;",
"const db: FirebaseFirestore.Firestore;",
"const auth: firebaseauth.BaseAuth;",
`type Row = {${rowDefinition}};`,
`type Field = ${availableFields} | string | object;`,
`type Fields = Field[];`,
].join("\n"),
"ts:filename/rowFields.d.ts"
);
};
//TODO: Set row definitions
useEffect(() => {
if (!monaco) return;
try {
setBaseDefinitions();
} catch (error) {
console.error("Could not set basic", error);
}
}, [monaco, tableColumnsOrdered]);
useEffect(() => {
if (!monaco) return;
if (secretNames.loading) return;
if (!secretNames.secretNames) return;
const secretsDef = `type SecretNames = ${secretNames.secretNames
.map((secret: string) => `"${secret}"`)
.join(" | ")} \n
enum secrets {
${secretNames.secretNames
.map((secret: string) => `"${secret}" = "${secret}"`)
.join("\n")}
}
`;
monaco?.languages.typescript.javascriptDefaults.addExtraLib(secretsDef);
setReplacementActions();
}, [monaco, secretNames]);
let boxSx: SystemStyleObject<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",
},
};
if (fullScreen)
boxSx = {
...boxSx,
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: theme.zIndex.tooltip * 2,
m: "0 !important",
resize: "none",
backgroundColor: theme.palette.background.paper,
borderRadius: 0,
"&::after": { display: "none" },
};
return { boxSx };
}