From 03b02ed613fb4bc43e5e696f7a981888bd2bf494 Mon Sep 17 00:00:00 2001 From: IRBorisov <8611739+IRBorisov@users.noreply.github.com> Date: Tue, 18 Jun 2024 19:53:56 +0300 Subject: [PATCH] Implement click navigation for RSInput --- TODO.txt | 2 - .../src/components/RSInput/RSInput.tsx | 16 +++++--- .../src/components/RSInput/clickNavigation.ts | 38 +++++++++++++++++++ .../src/components/RSInput/tooltip.ts | 22 +---------- .../dialogs/DlgCreateCst/FormCreateCst.tsx | 1 + .../EditorConstituenta/EditorConstituenta.tsx | 1 + .../EditorConstituenta/FormConstituenta.tsx | 7 +++- .../EditorRSExpression/EditorRSExpression.tsx | 6 ++- .../ViewConstituents/ConstituentsTable.tsx | 2 +- rsconcept/frontend/src/utils/codemirror.ts | 37 +++++++++++++++--- 10 files changed, 94 insertions(+), 38 deletions(-) create mode 100644 rsconcept/frontend/src/components/RSInput/clickNavigation.ts diff --git a/TODO.txt b/TODO.txt index 171a23fa..d5bb5628 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,8 +4,6 @@ For more specific TODOs see comments in code [Functionality - PROGRESS] - Operational synthesis schema as LibraryItem ? -- Clickable IDs in RSEditor tooltips - - Library organization, search and exploration. Consider new user experience - Private projects and permissions. Consider cooperative editing diff --git a/rsconcept/frontend/src/components/RSInput/RSInput.tsx b/rsconcept/frontend/src/components/RSInput/RSInput.tsx index d20f5d8d..edd650a5 100644 --- a/rsconcept/frontend/src/components/RSInput/RSInput.tsx +++ b/rsconcept/frontend/src/components/RSInput/RSInput.tsx @@ -10,12 +10,13 @@ import { forwardRef, useCallback, useMemo, useRef } from 'react'; import Label from '@/components/ui/Label'; import { useConceptOptions } from '@/context/OptionsContext'; -import { useRSForm } from '@/context/RSFormContext'; import { getFontClassName } from '@/models/miscellaneousAPI'; +import { ConstituentaID, IRSForm } from '@/models/rsform'; import { generateAlias, getCstTypePrefix, guessCstType } from '@/models/rsformAPI'; import { extractGlobals } from '@/models/rslangAPI'; import { ccBracketMatching } from './bracketMatching'; +import { rsNavigation } from './clickNavigation'; import { RSLanguage } from './rslang'; import { getSymbolSubstitute, RSTextWrapper } from './textEditing'; import { rsHoverTooltip } from './tooltip'; @@ -39,6 +40,8 @@ interface RSInputProps noTooltip?: boolean; onChange?: (newValue: string) => void; onAnalyze?: () => void; + schema?: IRSForm; + onOpenEdit?: (cstID: ConstituentaID) => void; } const RSInput = forwardRef( @@ -49,6 +52,9 @@ const RSInput = forwardRef( disabled, noTooltip, + schema, + onOpenEdit, + className, style, @@ -59,7 +65,6 @@ const RSInput = forwardRef( ref ) => { const { darkMode, colors, mathFont } = useConceptOptions(); - const { schema } = useRSForm(); const internalRef = useRef(null); const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]); @@ -77,7 +82,7 @@ const RSInput = forwardRef( caret: colors.fgDefault }, styles: [ - { tag: tags.name, color: colors.fgPurple, cursor: 'default' }, // GlobalID + { tag: tags.name, color: colors.fgPurple, cursor: schema ? 'default' : 'text' }, // GlobalID { tag: tags.variableName, color: colors.fgGreen }, // LocalID { tag: tags.propertyName, color: colors.fgTeal }, // Radical { tag: tags.keyword, color: colors.fgBlue }, // keywords @@ -87,7 +92,7 @@ const RSInput = forwardRef( { tag: tags.brace, color: colors.fgPurple, fontWeight: '600' } // braces (curly brackets) ] }), - [disabled, colors, darkMode] + [disabled, colors, darkMode, schema] ); const editorExtensions = useMemo( @@ -95,9 +100,10 @@ const RSInput = forwardRef( EditorView.lineWrapping, RSLanguage, ccBracketMatching(darkMode), + ...(!schema || !onOpenEdit ? [] : [rsNavigation(schema, onOpenEdit)]), ...(noTooltip || !schema ? [] : [rsHoverTooltip(schema)]) ], - [darkMode, schema, noTooltip] + [darkMode, schema, noTooltip, onOpenEdit] ); const handleInput = useCallback( diff --git a/rsconcept/frontend/src/components/RSInput/clickNavigation.ts b/rsconcept/frontend/src/components/RSInput/clickNavigation.ts new file mode 100644 index 00000000..2ee94a6b --- /dev/null +++ b/rsconcept/frontend/src/components/RSInput/clickNavigation.ts @@ -0,0 +1,38 @@ +import { Extension } from '@codemirror/state'; +import { EditorView } from '@uiw/react-codemirror'; + +import { ConstituentaID, IRSForm } from '@/models/rsform'; +import { findAliasAt } from '@/utils/codemirror'; + +const globalsNavigation = (schema: IRSForm, onOpenEdit: (cstID: ConstituentaID) => void) => { + return EditorView.domEventHandlers({ + click: (event: MouseEvent, view: EditorView) => { + if (!event.ctrlKey) { + return; + } + + const pos = view.posAtCoords({ x: event.clientX, y: event.clientY }); + if (!pos) { + return; + } + + const { alias } = findAliasAt(pos, view.state); + if (!alias) { + return; + } + + const cst = schema.cstByAlias.get(alias); + if (!cst) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + onOpenEdit(cst.id); + } + }); +}; + +export function rsNavigation(schema: IRSForm, onOpenEdit: (cstID: ConstituentaID) => void): Extension { + return [globalsNavigation(schema, onOpenEdit)]; +} diff --git a/rsconcept/frontend/src/components/RSInput/tooltip.ts b/rsconcept/frontend/src/components/RSInput/tooltip.ts index 341a6942..95239a53 100644 --- a/rsconcept/frontend/src/components/RSInput/tooltip.ts +++ b/rsconcept/frontend/src/components/RSInput/tooltip.ts @@ -1,30 +1,10 @@ -import { syntaxTree } from '@codemirror/language'; import { Extension } from '@codemirror/state'; import { hoverTooltip } from '@codemirror/view'; -import { EditorState } from '@uiw/react-codemirror'; import { IRSForm } from '@/models/rsform'; -import { findEnvelopingNodes } from '@/utils/codemirror'; +import { findAliasAt } from '@/utils/codemirror'; import { domTooltipConstituenta } from '@/utils/codemirror'; -import { GlobalTokens } from './rslang'; - -function findAliasAt(pos: number, state: EditorState) { - const { from: lineStart, to: lineEnd, text } = state.doc.lineAt(pos); - const nodes = findEnvelopingNodes(pos, pos, syntaxTree(state), GlobalTokens); - let alias = ''; - let start = 0; - let end = 0; - nodes.forEach(node => { - if (node.to <= lineEnd && node.from >= lineStart) { - alias = text.slice(node.from - lineStart, node.to - lineStart); - start = node.from; - end = node.to; - } - }); - return { alias, start, end }; -} - const globalsHoverTooltip = (schema: IRSForm) => { return hoverTooltip((view, pos) => { const { alias, start, end } = findAliasAt(pos, view.state); diff --git a/rsconcept/frontend/src/dialogs/DlgCreateCst/FormCreateCst.tsx b/rsconcept/frontend/src/dialogs/DlgCreateCst/FormCreateCst.tsx index c1cc724e..75d8e208 100644 --- a/rsconcept/frontend/src/dialogs/DlgCreateCst/FormCreateCst.tsx +++ b/rsconcept/frontend/src/dialogs/DlgCreateCst/FormCreateCst.tsx @@ -95,6 +95,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat } value={state.definition_formal} onChange={value => partialUpdate({ definition_formal: value })} + schema={schema} /> diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/EditorConstituenta.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/EditorConstituenta.tsx index 974e9828..f52321ab 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/EditorConstituenta.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/EditorConstituenta.tsx @@ -113,6 +113,7 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit } setIsModified={setIsModified} onEditTerm={controller.editTermForms} onRename={controller.renameCst} + onOpenEdit={onOpenEdit} /> {showList ? ( diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/FormConstituenta.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/FormConstituenta.tsx index 05b86374..211c5b3b 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/FormConstituenta.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/FormConstituenta.tsx @@ -11,7 +11,7 @@ import SubmitButton from '@/components/ui/SubmitButton'; import TextArea from '@/components/ui/TextArea'; import AnimateFade from '@/components/wrap/AnimateFade'; import { useRSForm } from '@/context/RSFormContext'; -import { CstType, IConstituenta, ICstUpdateData } from '@/models/rsform'; +import { ConstituentaID, CstType, IConstituenta, ICstUpdateData } from '@/models/rsform'; import { isBaseSet, isBasicConcept, isFunctional } from '@/models/rsformAPI'; import { information, labelCstTypification } from '@/utils/labels'; @@ -35,6 +35,7 @@ interface FormConstituentaProps { onRename: () => void; onEditTerm: () => void; + onOpenEdit?: (cstID: ConstituentaID) => void; } function FormConstituenta({ @@ -47,7 +48,8 @@ function FormConstituenta({ toggleReset, onRename, - onEditTerm + onEditTerm, + onOpenEdit }: FormConstituentaProps) { const { schema, cstUpdate, processing } = useRSForm(); @@ -183,6 +185,7 @@ function FormConstituenta({ toggleReset={toggleReset} onChange={newValue => setExpression(newValue)} setTypification={setTypification} + onOpenEdit={onOpenEdit} /> diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorRSExpression/EditorRSExpression.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorRSExpression/EditorRSExpression.tsx index 284ad11b..c61ac3f8 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorRSExpression/EditorRSExpression.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorRSExpression/EditorRSExpression.tsx @@ -14,7 +14,7 @@ import DlgShowAST from '@/dialogs/DlgShowAST'; import useCheckExpression from '@/hooks/useCheckExpression'; import useLocalStorage from '@/hooks/useLocalStorage'; import { HelpTopic } from '@/models/miscellaneous'; -import { IConstituenta } from '@/models/rsform'; +import { ConstituentaID, IConstituenta } from '@/models/rsform'; import { getDefinitionPrefix } from '@/models/rsformAPI'; import { IExpressionParse, IRSErrorDescription, SyntaxTree } from '@/models/rslang'; import { TokenID } from '@/models/rslang'; @@ -38,6 +38,7 @@ interface EditorRSExpressionProps { setTypification: (typification: string) => void; onChange: (newValue: string) => void; + onOpenEdit?: (cstID: ConstituentaID) => void; } function EditorRSExpression({ @@ -47,6 +48,7 @@ function EditorRSExpression({ toggleReset, setTypification, onChange, + onOpenEdit, ...restProps }: EditorRSExpressionProps) { const model = useRSForm(); @@ -185,6 +187,8 @@ function EditorRSExpression({ disabled={disabled} onChange={handleChange} onAnalyze={handleCheckExpression} + schema={model.schema} + onOpenEdit={onOpenEdit} {...restProps} /> diff --git a/rsconcept/frontend/src/pages/RSFormPage/ViewConstituents/ConstituentsTable.tsx b/rsconcept/frontend/src/pages/RSFormPage/ViewConstituents/ConstituentsTable.tsx index 6839cb72..606a6ce5 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/ViewConstituents/ConstituentsTable.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/ViewConstituents/ConstituentsTable.tsx @@ -37,7 +37,7 @@ function ConstituentsTable({ items, activeCst, onOpenEdit, maxHeight, denseThres if (element) { element.scrollIntoView({ behavior: 'smooth', - block: 'nearest', + block: 'center', inline: 'end' }); } diff --git a/rsconcept/frontend/src/utils/codemirror.ts b/rsconcept/frontend/src/utils/codemirror.ts index d1728b6f..aa989914 100644 --- a/rsconcept/frontend/src/utils/codemirror.ts +++ b/rsconcept/frontend/src/utils/codemirror.ts @@ -3,9 +3,10 @@ */ import { syntaxTree } from '@codemirror/language'; import { NodeType, Tree, TreeCursor } from '@lezer/common'; -import { ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror'; +import { EditorState, ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror'; import clsx from 'clsx'; +import { GlobalTokens } from '@/components/RSInput/rslang'; import { IEntityReference, ISyntacticReference } from '@/models/language'; import { parseGrammemes } from '@/models/languageAPI'; import { IConstituenta } from '@/models/rsform'; @@ -122,6 +123,25 @@ export function findContainedNodes(start: number, finish: number, tree: Tree, fi return result; } +/** + * Retrieves globalID from position in Editor. + */ +export function findAliasAt(pos: number, state: EditorState) { + const { from: lineStart, to: lineEnd, text } = state.doc.lineAt(pos); + const nodes = findEnvelopingNodes(pos, pos, syntaxTree(state), GlobalTokens); + let alias = ''; + let start = 0; + let end = 0; + nodes.forEach(node => { + if (node.to <= lineEnd && node.from >= lineStart) { + alias = text.slice(node.from - lineStart, node.to - lineStart); + start = node.from; + end = node.to; + } + }); + return { alias, start, end }; +} + /** * Create DOM tooltip for {@link Constituenta}. */ @@ -137,7 +157,11 @@ export function domTooltipConstituenta(cst?: IConstituenta) { 'text-sm font-main' ); - if (cst) { + if (!cst) { + const text = document.createElement('p'); + text.innerText = 'Конституента не определена'; + dom.appendChild(text); + } else { const alias = document.createElement('p'); alias.innerHTML = `${cst.alias}: ${labelCstTypification(cst)}`; dom.appendChild(alias); @@ -181,10 +205,11 @@ export function domTooltipConstituenta(cst?: IConstituenta) { children.innerHTML = `Порождает: ${cst.children_alias.join(', ')}`; dom.appendChild(children); } - } else { - const text = document.createElement('p'); - text.innerText = 'Конституента не определена'; - dom.appendChild(text); + + const clickTip = document.createElement('p'); + clickTip.className = 'w-full text-center text-xs mt-2'; + clickTip.innerText = 'Ctrl + клик для перехода'; + dom.appendChild(clickTip); } return { dom: dom }; }