ConceptPortal-public/rsconcept/frontend/src/components/RSInput/RSInput.tsx

187 lines
6.0 KiB
TypeScript
Raw Normal View History

'use client';
import { Extension } from '@codemirror/state';
import { tags } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
import CodeMirror, { BasicSetupOptions, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror';
import clsx from 'clsx';
import { EditorView } from 'codemirror';
2023-12-27 18:44:37 +03:00
import { forwardRef, useCallback, useMemo, useRef } from 'react';
import Label from '@/components/ui/Label';
2024-04-01 19:07:20 +03:00
import { useConceptOptions } from '@/context/OptionsContext';
import { useRSForm } from '@/context/RSFormContext';
2024-03-26 12:49:38 +03:00
import { getFontClassName } from '@/models/miscellaneousAPI';
2024-02-20 13:12:06 +03:00
import { generateAlias, getCstTypePrefix, guessCstType } from '@/models/rsformAPI';
import { extractGlobals } from '@/models/rslangAPI';
import { ccBracketMatching } from './bracketMatching';
import { RSLanguage } from './rslang';
2023-12-28 14:04:44 +03:00
import { getSymbolSubstitute, RSTextWrapper } from './textEditing';
import { rsHoverTooltip } from './tooltip';
2023-12-28 14:04:44 +03:00
interface RSInputProps
extends Pick<
ReactCodeMirrorProps,
'id' | 'height' | 'minHeight' | 'maxHeight' | 'value' | 'onFocus' | 'onBlur' | 'placeholder' | 'style' | 'className'
> {
label?: string;
disabled?: boolean;
noTooltip?: boolean;
onChange?: (newValue: string) => void;
onAnalyze?: () => void;
}
2023-12-27 18:44:37 +03:00
const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
2024-03-20 15:03:53 +03:00
(
{
id, // prettier: split lines
label,
disabled,
noTooltip,
className,
style,
onChange,
onAnalyze,
...restProps
},
ref
) => {
2024-04-01 19:07:20 +03:00
const { darkMode, colors, mathFont } = useConceptOptions();
2023-12-28 14:04:44 +03:00
const { schema } = useRSForm();
2023-12-28 14:04:44 +03:00
const internalRef = useRef<ReactCodeMirrorRef>(null);
const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]);
2023-12-28 14:04:44 +03:00
const cursor = useMemo(() => (!disabled ? 'cursor-text' : 'cursor-default'), [disabled]);
const customTheme: Extension = useMemo(
() =>
createTheme({
theme: darkMode ? 'dark' : 'light',
settings: {
fontFamily: 'inherit',
background: !disabled ? colors.bgInput : colors.bgDefault,
foreground: colors.fgDefault,
2024-03-06 21:33:59 +03:00
selection: colors.bgHover,
caret: colors.fgDefault
2023-12-28 14:04:44 +03:00
},
styles: [
{ tag: tags.name, color: colors.fgPurple, cursor: 'default' }, // GlobalID
{ tag: tags.variableName, color: colors.fgGreen }, // LocalID
{ tag: tags.propertyName, color: colors.fgTeal }, // Radical
{ tag: tags.keyword, color: colors.fgBlue }, // keywords
{ tag: tags.literal, color: colors.fgBlue }, // literals
2023-12-30 19:43:24 +03:00
{ tag: tags.controlKeyword, fontWeight: '400' }, // R | I | D
2023-12-28 14:04:44 +03:00
{ tag: tags.unit, fontSize: '0.75rem' }, // indices
2023-12-30 19:43:24 +03:00
{ tag: tags.brace, color: colors.fgPurple, fontWeight: '600' } // braces (curly brackets)
2023-12-28 14:04:44 +03:00
]
}),
[disabled, colors, darkMode]
);
2023-12-28 14:04:44 +03:00
const editorExtensions = useMemo(
() => [
EditorView.lineWrapping,
RSLanguage,
ccBracketMatching(darkMode),
...(noTooltip ? [] : [rsHoverTooltip(schema?.items || [])])
],
[darkMode, schema?.items, noTooltip]
);
2023-12-28 14:04:44 +03:00
const handleInput = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (!thisRef.current) {
return;
}
const text = new RSTextWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
2024-02-20 13:12:06 +03:00
if (event.ctrlKey && event.code === 'Space') {
const selection = text.getSelection();
if (!selection.empty || !schema) {
return;
}
const hint = text.getText(selection.from - 1, selection.from);
const type = guessCstType(hint);
if (hint === getCstTypePrefix(type)) {
text.setSelection(selection.from - 1, selection.from);
}
const takenAliases = [...extractGlobals(thisRef.current.view?.state.doc.toString() ?? '')];
const newAlias = generateAlias(type, schema, takenAliases);
text.replaceWith(newAlias);
event.preventDefault();
event.stopPropagation();
} else if (event.altKey) {
2023-12-28 14:04:44 +03:00
if (text.processAltKey(event.code, event.shiftKey)) {
event.preventDefault();
event.stopPropagation();
}
} else if (!event.ctrlKey) {
const newSymbol = getSymbolSubstitute(event.code, event.shiftKey);
if (newSymbol) {
text.replaceWith(newSymbol);
event.preventDefault();
event.stopPropagation();
}
} else if (event.ctrlKey && event.code === 'KeyQ' && onAnalyze) {
onAnalyze();
event.preventDefault();
event.stopPropagation();
}
},
2024-02-20 13:12:06 +03:00
[thisRef, onAnalyze, schema]
2023-12-28 14:04:44 +03:00
);
2023-12-28 14:04:44 +03:00
return (
<div className={clsx('flex flex-col gap-2', className, cursor)} style={style}>
<Label text={label} />
2023-12-28 14:04:44 +03:00
<CodeMirror
2024-03-26 12:49:38 +03:00
className={getFontClassName(mathFont)}
2023-12-28 14:04:44 +03:00
id={id}
ref={thisRef}
basicSetup={editorSetup}
theme={customTheme}
extensions={editorExtensions}
indentWithTab={false}
onChange={onChange}
editable={!disabled}
onKeyDown={handleInput}
{...restProps}
/>
</div>
);
}
);
2023-12-28 14:04:44 +03:00
export default RSInput;
2024-03-20 15:03:53 +03:00
// ======= Internal ==========
const editorSetup: BasicSetupOptions = {
highlightSpecialChars: false,
history: true,
drawSelection: true,
syntaxHighlighting: false,
defaultKeymap: true,
historyKeymap: true,
lineNumbers: false,
highlightActiveLineGutter: false,
foldGutter: false,
dropCursor: true,
allowMultipleSelections: false,
indentOnInput: false,
bracketMatching: false,
closeBrackets: false,
autocompletion: false,
rectangularSelection: false,
crosshairCursor: false,
highlightActiveLine: false,
highlightSelectionMatches: false,
closeBracketsKeymap: false,
searchKeymap: false,
foldKeymap: false,
completionKeymap: false,
lintKeymap: false
};