in frontend/libs/code-editor/src/lib/CodeEditor.tsx [37:558]
export function CodeEditor({
language,
theme,
codeEditorPlace,
onEditorReady,
onEscape,
onCodeChange,
onSaveButton,
onUndo,
onRedo,
onEnter,
onTab,
onStartPointClick,
onStopPointClick,
onRightArrow,
onLeftArrow,
onBottomArrow,
onTopArrow,
onCtrlEnter,
onBlur,
onFocus,
onGoToTable,
onGoToField,
options = {},
errors = [],
functions = [],
parsedSheets = {},
disableHelpers = false,
setCode,
setFocus,
sheetContent = '',
currentTableName,
currentFieldName,
}: CodeEditorProps) {
const {
selectedError,
updateSelectedError,
initialOffset,
updateInitialOffset,
setCodeEditorInstance,
} = useContext(CodeEditorContext);
const [monaco, setMonaco] = useState<Monaco | undefined>(undefined);
const [codeEditor, setCodeEditor] = useState<
editor.IStandaloneCodeEditor | undefined
>(undefined);
useInlineSuggestions({
monaco,
codeEditorPlace,
disableHelpers,
language,
functions,
sheetContent,
currentTableName,
currentFieldName,
});
const callbacks = useRef<CodeEditorCallbacks>({
onSaveButton,
onEnter,
onTab,
onRightArrow,
onLeftArrow,
onBottomArrow,
onTopArrow,
onCtrlEnter,
onUndo,
onRedo,
onEscape,
onGoToTable,
onGoToField,
});
useEffect(() => {
callbacks.current = {
onSaveButton,
onEnter,
onTab,
onRightArrow,
onLeftArrow,
onBottomArrow,
onTopArrow,
onCtrlEnter,
onRedo,
onUndo,
onEscape,
onGoToTable,
onGoToField,
};
}, [
onSaveButton,
onEnter,
onTab,
onRightArrow,
onLeftArrow,
onBottomArrow,
onTopArrow,
onCtrlEnter,
onRedo,
onUndo,
onEscape,
onGoToTable,
onGoToField,
]);
const makeCallback = useCallback(
(
callback:
| 'onEnter'
| 'onEscape'
| 'onUndo'
| 'onRedo'
| 'onTab'
| 'onRightArrow'
| 'onCtrlEnter'
) => {
callbacks.current[callback]?.();
},
[]
);
useEditorRegisterProviders({
monaco,
codeEditor,
codeEditorPlace,
functions,
parsedSheets,
language,
disableHelpers,
currentTableName,
currentFieldName,
});
useHandleDefaultFeatures({
codeEditor,
codeEditorPlace,
onEnter,
onTab,
onRightArrow,
onLeftArrow,
onTopArrow,
onBottomArrow,
onCtrlEnter,
onEscape,
onUndo,
onRedo,
makeCallback,
});
const checkEnablePointAndClick = useCallback(
(value: string | undefined) => {
// set timeout because need to wait for monaco focus
setTimeout(() => {
if (!codeEditor || !value || !onStartPointClick || !onStopPointClick)
return;
if (!codeEditor.hasTextFocus()) return;
const cursorOffset = getCursorOffset(codeEditor) || 0;
if (canEnablePointAndClick(value, codeEditor)) {
onStartPointClick(cursorOffset);
} else {
onStopPointClick(cursorOffset);
}
}, 0);
},
[codeEditor, onStartPointClick, onStopPointClick]
);
const setCodeEditorValue = useCallback(
(value: string) => {
if (codeEditor && codeEditor.getValue() !== value) {
codeEditor.setValue(value);
checkEnablePointAndClick(value);
}
},
[codeEditor, checkEnablePointAndClick]
);
const setEditorFocus = useCallback(
(
{ cursorOffset }: { cursorOffset?: number | undefined } = {
cursorOffset: undefined,
}
) => {
codeEditor?.focus();
if (cursorOffset) {
const model = codeEditor?.getModel();
if (!model) return;
const length = model.getValueLength();
codeEditor?.setPosition(
model.getPositionAt(
cursorOffset < 0 ? length + cursorOffset : cursorOffset
)
);
return;
}
// Set cursor to end by default
const model = codeEditor?.getModel();
if (!model) return;
const lastLine = model.getLineCount();
const lastColumn = model.getLineMaxColumn(lastLine);
codeEditor?.setPosition({ lineNumber: lastLine, column: lastColumn });
},
[codeEditor]
);
const jumpToDSLPosition = useCallback(
(dslPosition: number) => {
codeEditor?.focus();
const model = codeEditor?.getModel();
if (!model) return;
const position = model.getPositionAt(dslPosition);
codeEditor?.setPosition(position);
codeEditor?.revealLineInCenterIfOutsideViewport(position.lineNumber);
},
[codeEditor]
);
useEffect(() => {
if (!setCode) return;
setCode.current = setCodeEditorValue;
}, [setCode, setCodeEditorValue]);
useEffect(() => {
if (!setFocus) return;
setFocus.current = setEditorFocus;
}, [setFocus, setEditorFocus]);
useEffect(() => {
if (!codeEditor || codeEditorPlace !== 'codeEditor') {
return;
}
if (initialOffset !== undefined) {
setTimeout(() => {
jumpToDSLPosition(initialOffset);
updateInitialOffset(undefined);
}, 0);
}
}, [
codeEditor,
codeEditorPlace,
initialOffset,
jumpToDSLPosition,
updateInitialOffset,
]);
useEffect(() => {
onEditorReady?.(codeEditor);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [codeEditor]);
useEffect(() => {
const onFocusDisposable = codeEditor?.onDidFocusEditorText(() => {
onFocus?.();
});
return () => onFocusDisposable?.dispose();
}, [codeEditor, onFocus]);
useEffect(() => {
const onBlurDisposable = codeEditor?.onDidBlurEditorText(() => {
// Fix case when closing very custom 'All formulas' widget or open it more than once
// fires blur event and closes editor
setTimeout(() => {
const widget = document.querySelector('.editor-widget.suggest-widget');
if (!widget) {
onBlur?.();
return;
}
const isVisible = (widget as HTMLElement).style.display !== 'none';
if (isVisible) return;
onBlur?.();
}, 0);
});
return () => onBlurDisposable?.dispose();
}, [codeEditor, onBlur]);
useEffect(() => {
if (codeEditor && selectedError && codeEditorPlace === 'codeEditor') {
const position: IPosition = {
lineNumber: selectedError.source.startLine,
column: selectedError.source.startColumn || 1,
};
setTimeout(() => {
codeEditor.focus();
codeEditor.setPosition(position);
codeEditor.revealLineInCenterIfOutsideViewport(position.lineNumber);
updateSelectedError(null);
}, 0);
}
}, [selectedError, codeEditor, updateSelectedError, codeEditorPlace]);
useEffect(() => {
if (!codeEditor || !monaco) return;
const codeEditorModel = codeEditor.getModel();
if (codeEditorModel) {
monaco.editor.setModelMarkers(codeEditorModel, '', errors);
}
}, [codeEditor, monaco, errors]);
useEffect(() => {
if (!codeEditor) return;
codeEditor.onDidChangeCursorSelection(() => {
checkEnablePointAndClick(codeEditor.getValue());
});
codeEditor.onDidFocusEditorText(() => {
checkEnablePointAndClick(codeEditor.getValue());
});
}, [checkEnablePointAndClick, codeEditor]);
const codeChangeCallback = useCallback(
async (value: string | undefined, _: editor.IModelContentChangedEvent) => {
if (onCodeChange) onCodeChange(value || '');
checkEnablePointAndClick(value);
},
[onCodeChange, checkEnablePointAndClick]
);
/**
* Hide suggest widget and parameter hints widget on spreadsheet scroll
* Cause: widgets are not scrolling with cell editor
*/
useEffect(() => {
if (codeEditorPlace !== 'cellEditor') return;
const handleScroll = () => {
codeEditor?.trigger('', 'hideSuggestWidget', {});
codeEditor?.trigger('', 'closeParameterHints', {});
};
const gridDataScroller = getDataScroller();
gridDataScroller?.addEventListener('scroll', handleScroll);
return () => {
gridDataScroller?.removeEventListener('scroll', handleScroll);
};
}, [codeEditor, codeEditorPlace]);
useEffect(() => {
if (!monaco) return;
registerTheme(monaco, theme);
}, [language, monaco, theme]);
useEffect(() => {
if (codeEditorPlace !== 'codeEditor' || !codeEditor) return;
setCodeEditorInstance(codeEditor);
}, [codeEditor, codeEditorPlace, setCodeEditorInstance]);
const onCodeEditorMount = useCallback(
(editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
setCodeEditor(editor);
setMonaco(monaco);
editor.onDidFocusEditorText(() => {
editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyS, () =>
callbacks.current.onSaveButton?.(true)
);
});
monaco.editor.registerCommand(
CustomCommands.SuggestionInsertFunction,
(_, modelId: string) => {
if (!modelId) return;
const editors = monaco.editor.getEditors();
// To get Position we need first to get correct editor instance by model id
const currentEditor = editors.find((e) => {
const model = e.getModel();
return model?.id === modelId;
});
if (!currentEditor) return;
const position = currentEditor.getPosition();
if (!position) return;
currentEditor.setPosition({
lineNumber: position.lineNumber,
column: Math.max(0, position.column - 1),
});
currentEditor.getAction('editor.action.triggerParameterHints')?.run();
}
);
monaco.editor.registerCommand(
CustomCommands.SuggestionAcceptTableOrField,
(_, modelId: string) => {
if (!modelId) return;
const editors = monaco.editor.getEditors();
// To get Position we need first to get correct editor instance by model id
const currentEditor = editors.find((e) => {
const model = e.getModel();
return model?.id === modelId;
});
if (!currentEditor) return;
currentEditor.trigger('', 'editor.action.triggerSuggest', {});
}
);
// Go to table context menu item
editor.addAction({
id: 'go-to-table',
label: 'Go To Table',
contextMenuGroupId: 'navigation',
contextMenuOrder: 1,
keybindings: [KeyMod.CtrlCmd | KeyCode.F6],
run: function (editor) {
try {
const dsl = editor.getModel()?.getValue();
const parsedSheet = SheetReader.parseSheet(dsl);
const position = editor.getPosition();
if (!parsedSheet || !position) return;
const table = getTableAtPosition(parsedSheet, position.lineNumber);
if (!table) return;
callbacks.current.onGoToTable?.(table.tableName);
} catch (error) {
// empty block
}
},
});
// Go to field context menu item
editor.addAction({
id: 'go-to-field',
label: 'Go To Field',
contextMenuGroupId: 'navigation',
contextMenuOrder: 2,
run: function (editor) {
try {
const model = editor.getModel();
if (!model) return;
const dsl = model.getValue();
const parsedSheet = SheetReader.parseSheet(dsl);
const position = editor.getPosition();
if (!parsedSheet || !position) return;
const offset = model.getOffsetAt(position);
const field = getFieldAtPosition(
parsedSheet,
position.lineNumber,
offset
);
if (!field) return;
const { fieldName, tableName } = field.key;
callbacks.current.onGoToField?.(tableName, fieldName);
} catch (error) {
// empty block
}
},
});
},
[]
);
return (
<Editor
beforeMount={(monaco) =>
registerQuantgridLanguage(monaco, language, theme)
}
language={language}
options={{
...codeEditorOptions,
...options,
}}
theme={codeEditorTheme}
onChange={codeChangeCallback}
onMount={onCodeEditorMount}
/>
);
}