editor/editor-runtime/source/jetbrains/mps/nodeEditor/IntelligentInputUtil.java (431 lines of code) (raw):

/* * Copyright 2003-2024 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.mps.nodeEditor; import jetbrains.mps.core.aspects.behaviour.api.SMethod; import jetbrains.mps.editor.runtime.SideTransformInfoUtil; import jetbrains.mps.editor.runtime.commands.EditorComputable; import jetbrains.mps.logging.Logger; import jetbrains.mps.nodeEditor.cellActions.SideTransformSubstituteInfo; import jetbrains.mps.nodeEditor.cellActions.SideTransformSubstituteInfo.Side; import jetbrains.mps.nodeEditor.cellMenu.NodeSubstituteInfoFilterDecorator; import jetbrains.mps.nodeEditor.cellMenu.NullSubstituteInfo; import jetbrains.mps.nodeEditor.cells.CellFinderUtil; import jetbrains.mps.nodeEditor.cells.CellFinderUtil.Finder; import jetbrains.mps.nodeEditor.cells.EditorCell_Constant; import jetbrains.mps.nodeEditor.cells.EditorCell_Label; import jetbrains.mps.nodeEditor.sidetransform.EditorCell_STHint; import jetbrains.mps.openapi.editor.EditorContext; import jetbrains.mps.openapi.editor.cells.CellAction; import jetbrains.mps.openapi.editor.cells.CellActionType; import jetbrains.mps.openapi.editor.cells.CellInfo; import jetbrains.mps.openapi.editor.cells.CellTraversalUtil; import jetbrains.mps.openapi.editor.cells.EditorCell; import jetbrains.mps.openapi.editor.cells.SubstituteAction; import jetbrains.mps.openapi.editor.cells.SubstituteInfo; import jetbrains.mps.smodel.SNodeUtil; import jetbrains.mps.smodel.adapter.MetaAdapterByDeclaration; import jetbrains.mps.smodel.language.ConceptRegistry; import jetbrains.mps.typechecking.TypecheckingFacade; import org.jetbrains.mps.openapi.model.SNode; import java.util.List; public class IntelligentInputUtil { private static final Logger LOG = Logger.getLogger(IntelligentInputUtil.class); public static boolean processCell(final EditorCell_Label cell, final EditorContext editorContext, final String pattern, final CellSide side) { IntelligentCellProcessor intelligentCellProcessor = new IntelligentCellProcessor(cell, editorContext, side); return intelligentCellProcessor.processCell(pattern); } public static IntelligentCellProcessor getIntelligentCellProcessor(final EditorCell_Label cell, final EditorContext editorContext, final CellSide side) { return new IntelligentCellProcessor(cell, editorContext, side); } public static String trimLeft(String text) { for (int i = 0; i < text.length(); i++) { if (!Character.isWhitespace(text.charAt(i))) { return text.substring(i); } } return ""; } public static class IntelligentCellProcessor { private final EditorCell_Label myCell; private final EditorContext myEditorContext; private final CellSide mySide; private final SubstituteInfo mySubstituteInfo; private IntelligentCellProcessor(EditorCell_Label cell, EditorContext editorContext, CellSide side) { myCell = cell; myEditorContext = editorContext; mySide = side; mySubstituteInfo = createSubstituteInfo(cell.getSubstituteInfo()); } private SubstituteInfo createSubstituteInfo(final SubstituteInfo substituteInfo) { SubstituteInfo result; if (substituteInfo == null) { result = new NullSubstituteInfo(); } else { result = NodeSubstituteInfoFilterDecorator.createSubstituteInfoWithPatternMatchingFilter(substituteInfo, myEditorContext.getRepository()); } return result; } public boolean processCell(String pattern) { EditorComputable<Boolean> command = new EditorComputable<Boolean>(myEditorContext) { @Override protected Boolean doCompute() { // TODO: no idea what the default value should be here, no docs whatsoever if (((EditorComponent) myEditorContext.getEditorComponent()).getTypecheckingSession() == null) return false; return TypecheckingFacade .getFromContext() .computeWithSession(((EditorComponent) myEditorContext.getEditorComponent()).getTypecheckingSession(), (session) -> { if (myCell instanceof EditorCell_STHint) { return processSTHintCell(pattern); } if (mySide == CellSide.LEFT) { String head = "" + pattern.charAt(0); String smallPattern = pattern.substring(1); return processCellAtStart(head, smallPattern); } else { String smallPattern = pattern.substring(0, pattern.length() - 1); String tail = pattern.substring(pattern.length() - 1); return processCellAtEnd(smallPattern, tail); } }); } }; myEditorContext.getRepository().getModelAccess().executeCommand(command); return command.getResult(); } private boolean processSTHintCell(String pattern) { EditorCell_STHint stHintCell = ((EditorCell_STHint) myCell); String smallPattern = pattern.substring(0, pattern.length() - 1); String tail = "" + pattern.charAt(pattern.length() - 1); EditorCell nextCell = CellTraversalUtil.getNextLeaf(stHintCell); while (nextCell != null && !nextCell.isSelectable()) { nextCell = CellTraversalUtil.getNextLeaf(nextCell); } if (canCompleteSmallPatternImmediately(mySubstituteInfo, pattern, "") || canCompleteSmallPatternImmediately(mySubstituteInfo, trimLeft(pattern), "")) { String trimmedPattern = pattern; if (!canCompleteSmallPatternImmediately(mySubstituteInfo, pattern, "")) { trimmedPattern = trimLeft(pattern); } mySubstituteInfo.getMatchingActions(trimmedPattern, true).get(0).substitute(myEditorContext, pattern); return true; } else if (pattern.length() > 0 && (canCompleteSmallPatternImmediately(mySubstituteInfo, smallPattern, tail) || canCompleteSmallPatternImmediately(mySubstituteInfo, trimLeft(smallPattern), tail))) { if (!canCompleteSmallPatternImmediately(mySubstituteInfo, smallPattern, tail)) { smallPattern = trimLeft(smallPattern); } List<SubstituteAction> matchingActions = mySubstituteInfo.getMatchingActions(smallPattern, true); SubstituteAction item = matchingActions.get(0); SNode newNode = item.substitute(myEditorContext, smallPattern); if (newNode == null) { newNode = myEditorContext.getSelectedNode(); } myEditorContext.flushEvents(); EditorCell cellForNewNode; cellForNewNode = myEditorContext.getEditorComponent().findNodeCell(newNode); if (cellForNewNode != null) { EditorCell_Label target = null; EditorCell errorCell = CellFinderUtil.findChildByManyFinders(cellForNewNode, true, Finder.FIRST_ERROR); if (errorCell instanceof EditorCell_Label) { target = (EditorCell_Label) errorCell; } if (target != null && !tail.isBlank()) { target.changeText(tail); target.end(); if (target.isErrorState()) { target.validate(true, false); } myEditorContext.flushEvents(); if (myEditorContext.getSelectedCell() instanceof EditorCell_Label) { EditorCell_Label label = (EditorCell_Label) myEditorContext.getSelectedCell(); label.end(); } } } return true; } else if (mySubstituteInfo.getMatchingActions(pattern, false).isEmpty() && mySubstituteInfo.getMatchingActions(trimLeft(pattern), false).isEmpty() && nextCell != null && nextCell.isErrorState() && nextCell instanceof EditorCell_Label && ((EditorCell_Label) nextCell).isEditable()) { SideTransformInfoUtil.removeTransformInfo(stHintCell.getSNode()); EditorCell_Label label = (EditorCell_Label) nextCell; label.changeText(pattern); label.end(); myEditorContext.getEditorComponent().changeSelection(label); return true; } else if (isInOneStepAmbigousPosition(mySubstituteInfo, smallPattern + tail)) { activateNodeSubstituteChooser(myEditorContext, stHintCell); } return false; } private boolean processCellAtEnd(String smallPattern, final String tail) { EditorCell cellForNewNode; final SNode newNode; if (myCell.isValidText(smallPattern) && smallPattern != null && !smallPattern.isEmpty() && mySubstituteInfo.hasExactlyNActions(smallPattern + tail, false, 0)) { newNode = myCell.getSNode(); cellForNewNode = myCell; return applyRightTransform(smallPattern, tail, cellForNewNode, newNode); } else if (canCompleteSmallPatternImmediately(mySubstituteInfo, smallPattern, tail) || canCompleteSmallPatternImmediately(mySubstituteInfo, trimLeft(smallPattern), tail)) { if (!canCompleteSmallPatternImmediately(mySubstituteInfo, smallPattern, tail) && canCompleteSmallPatternImmediately(mySubstituteInfo, trimLeft(smallPattern), tail)) { smallPattern = trimLeft(smallPattern); } List<SubstituteAction> matchingActions = mySubstituteInfo.getMatchingActions(smallPattern, true); SubstituteAction item = matchingActions.get(0); item.substitute(myEditorContext, smallPattern); newNode = myEditorContext.getSelectedCell().getSNode(); if (newNode == null) { return true; } cellForNewNode = myEditorContext.getEditorComponent().findNodeCell(newNode); EditorCell_Label errorCell = CellFinderUtil.findFirstError(cellForNewNode, true); if (errorCell != null) { myEditorContext.flushEvents(); EditorCell cellForNewNode1 = myEditorContext.getEditorComponent().findNodeCell(newNode); EditorCell_Label errorCell1 = CellFinderUtil.findFirstError(cellForNewNode1, true); errorCell1.changeText(tail); errorCell1.setCaretPosition(tail.length()); return true; } return applyRightTransform(smallPattern, tail, cellForNewNode, newNode); } else if (canCompleteTheWholeStringImmediately(mySubstituteInfo, smallPattern + tail) || canCompleteTheWholeStringImmediately(mySubstituteInfo, trimLeft(smallPattern) + tail)) { if (!canCompleteTheWholeStringImmediately(mySubstituteInfo, smallPattern + tail) && canCompleteTheWholeStringImmediately(mySubstituteInfo, trimLeft(smallPattern) + tail)) { smallPattern = trimLeft(smallPattern); } List<SubstituteAction> matchingActions = mySubstituteInfo.getMatchingActions(smallPattern + tail, true); SubstituteAction item = matchingActions.get(0); item.substitute(myEditorContext, smallPattern + tail); return true; } else { String text = smallPattern + tail; if (isInOneStepAmbigousPosition(mySubstituteInfo, text)) { if (tryToSubstituteFirstSuitable(text, mySubstituteInfo)) { return true; } myCell.setText(text); myCell.setCaretPosition(text.length()); activateNodeSubstituteChooser(myEditorContext, myCell); return true; } else if (isInAmbigousPosition(mySubstituteInfo, smallPattern, tail)) { if (tryToSubstituteFirstSuitable(smallPattern, mySubstituteInfo)) { return true; } activateNodeSubstituteChooser(myEditorContext, myCell); return true; } } return false; } private boolean applyRightTransform(String smallPattern, final String tail, final EditorCell cellForNewNode, SNode newNode) { EditorCell selectableLeaf = CellFinderUtil.findLastSelectableLeaf(cellForNewNode, true); CellAction rtAction = selectableLeaf != null ? myEditorContext.getEditorComponent().getActionHandler().getApplicableCellAction(selectableLeaf, CellActionType.RIGHT_TRANSFORM) : null; boolean hasSideActions = hasSideActions(cellForNewNode, Side.RIGHT, tail); if (rtAction == null || !hasSideActions) { final CellInfo cellInfo = cellForNewNode.getCellInfo(); putTextInErrorChild(cellInfo, selectableLeaf, smallPattern + tail, myEditorContext); return false; } if (cellForNewNode instanceof EditorCell_Label) { ((EditorCell_Label) cellForNewNode).changeText(smallPattern); } myEditorContext.getEditorComponent().getActionHandler().executeAction(selectableLeaf, CellActionType.RIGHT_TRANSFORM); EditorCell rtHintCell = prepareSTCell(myEditorContext, newNode, tail); if (rtHintCell != null) { final SubstituteInfo rtSubstituteInfo = createSubstituteInfo(rtHintCell.getSubstituteInfo()); List<SubstituteAction> rtMatchingActions = rtSubstituteInfo.getMatchingActions(tail, true); if (!canCompleteSmallPatternImmediately(rtSubstituteInfo, tail, "")) { //don't execute non-unique action on RT hint cell myEditorContext.flushEvents(); EditorCell_Label foundCell = prepareRTCell(myEditorContext, newNode, tail); if (foundCell != null) { myEditorContext.getEditorComponent().changeSelection(foundCell); IntelligentInputUtil.processCell(foundCell, myEditorContext, tail, CellSide.RIGHT); } return true; } SubstituteAction rtItem = rtMatchingActions.get(0); SNode yetNewNode = rtItem.substitute(myEditorContext, tail); myEditorContext.flushEvents(); if (yetNewNode != null) { EditorCell yetNewNodeCell = findNodeCell(myEditorContext, yetNewNode); if (yetNewNodeCell == null) { LOG.warning( "Unable to find editor cell for the node returned as a result of right-transform: " + yetNewNode.toString() + "(" + yetNewNode.getConcept() + "). Seems like the node is invisible in editor. Node was created by RT: " + rtItem.toString()); return true; } EditorCell errorCell = CellFinderUtil.findFirstError(yetNewNodeCell, true); if (errorCell != null) { myEditorContext.selectWRTFocusPolicy(errorCell); } else { myEditorContext.selectWRTFocusPolicy(yetNewNodeCell); } } } else { myEditorContext.flushEvents(); EditorCell_Label rtCell = prepareRTCell(myEditorContext, newNode, tail); if (rtCell != null) { IntelligentInputUtil.processCell(rtCell, myEditorContext, tail, CellSide.RIGHT); } } return true; } private boolean tryToSubstituteFirstSuitable(String text, SubstituteInfo substituteInfo) { SNode concept = substituteInfo.getMatchingActions(text, true).get(0).getOutputConcept(); if (concept == null) { return false; } // substituteInAmbigousPosition() is a behavior method declared in BaseConcept // No idea why it's handwritten code boolean property = false; // FIXME need a mechanism like myEditorContext.getComponentHost().findComponent(CR.class), just need to figure out how to pass CH into the editor. // EditorConfigurationBuilder, perhaps? // Then, need to decide whether BehaviorRegistry is CoreComponent or not. Perhaps, centralized mediator like ConceptRegistry is not that bad, after all. //noinspection removal for (SMethod<?> dm : ConceptRegistry.getInstance() .getBehaviorRegistry() .getBHDescriptor(SNodeUtil.concept_BaseConcept) .getDeclaredMethods()) { if ("substituteInAmbigousPosition".equals(dm.getName()) && !dm.isPrivate() && !dm.isAbstract()) { // we assume only 1 method with this name property = (Boolean) dm.invoke(MetaAdapterByDeclaration.getConcept(concept)); break; } } if (property) { SNode outputConcept = substituteInfo.getMatchingActions(text, true).get(0).getOutputConcept(); for (SubstituteAction action : substituteInfo.getMatchingActions(text, true)) { if (outputConcept != action.getOutputConcept()) { return false; } } SubstituteAction action = substituteInfo.getMatchingActions(text, true).get(0); action.substitute(myEditorContext, text); return true; } return false; } private boolean processCellAtStart(String head, String smallPattern) { EditorCell cellForNewNode; SNode newNode; if (myCell.isValidText(smallPattern) && (smallPattern != null && !smallPattern.isEmpty() || myCell instanceof EditorCell_Constant) && mySubstituteInfo.hasExactlyNActions(head + smallPattern, false, 0)) { newNode = myCell.getSNode(); cellForNewNode = myCell; return applyLeftTransform(head, smallPattern, cellForNewNode, newNode, true); } else if (canCompleteSmallPatternImmediatelyLeft(mySubstituteInfo, head, smallPattern) && !canCompleteTheWholeStringImmediately(mySubstituteInfo, head + smallPattern)) { final SubstituteAction substituteAction = mySubstituteInfo.getMatchingActions(smallPattern, true).get(0); newNode = substituteAction.substitute(myEditorContext, smallPattern); if (newNode == null) { newNode = myEditorContext.getSelectedNode(); } if (newNode == null) { return true; } cellForNewNode = findNodeCell(myEditorContext, newNode); return applyLeftTransform(head, smallPattern, cellForNewNode, newNode, false); } else if (canCompleteTheWholeStringImmediately(mySubstituteInfo, head + smallPattern)) { List<SubstituteAction> matchingActions = mySubstituteInfo.getMatchingActions(head + smallPattern, true); SubstituteAction item = matchingActions.get(0); item.substitute(myEditorContext, head + smallPattern); return true; } return false; } private boolean applyLeftTransform(final String head, String smallPattern, final EditorCell cellForNewNode, SNode newNode, boolean sourceCellRemains) { EditorCell firstSelectableLeaf = CellFinderUtil.findFirstSelectableLeaf(cellForNewNode, true); CellAction ltAction = myEditorContext.getEditorComponent().getActionHandler().getApplicableCellAction(firstSelectableLeaf, CellActionType.LEFT_TRANSFORM); boolean hasSideActions = hasSideActions(cellForNewNode, Side.LEFT, head); if (ltAction == null || !hasSideActions) { CellInfo cellInfo = cellForNewNode.getCellInfo(); if (!sourceCellRemains) { putTextInErrorChild(cellInfo, firstSelectableLeaf, head + smallPattern, myEditorContext); return false; } else { return false; } } if (sourceCellRemains) { ((EditorCell_Label) cellForNewNode).changeText(smallPattern); } myEditorContext.getEditorComponent().getActionHandler().executeAction(firstSelectableLeaf, CellActionType.LEFT_TRANSFORM); final EditorCell_Label ltCell = prepareSTCell(myEditorContext, newNode, head); if (ltCell instanceof EditorCell_STHint) { SubstituteInfo substituteInfo = createSubstituteInfo(ltCell.getSubstituteInfo()); if (canCompleteSmallPatternImmediately(substituteInfo, head, "")) { substituteInfo.getMatchingActions(head, true).get(0).substitute(myEditorContext, head); } } return true; } private boolean canCompleteSmallPatternImmediatelyLeft(SubstituteInfo info, String head, String smallPattern) { return info.hasExactlyNActions(smallPattern, true, 1) && info.hasExactlyNActions(head + smallPattern, false, 0); } private boolean canCompleteSmallPatternImmediately(SubstituteInfo info, String smallPattern, String tail) { if (tail != null && tail.isEmpty()) { return info.hasExactlyNActions(smallPattern, true, 1) && info.hasExactlyNActions(smallPattern, false, 1); } // * has special meaning in completion patterns but we often want to complete multiplication // operations return info.hasExactlyNActions(smallPattern, true, 1) && (info.hasExactlyNActions(smallPattern + tail, false, 0)); } private boolean canCompleteTheWholeStringImmediately(SubstituteInfo info, String pattern) { return info.hasExactlyNActions(pattern, true, 1) && (info.hasExactlyNActions(pattern, false, 1) || info.hasExactlyNActions(pattern, false, 0)); } private boolean isInAmbigousPosition(SubstituteInfo info, String smallPattern, String tail) { return info.getMatchingActions(smallPattern, true).size() > 1 && info.getMatchingActions(smallPattern + tail, false).isEmpty(); } private boolean isInOneStepAmbigousPosition(SubstituteInfo info, String smallPattern) { return info.getMatchingActions(smallPattern, true).size() > 1 && info.getMatchingActions(smallPattern, true).size() == info.getMatchingActions(smallPattern, false).size(); } private EditorCell_Label prepareSTCell(EditorContext context, SNode transformingNode, String textToSet) { EditorCell_Label rtCell = EditorCell_STHint.getSTHintCell(transformingNode, context.getEditorComponent()); if (rtCell == null) { EditorCell selectedCell = context.getSelectedCell(); if (selectedCell instanceof EditorCell_Label && selectedCell.isErrorState()) { rtCell = (EditorCell_Label) selectedCell; } else { return null; } } rtCell.changeText(textToSet); rtCell.end(); return rtCell; } private EditorCell_Label prepareRTCell(EditorContext context, SNode node, String textToSet) { EditorCell root = findNodeCell(context, node); if (root == null) { return null; } return prepareSTCell(context, node, textToSet); } private void putTextInErrorChild(CellInfo cellInfo, EditorCell newCell, String textToSet, EditorContext editorContext) { editorContext.flushEvents(); EditorComponent component = (EditorComponent) editorContext.getEditorComponent(); EditorCell cellToSelect = cellInfo.findCell(component); if (cellToSelect != null) { EditorCell_Label label = CellFinderUtil.findFirstError(cellToSelect, true); if (label != null && label != cellToSelect && label.isEditable() && !(label instanceof EditorCell_Constant)) { label.changeText(textToSet); label.end(); return; } } if (newCell != cellToSelect && newCell instanceof EditorCell_Label && !(newCell instanceof EditorCell_Constant)) { EditorCell_Label label = (EditorCell_Label) newCell; label.changeText(textToSet); label.end(); } } private boolean hasSideActions(EditorCell cell, Side side, String prefix) { SubstituteInfo info = createSubstituteInfo(new SideTransformSubstituteInfo(cell, side)); return !info.hasExactlyNActions(prefix, false, 0); } private void activateNodeSubstituteChooser(EditorContext editorContext, EditorCell cell) { ((EditorComponent) editorContext.getEditorComponent()).activateNodeSubstituteChooser(cell, false); } private EditorCell findNodeCell(EditorContext editorContext, SNode newNode) { return editorContext.getEditorComponent().findNodeCell(newNode); } } }