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 { ccBracketMatching } from './bracketMatching';
import { RSLanguage } from './rslang';
import { getSymbolSubstitute,TextWrapper } from './textEditing';
import { getSymbolSubstitute,RSTextWrapper } from './textEditing';
import { rsHoverTooltip } from './tooltip';
const editorSetup: BasicSetupOptions = {
@ -99,7 +99,7 @@ function RSInput({
if (!thisRef.current) {
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) {
text.insertToken(TokenID.DECART);
} else if (event.altKey) {

View File

@ -3,6 +3,7 @@
import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { TokenID } from '../../models/rslang';
import { CodeMirrorWrapper } from '../../utils/codemirror';
export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): string | undefined {
if (shiftPressed) {
@ -41,43 +42,17 @@ export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): str
return undefined;
}
// Note: Wrapper class for textareafield.
// WARNING! Manipulations on value do not support UNDO browser
// WARNING! No checks for selection out of text boundaries
export class TextWrapper {
ref: Required<ReactCodeMirrorRef>
/**
* Wrapper class for RSLang editor.
*/
export class RSTextWrapper extends CodeMirrorWrapper {
constructor(object: Required<ReactCodeMirrorRef>) {
this.ref = 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);
super(object);
}
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) {
case TokenID.NT_DECLARATIVE_EXPR: {
if (hasSelection) {
@ -87,7 +62,7 @@ export class TextWrapper {
}
this.ref.view.dispatch({
selection: {
anchor: this.ref.view.state.selection.main.from + 2,
anchor: selection.from + 2,
}
});
return true;
@ -120,7 +95,7 @@ export class TextWrapper {
this.envelopeWith('(', ')');
this.ref.view.dispatch({
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;
@ -130,14 +105,14 @@ export class TextWrapper {
if (hasSelection) {
this.ref.view.dispatch({
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;
}
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) === '') {
this.envelopeWith('', '');
} else {

View File

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

View File

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

View File

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

View File

@ -158,15 +158,18 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
text: inputText
}
textProcessor.generateLexeme(data, response => {
const newForms: IWordForm[] = response.items.map(
form => ({
text: form.text,
grams: parseGrammemes(form.grams).filter(gram => SelectorGrammemesList.find(item => item === gram as Grammeme))
}));
setForms(forms => [
...newForms,
...forms.filter(value => !newForms.find(test => matchWordForm(value, test))),
]);
const lexeme: IWordForm[] = [];
response.items.forEach(
form => {
const newForm: IWordForm = {
text: form.text,
grams: parseGrammemes(form.grams).filter(gram => SelectorGrammemesList.find(item => item === gram as Grammeme))
}
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 { ConceptLoader } from '../../components/Common/ConceptLoader';
import RSInput from '../../components/RSInput';
import { TextWrapper } from '../../components/RSInput/textEditing';
import { RSTextWrapper } from '../../components/RSInput/textEditing';
import { useRSForm } from '../../context/RSFormContext';
import useCheckExpression from '../../hooks/useCheckExpression';
import { IConstituenta } from '../../models/rsform';
@ -97,7 +97,7 @@ function EditorRSExpression({
if (!rsInput.current || !rsInput.current.editor || !rsInput.current.state || !rsInput.current.view) {
return;
}
const text = new TextWrapper(rsInput.current as Required<ReactCodeMirrorRef>);
const text = new RSTextWrapper(rsInput.current as Required<ReactCodeMirrorRef>);
if (id === TokenID.ID_LOCAL) {
text.insertChar(key ?? 'unknown_local');
} else {

View File

@ -1,4 +1,6 @@
import { syntaxTree } from '@codemirror/language'
import { NodeType, Tree, TreeCursor } from '@lezer/common'
import { ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror'
import { IEntityReference, ISyntacticReference, parseGrammemes } from '../models/language'
import { IConstituenta } from '../models/rsform'
@ -225,3 +227,76 @@ export function domTooltipSyntacticReference(ref: ISyntacticReference, masterRef
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.
*/
export const SelectorGrammemesList = [
Grammeme.NOUN, Grammeme.VERB,
Grammeme.sing, Grammeme.plur,
Grammeme.nomn, Grammeme.gent, Grammeme.datv,
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,
];
/**