2024-06-07 20:17:03 +03:00
|
|
|
'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';
|
|
|
|
import { forwardRef, useCallback, useMemo, useRef } from 'react';
|
|
|
|
|
|
|
|
import Label from '@/components/ui/Label';
|
2024-06-26 19:47:05 +03:00
|
|
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
2024-06-18 19:53:56 +03:00
|
|
|
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
2024-06-07 20:17:03 +03:00
|
|
|
import { generateAlias, getCstTypePrefix, guessCstType } from '@/models/rsformAPI';
|
|
|
|
import { extractGlobals } from '@/models/rslangAPI';
|
|
|
|
|
|
|
|
import { ccBracketMatching } from './bracketMatching';
|
2024-06-18 19:53:56 +03:00
|
|
|
import { rsNavigation } from './clickNavigation';
|
2024-06-07 20:17:03 +03:00
|
|
|
import { RSLanguage } from './rslang';
|
|
|
|
import { getSymbolSubstitute, RSTextWrapper } from './textEditing';
|
|
|
|
import { rsHoverTooltip } from './tooltip';
|
|
|
|
|
|
|
|
interface RSInputProps
|
|
|
|
extends Pick<
|
|
|
|
ReactCodeMirrorProps,
|
|
|
|
| 'id' // prettier: split-lines
|
|
|
|
| 'height'
|
|
|
|
| 'minHeight'
|
|
|
|
| 'maxHeight'
|
|
|
|
| 'value'
|
|
|
|
| 'onFocus'
|
|
|
|
| 'onBlur'
|
|
|
|
| 'placeholder'
|
|
|
|
| 'style'
|
|
|
|
| 'className'
|
|
|
|
> {
|
|
|
|
label?: string;
|
|
|
|
disabled?: boolean;
|
|
|
|
noTooltip?: boolean;
|
|
|
|
onChange?: (newValue: string) => void;
|
|
|
|
onAnalyze?: () => void;
|
2024-06-18 19:53:56 +03:00
|
|
|
schema?: IRSForm;
|
|
|
|
onOpenEdit?: (cstID: ConstituentaID) => void;
|
2024-06-07 20:17:03 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
|
|
|
(
|
|
|
|
{
|
|
|
|
id, // prettier: split lines
|
|
|
|
label,
|
|
|
|
disabled,
|
|
|
|
noTooltip,
|
|
|
|
|
2024-06-18 19:53:56 +03:00
|
|
|
schema,
|
|
|
|
onOpenEdit,
|
|
|
|
|
2024-06-07 20:17:03 +03:00
|
|
|
className,
|
|
|
|
style,
|
|
|
|
|
|
|
|
onChange,
|
|
|
|
onAnalyze,
|
|
|
|
...restProps
|
|
|
|
},
|
|
|
|
ref
|
|
|
|
) => {
|
2024-08-21 20:32:04 +03:00
|
|
|
const { darkMode, colors } = useConceptOptions();
|
2024-06-07 20:17:03 +03:00
|
|
|
|
|
|
|
const internalRef = useRef<ReactCodeMirrorRef>(null);
|
|
|
|
const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]);
|
|
|
|
|
|
|
|
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,
|
|
|
|
selection: colors.bgHover,
|
|
|
|
caret: colors.fgDefault
|
|
|
|
},
|
|
|
|
styles: [
|
2024-06-19 12:09:10 +03:00
|
|
|
{ tag: tags.name, color: colors.fgPurple, cursor: schema ? 'default' : cursor }, // GlobalID
|
2024-06-07 20:17:03 +03:00
|
|
|
{ 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
|
|
|
|
{ tag: tags.controlKeyword, fontWeight: '400' }, // R | I | D
|
|
|
|
{ tag: tags.unit, fontSize: '0.75rem' }, // indices
|
|
|
|
{ tag: tags.brace, color: colors.fgPurple, fontWeight: '600' } // braces (curly brackets)
|
|
|
|
]
|
|
|
|
}),
|
2024-06-19 12:09:10 +03:00
|
|
|
[disabled, colors, darkMode, schema, cursor]
|
2024-06-07 20:17:03 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
const editorExtensions = useMemo(
|
|
|
|
() => [
|
|
|
|
EditorView.lineWrapping,
|
|
|
|
RSLanguage,
|
|
|
|
ccBracketMatching(darkMode),
|
2024-06-18 19:53:56 +03:00
|
|
|
...(!schema || !onOpenEdit ? [] : [rsNavigation(schema, onOpenEdit)]),
|
2024-06-19 12:09:10 +03:00
|
|
|
...(noTooltip || !schema ? [] : [rsHoverTooltip(schema, onOpenEdit !== undefined)])
|
2024-06-07 20:17:03 +03:00
|
|
|
],
|
2024-06-18 19:53:56 +03:00
|
|
|
[darkMode, schema, noTooltip, onOpenEdit]
|
2024-06-07 20:17:03 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
const handleInput = useCallback(
|
|
|
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
|
|
if (!thisRef.current) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const text = new RSTextWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
|
|
|
|
if ((event.ctrlKey || event.metaKey) && 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) {
|
|
|
|
if (text.processAltKey(event.code, event.shiftKey)) {
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
}
|
|
|
|
} else if (!(event.ctrlKey || event.metaKey)) {
|
|
|
|
const newSymbol = getSymbolSubstitute(event.code, event.shiftKey);
|
|
|
|
if (newSymbol) {
|
|
|
|
text.replaceWith(newSymbol);
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
}
|
|
|
|
} else if (event.code === 'KeyQ' && onAnalyze) {
|
|
|
|
onAnalyze();
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[thisRef, onAnalyze, schema]
|
|
|
|
);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className={clsx('flex flex-col gap-2', className, cursor)} style={style}>
|
|
|
|
<Label text={label} />
|
|
|
|
<CodeMirror
|
2024-08-23 22:53:32 +03:00
|
|
|
className='font-math'
|
2024-06-07 20:17:03 +03:00
|
|
|
id={id}
|
|
|
|
ref={thisRef}
|
|
|
|
basicSetup={editorSetup}
|
|
|
|
theme={customTheme}
|
|
|
|
extensions={editorExtensions}
|
|
|
|
indentWithTab={false}
|
|
|
|
onChange={onChange}
|
|
|
|
editable={!disabled}
|
|
|
|
onKeyDown={handleInput}
|
|
|
|
{...restProps}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
export default RSInput;
|
|
|
|
|
|
|
|
// ======= 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
|
|
|
|
};
|