mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-08-15 05:10:36 +03:00
Refactor CodeMirror wrappers and simplify available grams
This commit is contained in:
parent
1054db3a8a
commit
a8ad142544
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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'
|
||||||
|
@ -224,4 +226,77 @@ export function domTooltipSyntacticReference(ref: ISyntacticReference, masterRef
|
||||||
dom.appendChild(nominal);
|
dom.appendChild(nominal);
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue
Block a user