diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.stories.tsx b/packages/code-editor/src/CodeEditor/CodeEditor.stories.tsx index 37488ded0b..5455a15ef4 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.stories.tsx +++ b/packages/code-editor/src/CodeEditor/CodeEditor.stories.tsx @@ -49,7 +49,7 @@ export const XMLEditor: StoryObj = { docs: { description: { story: - "XML is one of the languages supported, and it can be enabled by setting the `language` property to `xml`. A XML schema can also be provided through the `xsdSchema` property. By providing a XML schema, the XML written will be validated against the schema showing errors. Providing a schema will also enable the code editor to show suggestions when opening a tag (`<`), writing an attribute, and when clicking on the CTRL and SPACE keys at the same time.", + "XML is one of the languages supported, and it can be enabled by setting the `language` property to `xml`. A XML schema can also be provided through the `xsdSchema` property. By providing a XML schema, the XML written will be validated against the schema showing errors. Providing a schema will also enable the code editor to show suggestions when opening a tag (`<`), writing an attribute, and when clicking on the CTRL and SPACE keys at the same time. By default, the XML code editor is formatted automatically. The property `disableAutoFormat` can be set to `true` to disable this behavior. You can also format manually the code using the `hvXmlFormatter` util.", }, source: { code: XmlStoryRaw }, }, diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.tsx b/packages/code-editor/src/CodeEditor/CodeEditor.tsx index 59c87f4309..9e5fd9186e 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.tsx +++ b/packages/code-editor/src/CodeEditor/CodeEditor.tsx @@ -3,7 +3,7 @@ import { Editor, useMonaco, type EditorProps } from "@monaco-editor/react"; import { useTheme, type ExtractNames } from "@hitachivantara/uikit-react-utils"; import { staticClasses, useClasses } from "./CodeEditor.styles"; -import { languagePlugins } from "./plugins"; +import { Formatter, languagePlugins } from "./plugins"; export { staticClasses as codeEditorClasses }; @@ -20,6 +20,12 @@ export interface HvCodeEditorProps extends EditorProps { options?: EditorProps["options"]; /** XSD schema used to validate the code editor content when the language is set to `xml`. */ xsdSchema?: string; + /** + * Whether to disable code auto format or not. Code format happens on mount, on blur, and on paste. + * Supported languages: `XML`. + * Defaults to `false`. + */ + disableAutoFormat?: boolean; } const defaultCodeEditorOptions: EditorProps["options"] = { @@ -51,6 +57,7 @@ export const HvCodeEditor = ({ defaultLanguage, language: languageProp, xsdSchema, + disableAutoFormat = false, onMount: onMountProp, beforeMount: beforeMountProp, ...others @@ -97,24 +104,19 @@ export const HvCodeEditor = ({ handleActiveThemes(); }, [handleActiveThemes]); - const handleFormatCode = () => { - // Get language plugin - const languagePlugin = language ? languagePlugins[language] : undefined; - - if (!languagePlugin) return; - - const { formatter } = languagePlugin; - - // Format code - if (editorRef.current && formatter) { - try { - const unformattedCode = editorRef.current.getValue(); - const formattedCode = formatter(unformattedCode); - editorRef.current.setValue(formattedCode); - } catch (error) { - // eslint-disable-next-line no-console - if (import.meta.env.DEV) console.error(error); - } + const handleFormatCode = async ( + editor: any, + monaco: any, + formatter: Formatter, + ) => { + try { + const model = editor.getModel(); + const content = model.getValue(); + const formattedCode = await formatter(content, editor, monaco); + if (formattedCode) editorRef.current.setValue(formattedCode); + } catch (error) { + // eslint-disable-next-line no-console + if (import.meta.env.DEV) console.error(error); } }; @@ -140,6 +142,7 @@ export const HvCodeEditor = ({ editorOptions, keyDownListener, validationMarker, + formatter, } = languagePlugin; // Update options @@ -174,9 +177,19 @@ export const HvCodeEditor = ({ if (keyDownListener) editor.onKeyDown((event: any) => keyDownListener(event, editor, monaco)); - // Listen for events to auto format code: on paste and on blur - editor.onDidBlurEditorText(() => handleFormatCode()); - editor.onDidPaste(() => handleFormatCode()); + // Listen for events to auto format code + if (formatter && !disableAutoFormat) { + // On mount + handleFormatCode(editor, monaco, formatter); + + // On blur + editor.onDidBlurEditorText(() => + handleFormatCode(editor, monaco, formatter), + ); + + // On paste + editor.onDidPaste(() => handleFormatCode(editor, monaco, formatter)); + } }; return ( diff --git a/packages/code-editor/src/CodeEditor/index.ts b/packages/code-editor/src/CodeEditor/index.ts index 6bb8194a87..81af0ec2bf 100644 --- a/packages/code-editor/src/CodeEditor/index.ts +++ b/packages/code-editor/src/CodeEditor/index.ts @@ -1 +1,2 @@ export * from "./CodeEditor"; +export * from "./languages"; diff --git a/packages/code-editor/src/CodeEditor/languages/index.ts b/packages/code-editor/src/CodeEditor/languages/index.ts new file mode 100644 index 0000000000..8b9b92f8d8 --- /dev/null +++ b/packages/code-editor/src/CodeEditor/languages/index.ts @@ -0,0 +1 @@ +export { hvXmlFormatter } from "./xml"; diff --git a/packages/code-editor/src/CodeEditor/languages/xml.ts b/packages/code-editor/src/CodeEditor/languages/xml.ts index 9040b8c815..279257755e 100644 --- a/packages/code-editor/src/CodeEditor/languages/xml.ts +++ b/packages/code-editor/src/CodeEditor/languages/xml.ts @@ -1,5 +1,5 @@ import { type Monaco } from "@monaco-editor/react"; -import formatter from "xml-formatter"; +import formatter, { XMLFormatterOptions } from "xml-formatter"; import { validateXML } from "xmllint-wasm"; // Helpful notes @@ -354,9 +354,31 @@ export const xmlOptions = { autoClosingBrackets: false, }; -/** XML code formatter. */ -export const xmlFormatter = (unformattedCode: string) => - formatter(unformattedCode, { - collapseContent: true, - forceSelfClosingEmptyTag: true, - }); +/** + * XML code formatter. + * When the code has errors, it is not formatted and `undefined` is returned. + * @param content Current code editor content + * @param editor Editor instance + * @param monaco Monaco instance + * @param options XML formatter options + * @returns `string with the formatted code or `undefined` + */ +export const hvXmlFormatter = async ( + content: string, + editor: any, + monaco: Monaco, + options?: XMLFormatterOptions, +) => { + const validation = await getXmlValidationMarkers(content, editor, monaco); // without schema for XML + const hasError = validation.some( + (marker: any) => marker?.severity === monaco.MarkerSeverity.Error, + ); + + // Format only if there are no errors + return hasError + ? undefined + : formatter(content, { + collapseContent: true, + ...options, + }); +}; diff --git a/packages/code-editor/src/CodeEditor/plugins.ts b/packages/code-editor/src/CodeEditor/plugins.ts index d971f46658..23749b1aee 100644 --- a/packages/code-editor/src/CodeEditor/plugins.ts +++ b/packages/code-editor/src/CodeEditor/plugins.ts @@ -4,7 +4,7 @@ import { getXmlCompletionProvider, getXmlValidationMarkers, handleXmlKeyDown, - xmlFormatter, + hvXmlFormatter, xmlOptions, } from "./languages/xml"; @@ -19,7 +19,11 @@ type ValidationMarker = ( schema?: string, // needed for XML language ) => Promise; type KeyDownListener = (event: any, editor: any, monaco: Monaco) => void; -type Formatter = (unformattedCode: string) => Promise | string; +export type Formatter = ( + content: string, + editor: any, + monaco: Monaco, +) => Promise; interface LanguagePlugin { completionProvider?: CompletionProvider; @@ -35,7 +39,7 @@ const xmlLanguagePlugin = (): LanguagePlugin => { validationMarker: getXmlValidationMarkers, keyDownListener: handleXmlKeyDown, editorOptions: xmlOptions, - formatter: xmlFormatter, + formatter: hvXmlFormatter, }; }; diff --git a/packages/code-editor/src/CodeEditor/stories/Xml/Xml.tsx b/packages/code-editor/src/CodeEditor/stories/Xml/Xml.tsx index 2e521450a1..a187671b72 100644 --- a/packages/code-editor/src/CodeEditor/stories/Xml/Xml.tsx +++ b/packages/code-editor/src/CodeEditor/stories/Xml/Xml.tsx @@ -2,6 +2,7 @@ import { useRef, useState } from "react"; import { HvCodeEditor, HvCodeEditorProps, + hvXmlFormatter, } from "@hitachivantara/uikit-react-code-editor"; import { HvButtonProps, @@ -9,6 +10,7 @@ import { HvDialogContent, HvDialogTitle, HvTreeView, + HvTypography, } from "@hitachivantara/uikit-react-core"; // The code for these utils can be found at: https://github.com/lumada-design/hv-uikit-react/tree/master/packages/code-editor/src/CodeEditor/stories/Xml/utils.tsx @@ -60,11 +62,24 @@ export const XmlStory = () => { attributes: Attributes; }>(); const [defaultExpandedKeys, setDefaultExpandedKeys] = useState([]); + const [hasErrors, setHasErrors] = useState(false); const editorRef = useRef(null); + const monacoRef = useRef(null); - const handleMount: HvCodeEditorProps["onMount"] = (editor) => { + const handleMount: HvCodeEditorProps["onMount"] = (editor, monaco) => { editorRef.current = editor; + monacoRef.current = monaco; + + // Listen for errors for validation state + monaco.editor.onDidChangeMarkers(() => { + const model = editor.getModel(); + const markers = monaco.editor.getModelMarkers({ resource: model.uri }); + const errors = markers.some( + (marker: any) => marker.severity === monaco.MarkerSeverity.Error, + ); + setHasErrors(errors); + }); }; const handleOpenSearch: HvButtonProps["onClick"] = () => { @@ -81,10 +96,33 @@ export const XmlStory = () => { setOpened(true); }; + const handleFormat = async () => { + try { + const content = editorRef.current.getValue(); + const formattedCode = await hvXmlFormatter( + content, + editorRef.current, + monacoRef.current, + ); + if (formattedCode) editorRef.current.setValue(formattedCode); + } catch (error) { + // eslint-disable-next-line no-console + if (import.meta.env.DEV) console.error(error); + } + }; + return (
-
+ + Has errors: {String(hasErrors)} + +
(
XML @@ -103,6 +105,9 @@ export const Header = ({ XML Tree + + Format +
);