Implement click navigation for RSInput

This commit is contained in:
IRBorisov 2024-06-18 19:55:23 +03:00
parent 867e60581b
commit 568886d1b7
10 changed files with 94 additions and 38 deletions

View File

@ -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

View File

@ -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<ReactCodeMirrorRef, RSInputProps>(
@ -49,6 +52,9 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
disabled,
noTooltip,
schema,
onOpenEdit,
className,
style,
@ -59,7 +65,6 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
ref
) => {
const { darkMode, colors, mathFont } = useConceptOptions();
const { schema } = useRSForm();
const internalRef = useRef<ReactCodeMirrorRef>(null);
const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]);
@ -77,7 +82,7 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
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<ReactCodeMirrorRef, RSInputProps>(
{ 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<ReactCodeMirrorRef, RSInputProps>(
EditorView.lineWrapping,
RSLanguage,
ccBracketMatching(darkMode),
...(!schema || !onOpenEdit ? [] : [rsNavigation(schema, onOpenEdit)]),
...(noTooltip || !schema ? [] : [rsHoverTooltip(schema)])
],
[darkMode, schema, noTooltip]
[darkMode, schema, noTooltip, onOpenEdit]
);
const handleInput = useCallback(

View File

@ -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)];
}

View File

@ -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);

View File

@ -95,6 +95,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
}
value={state.definition_formal}
onChange={value => partialUpdate({ definition_formal: value })}
schema={schema}
/>
</AnimateFade>
<AnimateFade key='dlg_cst_definition' hideContent={!state.definition_raw && isElementary}>

View File

@ -113,6 +113,7 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
setIsModified={setIsModified}
onEditTerm={controller.editTermForms}
onRename={controller.renameCst}
onOpenEdit={onOpenEdit}
/>
<AnimatePresence>
{showList ? (

View File

@ -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}
/>
</AnimateFade>
<AnimateFade key='cst_definition_fade' hideContent={!!state && !state?.definition_raw && isElementary}>

View File

@ -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}
/>

View File

@ -37,7 +37,7 @@ function ConstituentsTable({ items, activeCst, onOpenEdit, maxHeight, denseThres
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
block: 'center',
inline: 'end'
});
}

View File

@ -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 = `<b>${cst.alias}:</b> ${labelCstTypification(cst)}`;
dom.appendChild(alias);
@ -181,10 +205,11 @@ export function domTooltipConstituenta(cst?: IConstituenta) {
children.innerHTML = `<b>Порождает:</b> ${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 };
}