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