diff --git a/rsconcept/frontend/src/components/RSInput/index.tsx b/rsconcept/frontend/src/components/RSInput/index.tsx index 7b427eff..7bf6e8f7 100644 --- a/rsconcept/frontend/src/components/RSInput/index.tsx +++ b/rsconcept/frontend/src/components/RSInput/index.tsx @@ -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); + const text = new RSTextWrapper(thisRef.current as Required); if (event.shiftKey && event.key === '*' && !event.altKey) { text.insertToken(TokenID.DECART); } else if (event.altKey) { diff --git a/rsconcept/frontend/src/components/RSInput/textEditing.ts b/rsconcept/frontend/src/components/RSInput/textEditing.ts index bb8ce496..b75229e1 100644 --- a/rsconcept/frontend/src/components/RSInput/textEditing.ts +++ b/rsconcept/frontend/src/components/RSInput/textEditing.ts @@ -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 - +/** + * Wrapper class for RSLang editor. +*/ +export class RSTextWrapper extends CodeMirrorWrapper { constructor(object: Required) { - 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 { diff --git a/rsconcept/frontend/src/components/RefsInput/index.tsx b/rsconcept/frontend/src/components/RefsInput/index.tsx index f9f6ab12..dca073bb 100644 --- a/rsconcept/frontend/src/components/RefsInput/index.tsx +++ b/rsconcept/frontend/src/components/RefsInput/index.tsx @@ -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) => { - 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); + 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} /> diff --git a/rsconcept/frontend/src/components/RefsInput/parse/index.ts b/rsconcept/frontend/src/components/RefsInput/parse/index.ts index 14ead267..47ee5022 100644 --- a/rsconcept/frontend/src/components/RefsInput/parse/index.ts +++ b/rsconcept/frontend/src/components/RefsInput/parse/index.ts @@ -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, diff --git a/rsconcept/frontend/src/components/RefsInput/tooltip.ts b/rsconcept/frontend/src/components/RefsInput/tooltip.ts index d2219e38..caafcadf 100644 --- a/rsconcept/frontend/src/components/RefsInput/tooltip.ts +++ b/rsconcept/frontend/src/components/RefsInput/tooltip.ts @@ -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; } }); } diff --git a/rsconcept/frontend/src/pages/RSFormPage/DlgEditTerm.tsx b/rsconcept/frontend/src/pages/RSFormPage/DlgEditTerm.tsx index 0ca36ace..74d35bf3 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/DlgEditTerm.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/DlgEditTerm.tsx @@ -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); }); } diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorRSExpression.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorRSExpression.tsx index 0578585b..d4640cd3 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorRSExpression.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorRSExpression.tsx @@ -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); + const text = new RSTextWrapper(rsInput.current as Required); if (id === TokenID.ID_LOCAL) { text.insertChar(key ?? 'unknown_local'); } else { diff --git a/rsconcept/frontend/src/utils/codemirror.ts b/rsconcept/frontend/src/utils/codemirror.ts index 5f455cf7..67fd455f 100644 --- a/rsconcept/frontend/src/utils/codemirror.ts +++ b/rsconcept/frontend/src/utils/codemirror.ts @@ -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' @@ -224,4 +226,77 @@ export function domTooltipSyntacticReference(ref: ISyntacticReference, masterRef dom.appendChild(nominal); return { dom: dom }; -} \ No newline at end of file +} + +/** + * Wrapper class for CodeMirror editor. + * + * Assumes single range selection. +*/ +export class CodeMirrorWrapper { + ref: Required; + + constructor(object: Required) { + 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); + } + } +} diff --git a/rsconcept/frontend/src/utils/selectors.ts b/rsconcept/frontend/src/utils/selectors.ts index daedce1f..76d39ea2 100644 --- a/rsconcept/frontend/src/utils/selectors.ts +++ b/rsconcept/frontend/src/utils/selectors.ts @@ -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, ]; /**