import React, { useEffect, useRef } from 'react'; import MonacoEditor from 'react-monaco-editor'; import * as monaco from 'monaco-editor'; import type * as monacoTypes from 'monaco-editor'; import { v4 as uuidv4 } from 'uuid'; import { css } from '@emotion/css'; import { installAutoHeightLayout } from '../shared/autoHeightLayout'; import { hideMirroredInputAreaClassName } from '../shared/hideMirroredInputArea'; import { languageConfiguration } from './kql'; import { buildMonarchLanguage } from './monarch/buildLanguage'; import { getKQLCompletionProvider } from './completion/getCompletionProvider'; import type { KQLEditorProps } from './types'; export type { KQLEditorProps } from './types'; const LANG_ID = 'kql'; const SIZE_MAP: Record = { small: { className: 'ant-input-sm', minHeight: 24 }, middle: { className: 'ant-input-md', minHeight: 32 }, large: { className: 'ant-input-lg', minHeight: 40 }, }; const themeMap: Record = { light: 'kql-light', dark: 'kql-dark', }; const containerDisabledClassName = css` .monaco-editor { user-select: none; pointer-events: none; } `; const containerReadOnlyClassName = css` .monaco-editor .cursors-layer > .cursor { opacity: 0 !important; } `; let languageRegistered = false; function ensureLanguage() { if (languageRegistered) return; if (!monaco.languages.getLanguages().some((lang) => lang.id === LANG_ID)) { monaco.languages.register({ id: LANG_ID }); monaco.languages.setLanguageConfiguration(LANG_ID, languageConfiguration); monaco.languages.setMonarchTokensProvider(LANG_ID, buildMonarchLanguage()); } languageRegistered = true; } let themesEnsured = false; function ensureThemes() { if (themesEnsured) return; themesEnsured = true; monaco.editor.defineTheme('kql-light', { base: 'vs', inherit: true, rules: [ { token: 'keyword', foreground: '0070c1' }, { token: 'keyword.literal', foreground: '795e26' }, { token: 'operator', foreground: 'b8860b' }, { token: 'operator.wildcard', foreground: 'b8860b', fontStyle: 'bold' }, { token: 'string', foreground: 'a31515' }, { token: 'string.quote', foreground: 'a31515' }, { token: 'string.escape', foreground: '811f3f' }, { token: 'number', foreground: '098658' }, { token: 'delimiter.parenthesis', foreground: '0070c1' }, { token: 'delimiter.curly', foreground: '0070c1' }, ], colors: { 'editor.background': '#00000000', focusBorder: '#00000000', }, }); monaco.editor.defineTheme('kql-dark', { base: 'vs-dark', inherit: true, rules: [ { token: 'keyword', foreground: '9cdcfe' }, { token: 'keyword.literal', foreground: 'dcdcaa' }, { token: 'operator', foreground: 'ffd700' }, { token: 'operator.wildcard', foreground: 'ffd700', fontStyle: 'bold' }, { token: 'string', foreground: 'ce9178' }, { token: 'string.quote', foreground: 'ce9178' }, { token: 'string.escape', foreground: 'd7ba7d' }, { token: 'number', foreground: 'b5cea8' }, { token: 'delimiter.parenthesis', foreground: '9cdcfe' }, { token: 'delimiter.curly', foreground: '9cdcfe' }, ], colors: { 'editor.background': '#00000000', focusBorder: '#00000000', }, }); } /* ---------- Global keybinding rules registered once ---------- */ let globalKeybindingDisposables: monaco.IDisposable[] | null = null; function ensureGlobalKeybindings() { if (globalKeybindingDisposables) return; globalKeybindingDisposables = [ monaco.editor.addKeybindingRule({ keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyF, command: null, }), monaco.editor.addKeybindingRule({ keybinding: monaco.KeyCode.Enter, command: '-', when: '!suggestWidgetVisible', }), ]; } function disposeGlobalKeybindings() { if (!globalKeybindingDisposables) return; globalKeybindingDisposables.forEach((d) => d.dispose()); globalKeybindingDisposables = null; } const KQLMonacoEditor: React.FC = (props) => { const id = uuidv4(); const { className, maxHeight, fontSize, size = 'middle', theme = 'light', value = '', placeholder, enableAutocomplete = true, readOnly = false, disabled = false, variables, onChange, onEnter, onBlur, onFocus, editorDidMount, fetchFieldNames, fetchFieldValues, } = props; const containerRef = useRef(null); const editorRef = useRef(null); const disposablesRef = useRef([]); const variablesRef = useRef(variables ?? []); const fetchFieldNamesRef = useRef(fetchFieldNames); const fetchFieldValuesRef = useRef(fetchFieldValues); useEffect(() => { variablesRef.current = variables ?? []; }, [variables]); useEffect(() => { fetchFieldNamesRef.current = fetchFieldNames; }, [fetchFieldNames]); useEffect(() => { fetchFieldValuesRef.current = fetchFieldValues; }, [fetchFieldValues]); ensureLanguage(); useEffect(() => { if (!enableAutocomplete) return; const disposable = monaco.languages.registerCompletionItemProvider(LANG_ID, getKQLCompletionProvider({ variablesRef, fetchFieldNamesRef, fetchFieldValuesRef, editorRef, })); disposablesRef.current.push(disposable); return () => { disposable.dispose(); disposablesRef.current = disposablesRef.current.filter((d) => d !== disposable); }; }, [enableAutocomplete]); useEffect(() => { ensureGlobalKeybindings(); return () => { disposeGlobalKeybindings(); disposablesRef.current.forEach((d) => d.dispose()); disposablesRef.current = []; }; }, []); const handleEditorMount = (editor: monacoTypes.editor.IStandaloneCodeEditor) => { editorRef.current = editor; ensureThemes(); const isEditorFocused = editor.createContextKey('isEditorFocused' + id, false); editor.onDidBlurEditorWidget(() => { isEditorFocused.set(false); onBlur?.(editor.getValue()); const position = editor.getPosition(); if (position) { editor.setSelection(new monaco.Selection(position.lineNumber, position.column, position.lineNumber, position.column)); } }); editor.onDidFocusEditorText(() => { isEditorFocused.set(true); onFocus?.(editor.getValue()); }); if (containerRef.current) { installAutoHeightLayout(editor, containerRef.current, { heightStyleProperty: 'minHeight' }); } // Shift+Enter → newline editor.addCommand( monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => { const position = editor.getPosition(); if (position) { editor.executeEdits('shift-enter', [ { range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column), text: '\n', }, ]); editor.setPosition({ lineNumber: position.lineNumber + 1, column: 1 }); } }, 'isEditorFocused' + id, ); // Enter → execute query editor.addCommand( monaco.KeyCode.Enter, () => { onEnter?.(editor.getValue()); }, '!suggestWidgetVisible && isEditorFocused' + id, ); // Trigger suggest when `$` is typed const model = editor.getModel(); if (model) { const changeDisposable = model.onDidChangeContent((e) => { const insertedDollar = e.changes.some((c) => c.text === '$'); if (insertedDollar) { setTimeout(() => editor.trigger('kql', 'editor.action.triggerSuggest', {}), 0); } }); disposablesRef.current.push(changeDisposable); } editorDidMount?.(editor); }; const handleChange = (newValue: string) => { onChange?.(newValue); }; const themeValue = themeMap[theme]; return (
); }; export default KQLMonacoEditor; // Re-export parser and locator for advanced consumers export { parse, locate } from './parser';