UNPKG

@finos/legend-lego

Version:
180 lines 8.93 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /** * Copyright (c) 2020-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { useState, useRef, useEffect } from 'react'; import { editor as monacoEditorAPI, KeyCode, KeyMod, } from 'monaco-editor'; import { getBaseCodeEditorOptions, resetLineNumberGutterWidth, getCodeEditorValue, normalizeLineEnding, setErrorMarkers, clearMarkers, CODE_EDITOR_THEME, configureCodeEditor, } from '@finos/legend-code-editor'; import { DEFAULT_TAB_SIZE, useApplicationStore, APPLICATION_EVENT, DEFAULT_MONOSPACED_FONT_FAMILY, } from '@finos/legend-application'; import { clsx, WordWrapIcon } from '@finos/legend-art'; import { LogEvent } from '@finos/legend-shared'; export const configureCodeEditorComponent = async (applicationStore) => { await configureCodeEditor(DEFAULT_MONOSPACED_FONT_FAMILY, (error) => applicationStore.logService.error(LogEvent.create(APPLICATION_EVENT.APPLICATION_SETUP__FAILURE), error.message)); // override native hotkeys supported by monaco-editor // here we map these keys to a dummy command that would just dispatch the key combination // to the application keyboard shortcut service, effectively bypassing the command associated // with the native keybinding const OVERRIDE_DEFAULT_KEYBINDING_COMMAND = 'legend.code-editor.override-default-keybinding'; monacoEditorAPI.registerCommand(OVERRIDE_DEFAULT_KEYBINDING_COMMAND, (accessor, ...args) => { applicationStore.keyboardShortcutsService.dispatch(args[0]); }); const hotkeyMapping = [ [KeyCode.F1, 'F1'], // show command center [KeyCode.F8, 'F8'], // show error [KeyCode.F9, 'F9'], // toggle debugger breakpoint [KeyMod.WinCtrl | KeyCode.KeyG, 'Control+KeyG'], // go-to line command [KeyMod.WinCtrl | KeyCode.KeyB, 'Control+KeyB'], // cursor move (core command) [KeyMod.WinCtrl | KeyCode.KeyO, 'Control+KeyO'], // cursor move (core command) [KeyMod.WinCtrl | KeyCode.KeyD, 'Control+KeyD'], // cursor move (core command) [KeyMod.WinCtrl | KeyCode.KeyP, 'Control+KeyP'], // cursor move (core command) [KeyMod.Shift | KeyCode.F10, 'Shift+F10'], // show editor context menu [KeyMod.WinCtrl | KeyCode.F2, 'Control+F2'], // change all instances [KeyMod.WinCtrl | KeyCode.F12, 'Control+F12'], // go-to definition ]; monacoEditorAPI.addKeybindingRules(hotkeyMapping.map(([nativeCodeEditorKeyBinding, keyCombination]) => ({ keybinding: nativeCodeEditorKeyBinding, command: OVERRIDE_DEFAULT_KEYBINDING_COMMAND, commandArgs: keyCombination, }))); }; /** * Normally `monaco-editor` worker disposes after 5 minutes staying idle, but we fasten * this pace just in case the usage of the editor causes memory-leak somehow */ export const disposeCodeEditor = (editor) => { editor.dispose(); // NOTE: just to be sure, we dispose the model after disposing the editor editor.getModel()?.dispose(); }; export const CodeEditor = (props) => { const { inputValue, updateInput, lightTheme, language, isReadOnly, hideMinimap, hideGutter, hidePadding, hideActionBar, lineToScroll, extraEditorOptions, error, } = props; const applicationStore = useApplicationStore(); const [editor, setEditor] = useState(); const [isWordWrap, setIsWordWrap] = useState(false); const onDidChangeModelContentEventDisposer = useRef(undefined); /** * NOTE: we want to normalize line ending here since if the original * input value includes CR '\r' character, it will get normalized, calling * the updateInput method and cause a rerender. With the way we setup * `onChange` method, React will warn about `setState` being called in * `render` method. * See https://github.com/finos/legend-studio/issues/608 */ const value = normalizeLineEnding(inputValue); const textInputRef = useRef(null); const toggleWordWrap = () => { const updatedWordWrap = !isWordWrap; setIsWordWrap(updatedWordWrap); editor?.updateOptions({ wordWrap: updatedWordWrap ? 'on' : 'off', }); }; useEffect(() => { if (!editor && textInputRef.current) { const element = textInputRef.current; const _editor = monacoEditorAPI.create(element, { ...getBaseCodeEditorOptions(), theme: applicationStore.layoutService .TEMPORARY__isLightColorThemeEnabled ? (lightTheme ?? CODE_EDITOR_THEME.BUILT_IN__VSCODE_LIGHT) : CODE_EDITOR_THEME.DEFAULT_DARK, // layout glyphMargin: !hidePadding, padding: !hidePadding ? { top: 20, bottom: 20 } : { top: 0, bottom: 0 }, formatOnType: true, formatOnPaste: true, }); setEditor(_editor); } }, [applicationStore, lightTheme, hidePadding, editor]); useEffect(() => { if (editor) { resetLineNumberGutterWidth(editor); const model = editor.getModel(); if (model) { monacoEditorAPI.setModelLanguage(model, language); } } }, [editor, language]); useEffect(() => { if (editor && lineToScroll !== undefined) { editor.revealLineInCenter(lineToScroll); } }, [editor, lineToScroll]); if (editor) { // dispose the old editor content setter in case the `updateInput` handler changes // for a more extensive note on this, see `LambdaEditor` onDidChangeModelContentEventDisposer.current?.dispose(); onDidChangeModelContentEventDisposer.current = editor.onDidChangeModelContent(() => { const currentVal = getCodeEditorValue(editor); if (currentVal !== value) { updateInput?.(currentVal); } }); // Set the text value and editor options const currentValue = getCodeEditorValue(editor); if (currentValue !== value) { editor.setValue(value); } editor.updateOptions({ readOnly: Boolean(isReadOnly), minimap: { enabled: !hideMinimap }, // Hide the line number gutter // See https://github.com/microsoft/vscode/issues/30795 ...(hideGutter ? { glyphMargin: !hidePadding, folding: false, lineNumbers: 'off', lineDecorationsWidth: 0, } : {}), ...(extraEditorOptions ?? {}), }); const model = editor.getModel(); model?.updateOptions({ tabSize: DEFAULT_TAB_SIZE }); if (model) { if (error?.sourceInformation) { setErrorMarkers(model, [ { message: error.message, startLineNumber: error.sourceInformation.startLine, startColumn: error.sourceInformation.startColumn, endLineNumber: error.sourceInformation.endLine, endColumn: error.sourceInformation.endColumn, }, ]); } else { clearMarkers(); } } } // dispose editor useEffect(() => () => { if (editor) { disposeCodeEditor(editor); onDidChangeModelContentEventDisposer.current?.dispose(); } }, [editor]); return (_jsxs("div", { className: "code-editor", children: [!hideActionBar && (_jsx("div", { className: "code-editor__header", children: _jsx("button", { tabIndex: -1, className: clsx('code-editor__header__action', { 'code-editor__header__action--active': isWordWrap, }), onClick: toggleWordWrap, title: `[${isWordWrap ? 'on' : 'off'}] Toggle word wrap`, children: _jsx(WordWrapIcon, {}) }) })), _jsx("div", { className: clsx('code-editor__content', { 'code-editor__content--padding': !hidePadding, 'code-editor__content--with__header': !hideActionBar, }), children: _jsx("div", { className: "code-editor__body", ref: textInputRef }) })] })); }; //# sourceMappingURL=CodeEditor.js.map