Refactor CodeMirror wrappers and simplify available grams

This commit is contained in:
IRBorisov 2023-09-28 16:31:10 +03:00
parent 1054db3a8a
commit a8ad142544
9 changed files with 126 additions and 71 deletions

View File

@ -12,7 +12,7 @@ import { TokenID } from '../../models/rslang';
import Label from '../Common/Label'; import Label from '../Common/Label';
import { ccBracketMatching } from './bracketMatching'; import { ccBracketMatching } from './bracketMatching';
import { RSLanguage } from './rslang'; import { RSLanguage } from './rslang';
import { getSymbolSubstitute,TextWrapper } from './textEditing'; import { getSymbolSubstitute,RSTextWrapper } from './textEditing';
import { rsHoverTooltip } from './tooltip'; import { rsHoverTooltip } from './tooltip';
const editorSetup: BasicSetupOptions = { const editorSetup: BasicSetupOptions = {
@ -99,7 +99,7 @@ function RSInput({
if (!thisRef.current) { if (!thisRef.current) {
return; return;
} }
const text = new TextWrapper(thisRef.current as Required<ReactCodeMirrorRef>); const text = new RSTextWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
if (event.shiftKey && event.key === '*' && !event.altKey) { if (event.shiftKey && event.key === '*' && !event.altKey) {
text.insertToken(TokenID.DECART); text.insertToken(TokenID.DECART);
} else if (event.altKey) { } else if (event.altKey) {

View File

@ -3,6 +3,7 @@
import { ReactCodeMirrorRef } from '@uiw/react-codemirror'; import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { TokenID } from '../../models/rslang'; import { TokenID } from '../../models/rslang';
import { CodeMirrorWrapper } from '../../utils/codemirror';
export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): string | undefined { export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): string | undefined {
if (shiftPressed) { if (shiftPressed) {
@ -41,43 +42,17 @@ export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): str
return undefined; return undefined;
} }
// Note: Wrapper class for textareafield. /**
// WARNING! Manipulations on value do not support UNDO browser * Wrapper class for RSLang editor.
// WARNING! No checks for selection out of text boundaries */
export class TextWrapper { export class RSTextWrapper extends CodeMirrorWrapper {
ref: Required<ReactCodeMirrorRef>
constructor(object: Required<ReactCodeMirrorRef>) { constructor(object: Required<ReactCodeMirrorRef>) {
this.ref = object; super(object);
}
replaceWith(data: string) {
this.ref.view.dispatch(this.ref.view.state.replaceSelection(data));
}
envelopeWith(left: string, right: string) {
const hasSelection = this.ref.view.state.selection.main.from !== this.ref.view.state.selection.main.to
const newSelection = hasSelection ? {
anchor: this.ref.view.state.selection.main.from,
head: this.ref.view.state.selection.main.to + left.length + right.length
} : {
anchor: this.ref.view.state.selection.main.to + left.length + right.length - 1,
}
this.ref.view.dispatch({
changes: [
{from: this.ref.view.state.selection.main.from, insert: left},
{from: this.ref.view.state.selection.main.to, insert: right}
],
selection: newSelection
});
}
insertChar(key: string) {
this.replaceWith(key);
} }
insertToken(tokenID: TokenID): boolean { insertToken(tokenID: TokenID): boolean {
const hasSelection = this.ref.view.state.selection.main.from !== this.ref.view.state.selection.main.to const selection = this.getSelection();
const hasSelection = selection.from !== selection.to
switch (tokenID) { switch (tokenID) {
case TokenID.NT_DECLARATIVE_EXPR: { case TokenID.NT_DECLARATIVE_EXPR: {
if (hasSelection) { if (hasSelection) {
@ -87,7 +62,7 @@ export class TextWrapper {
} }
this.ref.view.dispatch({ this.ref.view.dispatch({
selection: { selection: {
anchor: this.ref.view.state.selection.main.from + 2, anchor: selection.from + 2,
} }
}); });
return true; return true;
@ -120,7 +95,7 @@ export class TextWrapper {
this.envelopeWith('(', ')'); this.envelopeWith('(', ')');
this.ref.view.dispatch({ this.ref.view.dispatch({
selection: { selection: {
anchor: hasSelection ? this.ref.view.state.selection.main.to: this.ref.view.state.selection.main.from + 1, anchor: hasSelection ? selection.to: selection.from + 1,
} }
}); });
return true; return true;
@ -130,14 +105,14 @@ export class TextWrapper {
if (hasSelection) { if (hasSelection) {
this.ref.view.dispatch({ this.ref.view.dispatch({
selection: { selection: {
anchor: hasSelection ? this.ref.view.state.selection.main.to: this.ref.view.state.selection.main.from + 1, anchor: hasSelection ? selection.to: selection.from + 1,
} }
}); });
} }
return true; return true;
} }
case TokenID.BOOLEAN: { case TokenID.BOOLEAN: {
const selStart = this.ref.view.state.selection.main.from; const selStart = selection.from;
if (hasSelection && this.ref.view.state.sliceDoc(selStart, selStart + 1) === '') { if (hasSelection && this.ref.view.state.sliceDoc(selStart, selStart + 1) === '') {
this.envelopeWith('', ''); this.envelopeWith('', '');
} else { } else {

View File

@ -9,10 +9,11 @@ import { RefObject, useCallback, useMemo, useRef, useState } from 'react';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
import useResolveText from '../../hooks/useResolveText'; import useResolveText from '../../hooks/useResolveText';
import { CodeMirrorWrapper } from '../../utils/codemirror';
import Label from '../Common/Label'; import Label from '../Common/Label';
import Modal from '../Common/Modal'; import Modal from '../Common/Modal';
import PrettyJson from '../Common/PrettyJSON'; import PrettyJson from '../Common/PrettyJSON';
import { NaturalLanguage } from './parse'; import { NaturalLanguage, ReferenceTokens } from './parse';
import { refsHoverTooltip } from './tooltip'; import { refsHoverTooltip } from './tooltip';
const editorSetup: BasicSetupOptions = { const editorSetup: BasicSetupOptions = {
@ -97,7 +98,7 @@ function RefsInput({
() => [ () => [
EditorView.lineWrapping, EditorView.lineWrapping,
NaturalLanguage, NaturalLanguage,
refsHoverTooltip(schema?.items || [], colors), refsHoverTooltip(schema?.items || [], colors)
], [schema?.items, colors]); ], [schema?.items, colors]);
function handleChange(newValue: string) { function handleChange(newValue: string) {
@ -116,7 +117,7 @@ function RefsInput({
const handleInput = useCallback( const handleInput = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => { (event: React.KeyboardEvent<HTMLDivElement>) => {
if (!thisRef.current) { if (!thisRef.current?.view) {
event.preventDefault(); event.preventDefault();
return; return;
} }
@ -129,6 +130,10 @@ function RefsInput({
return; return;
} }
} }
if (event.ctrlKey && event.code === 'Space') {
const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
wrap.fixSelection(ReferenceTokens);
}
}, [thisRef, resolveText, value]); }, [thisRef, resolveText, value]);
return ( return (
@ -164,6 +169,7 @@ function RefsInput({
onKeyDown={handleInput} onKeyDown={handleInput}
onFocus={handleFocusIn} onFocus={handleFocusIn}
onBlur={handleFocusOut} onBlur={handleFocusOut}
// spellCheck={true} // TODO: figure out while automatic spellcheck doesnt work or implement with extension
{...props} {...props}
/> />
</div> </div>

View File

@ -1,6 +1,11 @@
import {LRLanguage} from '@codemirror/language' import {LRLanguage} from '@codemirror/language'
import { parser } from './parser'; import { parser } from './parser';
import { RefEntity, RefSyntactic } from './parser.terms';
export const ReferenceTokens: number[] = [
RefSyntactic, RefEntity
]
export const NaturalLanguage = LRLanguage.define({ export const NaturalLanguage = LRLanguage.define({
parser: parser, parser: parser,

View File

@ -1,4 +1,4 @@
import { syntaxTree } from "@codemirror/language" import { syntaxTree } from '@codemirror/language'
import { Extension } from '@codemirror/state'; import { Extension } from '@codemirror/state';
import { hoverTooltip } from '@codemirror/view'; import { hoverTooltip } from '@codemirror/view';
@ -6,11 +6,13 @@ import { parseEntityReference, parseSyntacticReference } from '../../models/lang
import { IConstituenta } from '../../models/rsform'; import { IConstituenta } from '../../models/rsform';
import { domTooltipEntityReference, domTooltipSyntacticReference, findContainedNodes, findEnvelopingNodes } from '../../utils/codemirror'; import { domTooltipEntityReference, domTooltipSyntacticReference, findContainedNodes, findEnvelopingNodes } from '../../utils/codemirror';
import { IColorTheme } from '../../utils/color'; import { IColorTheme } from '../../utils/color';
import { ReferenceTokens } from './parse';
import { RefEntity, RefSyntactic } from './parse/parser.terms'; import { RefEntity, RefSyntactic } from './parse/parser.terms';
export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme) => { export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme) => {
return hoverTooltip((view, pos) => { return hoverTooltip(
const nodes = findEnvelopingNodes(pos, pos, syntaxTree(view.state), [RefEntity, RefSyntactic]); (view, pos) => {
const nodes = findEnvelopingNodes(pos, pos, syntaxTree(view.state), ReferenceTokens);
if (nodes.length !== 1) { if (nodes.length !== 1) {
return null; return null;
} }
@ -26,7 +28,7 @@ export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme)
above: false, above: false,
create: () => domTooltipEntityReference(ref, cst, colors) create: () => domTooltipEntityReference(ref, cst, colors)
} }
} else { } else if (nodes[0].type.id === RefSyntactic) {
const ref = parseSyntacticReference(text); const ref = parseSyntacticReference(text);
let masterText: string | undefined = undefined; let masterText: string | undefined = undefined;
if (ref.offset > 0) { if (ref.offset > 0) {
@ -49,6 +51,8 @@ export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme)
above: false, above: false,
create: () => domTooltipSyntacticReference(ref, masterText) create: () => domTooltipSyntacticReference(ref, masterText)
} }
} else {
return null;
} }
}); });
} }

View File

@ -158,15 +158,18 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
text: inputText text: inputText
} }
textProcessor.generateLexeme(data, response => { textProcessor.generateLexeme(data, response => {
const newForms: IWordForm[] = response.items.map( const lexeme: IWordForm[] = [];
form => ({ response.items.forEach(
text: form.text, form => {
grams: parseGrammemes(form.grams).filter(gram => SelectorGrammemesList.find(item => item === gram as Grammeme)) const newForm: IWordForm = {
})); text: form.text,
setForms(forms => [ grams: parseGrammemes(form.grams).filter(gram => SelectorGrammemesList.find(item => item === gram as Grammeme))
...newForms, }
...forms.filter(value => !newForms.find(test => matchWordForm(value, test))), if (newForm.grams.length === 2 && !lexeme.some(test => matchWordForm(test, newForm))) {
]); lexeme.push(newForm);
}
});
setForms(lexeme);
}); });
} }

View File

@ -4,7 +4,7 @@ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import Button from '../../components/Common/Button'; import Button from '../../components/Common/Button';
import { ConceptLoader } from '../../components/Common/ConceptLoader'; import { ConceptLoader } from '../../components/Common/ConceptLoader';
import RSInput from '../../components/RSInput'; import RSInput from '../../components/RSInput';
import { TextWrapper } from '../../components/RSInput/textEditing'; import { RSTextWrapper } from '../../components/RSInput/textEditing';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import useCheckExpression from '../../hooks/useCheckExpression'; import useCheckExpression from '../../hooks/useCheckExpression';
import { IConstituenta } from '../../models/rsform'; import { IConstituenta } from '../../models/rsform';
@ -97,7 +97,7 @@ function EditorRSExpression({
if (!rsInput.current || !rsInput.current.editor || !rsInput.current.state || !rsInput.current.view) { if (!rsInput.current || !rsInput.current.editor || !rsInput.current.state || !rsInput.current.view) {
return; return;
} }
const text = new TextWrapper(rsInput.current as Required<ReactCodeMirrorRef>); const text = new RSTextWrapper(rsInput.current as Required<ReactCodeMirrorRef>);
if (id === TokenID.ID_LOCAL) { if (id === TokenID.ID_LOCAL) {
text.insertChar(key ?? 'unknown_local'); text.insertChar(key ?? 'unknown_local');
} else { } else {

View File

@ -1,4 +1,6 @@
import { syntaxTree } from '@codemirror/language'
import { NodeType, Tree, TreeCursor } from '@lezer/common' import { NodeType, Tree, TreeCursor } from '@lezer/common'
import { ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror'
import { IEntityReference, ISyntacticReference, parseGrammemes } from '../models/language' import { IEntityReference, ISyntacticReference, parseGrammemes } from '../models/language'
import { IConstituenta } from '../models/rsform' import { IConstituenta } from '../models/rsform'
@ -225,3 +227,76 @@ export function domTooltipSyntacticReference(ref: ISyntacticReference, masterRef
return { dom: dom }; return { dom: dom };
} }
/**
* Wrapper class for CodeMirror editor.
*
* Assumes single range selection.
*/
export class CodeMirrorWrapper {
ref: Required<ReactCodeMirrorRef>;
constructor(object: Required<ReactCodeMirrorRef>) {
this.ref = object;
}
getSelection(): SelectionRange {
return this.ref.view.state.selection.main;
}
setSelection(from: number, to: number) {
this.ref.view.dispatch({
selection: {
anchor: from,
head: to
}
});
}
replaceWith(data: string) {
this.ref.view.dispatch(this.ref.view.state.replaceSelection(data));
}
envelopeWith(left: string, right: string) {
const selection = this.getSelection();
const newSelection = !selection.empty ? {
anchor: selection.from,
head: selection.to + left.length + right.length
} : {
anchor: selection.to + left.length + right.length - 1,
};
this.ref.view.dispatch({
changes: [
{ from: selection.from, insert: left },
{ from: selection.to, insert: right }
],
selection: newSelection
});
}
insertChar(key: string) {
this.replaceWith(key);
}
/**
* Enlarges selection to nearest spaces.
*
* If tokenFilter is provided then minimal valid token is selected.
*/
fixSelection(tokenFilter?: number[]) {
const selection = this.getSelection();
if (tokenFilter) {
const nodes = findEnvelopingNodes(selection.from, selection.to, syntaxTree(this.ref.view.state), tokenFilter);
if (nodes.length > 0) {
const target = nodes[nodes.length - 1];
this.setSelection(target.from, target.to);
return;
}
}
const startWord = this.ref.view.state.wordAt(selection.from);
const endWord = this.ref.view.state.wordAt(selection.to);
if (startWord || endWord) {
this.setSelection(startWord?.from ?? selection.from, endWord?.to ?? selection.to);
}
}
}

View File

@ -65,22 +65,9 @@ export function compareGrammemeOptions(left: IGrammemeOption, right: IGrammemeOp
* Represents list of {@link Grammeme}s available in reference construction. * Represents list of {@link Grammeme}s available in reference construction.
*/ */
export const SelectorGrammemesList = [ export const SelectorGrammemesList = [
Grammeme.NOUN, Grammeme.VERB,
Grammeme.sing, Grammeme.plur, Grammeme.sing, Grammeme.plur,
Grammeme.nomn, Grammeme.gent, Grammeme.datv, Grammeme.nomn, Grammeme.gent, Grammeme.datv,
Grammeme.accs, Grammeme.ablt, Grammeme.loct, Grammeme.accs, Grammeme.ablt, Grammeme.loct,
Grammeme.INFN, Grammeme.ADJF, Grammeme.PRTF,
Grammeme.ADJS, Grammeme.PRTS,
Grammeme.perf, Grammeme.impf,
Grammeme.tran, Grammeme.intr,
Grammeme.pres, Grammeme.past, Grammeme.futr,
Grammeme.per1, Grammeme.per2, Grammeme.per3,
Grammeme.impr, Grammeme.indc,
Grammeme.incl, Grammeme.excl,
Grammeme.pssv, Grammeme.actv,
]; ];
/** /**