import { css } from '@emotion/css'; import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'; import React, { PureComponent } from 'react'; import { GrafanaTheme2, monacoLanguageRegistry } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { withTheme2 } from '../../themes'; import { Themeable2 } from '../../types'; import { ReactMonacoEditorLazy } from './ReactMonacoEditorLazy'; import { registerSuggestions } from './suggestions'; import { CodeEditorProps, Monaco, MonacoEditor as MonacoEditorType, MonacoOptions } from './types'; type Props = CodeEditorProps & Themeable2; class UnthemedCodeEditor extends PureComponent { completionCancel?: monacoType.IDisposable; monaco?: Monaco; modelId?: string; constructor(props: Props) { super(props); } componentWillUnmount() { if (this.completionCancel) { this.completionCancel.dispose(); } this.props.onEditorWillUnmount?.(); } componentDidUpdate(oldProps: Props) { const { getSuggestions, language } = this.props; const newLanguage = oldProps.language !== language; const newGetSuggestions = oldProps.getSuggestions !== getSuggestions; if (newGetSuggestions || newLanguage) { if (this.completionCancel) { this.completionCancel.dispose(); } if (!this.monaco) { console.warn('Monaco instance not loaded yet'); return; } if (getSuggestions && this.modelId) { this.completionCancel = registerSuggestions(this.monaco, language, getSuggestions, this.modelId); } } if (newLanguage) { this.loadCustomLanguage(); } } loadCustomLanguage = () => { const { language } = this.props; const customLanguage = monacoLanguageRegistry.getIfExists(language); if (customLanguage) { return customLanguage.init(); } return Promise.resolve(); }; // This is replaced with a real function when the actual editor mounts getEditorValue = () => ''; onBlur = () => { const { onBlur } = this.props; if (onBlur) { onBlur(this.getEditorValue()); } }; onFocus = () => { const { onFocus } = this.props; if (onFocus) { onFocus(this.getEditorValue()); } }; onSave = () => { const { onSave } = this.props; if (onSave) { onSave(this.getEditorValue()); } }; handleBeforeMount = (monaco: Monaco) => { this.monaco = monaco; const { onBeforeEditorMount } = this.props; onBeforeEditorMount?.(monaco); }; handleOnMount = (editor: MonacoEditorType, monaco: Monaco) => { const { getSuggestions, language, onEditorDidMount } = this.props; this.modelId = editor.getModel()?.id; this.getEditorValue = () => editor.getValue(); if (getSuggestions && this.modelId) { this.completionCancel = registerSuggestions(monaco, language, getSuggestions, this.modelId); } // Save when pressing Ctrl+S or Cmd+S editor.onKeyDown((e: monacoType.IKeyboardEvent) => { if (e.keyCode === monaco.KeyCode.KeyS && (e.ctrlKey || e.metaKey)) { e.preventDefault(); this.onSave(); } }); editor.getModel()?.onDidChangeContent(this.handleChangeContent); if (onEditorDidMount) { onEditorDidMount(editor, monaco); } }; handleChangeContent = () => { const { onChange } = this.props; if (onChange) { onChange(this.getEditorValue()); } }; render() { const { theme, language, width, height, showMiniMap, showLineNumbers, readOnly, monacoOptions } = this.props; const { alwaysConsumeMouseWheel, ...restMonacoOptions } = monacoOptions ?? {}; const value = this.props.value ?? ''; const longText = value.length > 100; const containerStyles = this.props.containerStyles ?? getStyles(theme).container; const options: MonacoOptions = { wordWrap: 'off', tabSize: 2, codeLens: false, contextmenu: false, minimap: { enabled: longText && showMiniMap, renderCharacters: false, }, readOnly, lineNumbersMinChars: 4, lineDecorationsWidth: 1 * theme.spacing.gridSize, overviewRulerBorder: false, automaticLayout: true, padding: { top: 0.5 * theme.spacing.gridSize, bottom: 0.5 * theme.spacing.gridSize, }, fixedOverflowWidgets: true, // Ensures suggestions menu is drawn on top scrollbar: { alwaysConsumeMouseWheel: alwaysConsumeMouseWheel ?? false, }, }; if (!showLineNumbers) { options.glyphMargin = false; options.folding = false; options.lineNumbers = 'off'; options.lineNumbersMinChars = 0; } return (
); } } export const CodeEditor = withTheme2(UnthemedCodeEditor); const getStyles = (theme: GrafanaTheme2) => { return { container: css({ borderRadius: theme.shape.radius.default, border: `1px solid ${theme.components.input.borderColor}`, }), }; };