src/org/intellij/grammar/refactor/BnfIntroduceTokenHandler.java (247 lines of code) (raw):
/*
* Copyright 2011-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
*/
package org.intellij.grammar.refactor;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.template.*;
import com.intellij.codeInsight.template.impl.TemplateManagerImpl;
import com.intellij.codeInsight.template.impl.TemplateState;
import com.intellij.codeInsight.template.impl.TextExpression;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.command.impl.FinishMarkAction;
import com.intellij.openapi.command.impl.StartMarkAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pass;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.refactoring.RefactoringActionHandler;
import com.intellij.refactoring.introduce.inplace.OccurrencesChooser;
import com.intellij.util.ExceptionUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.UniqueNameGenerator;
import org.intellij.grammar.KnownAttribute;
import org.intellij.grammar.generator.ParserGeneratorUtil;
import org.intellij.grammar.generator.RuleGraphHelper;
import org.intellij.grammar.psi.*;
import org.intellij.grammar.psi.impl.BnfElementFactory;
import org.intellij.grammar.psi.impl.GrammarUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Consumer;
/**
* @author greg
*/
public class BnfIntroduceTokenHandler implements RefactoringActionHandler {
public static final String REFACTORING_NAME = "Introduce Token";
@Override
public void invoke(@NotNull Project project, PsiElement @NotNull [] elements, DataContext dataContext) {
// do not support this case
}
@Override
public void invoke(@NotNull Project project,
Editor editor,
PsiFile file,
@Nullable DataContext dataContext) {
if (!(file instanceof BnfFile)) return;
BnfFile bnfFile = (BnfFile) file;
Map<String, String> tokenNameMap = RuleGraphHelper.getTokenNameToTextMap(bnfFile);
Map<String, String> tokenTextMap = RuleGraphHelper.getTokenTextToNameMap(bnfFile);
String tokenText;
String tokenName;
BnfExpression target = PsiTreeUtil.getParentOfType(file.findElementAt(editor.getCaretModel().getOffset()), BnfReferenceOrToken.class, BnfStringLiteralExpression.class);
if (target instanceof BnfReferenceOrToken) {
if (bnfFile.getRule(target.getText()) != null) return;
if (GrammarUtil.isExternalReference(target)) return;
tokenName = target.getText();
tokenText = "\"" + StringUtil.notNullize(tokenNameMap.get(tokenName), tokenName) + "\"";
}
else if (target instanceof BnfStringLiteralExpression) {
if (PsiTreeUtil.getParentOfType(target, BnfAttrs.class) != null) return;
tokenText = target.getText();
tokenName = tokenTextMap.get(GrammarUtil.unquote(tokenText));
}
else return;
List<BnfExpression> allOccurrences = new ArrayList<>();
Map<OccurrencesChooser.ReplaceChoice, List<BnfExpression>> occurrencesMap = new LinkedHashMap<>();
occurrencesMap.put(OccurrencesChooser.ReplaceChoice.NO, Collections.singletonList(target));
occurrencesMap.put(OccurrencesChooser.ReplaceChoice.ALL, allOccurrences);
BnfVisitor<Void> visitor = new BnfVisitor<>() {
@Override
public Void visitStringLiteralExpression(@NotNull BnfStringLiteralExpression o) {
if (Objects.equals(tokenText, o.getText())) {
allOccurrences.add(o);
}
return null;
}
@Override
public Void visitReferenceOrToken(@NotNull BnfReferenceOrToken o) {
if (GrammarUtil.isExternalReference(o)) return null;
if (tokenName != null && tokenName.equals(o.getText())) {
allOccurrences.add(o);
}
return null;
}
};
for (PsiElement o : GrammarUtil.bnfTraverserNoAttrs(file)) {
o.accept(visitor);
}
if (occurrencesMap.get(OccurrencesChooser.ReplaceChoice.ALL).size() <= 1 && !ApplicationManager.getApplication().isUnitTestMode()) {
occurrencesMap.remove(OccurrencesChooser.ReplaceChoice.ALL);
}
Consumer<OccurrencesChooser.ReplaceChoice> callback = new Pass<>() {
@Override
public void pass(OccurrencesChooser.ReplaceChoice choice) {
WriteCommandAction.writeCommandAction(project, file)
.withName(REFACTORING_NAME)
.run(() -> {
try {
buildTemplateAndRun(project, editor, bnfFile, occurrencesMap.get(choice), tokenName, tokenText, tokenNameMap.keySet());
}
catch (StartMarkAction.AlreadyStartedException e) {
ExceptionUtil.rethrowAllAsUnchecked(e);
}
});
}
};
if (ApplicationManager.getApplication().isUnitTestMode()) {
callback.accept(OccurrencesChooser.ReplaceChoice.ALL);
}
else {
new OccurrencesChooser<BnfExpression>(editor) {
@Override
protected TextRange getOccurrenceRange(BnfExpression occurrence) {
return occurrence.getTextRange();
}
}.showChooser(occurrencesMap, callback);
}
}
private static void buildTemplateAndRun(Project project,
Editor editor,
BnfFile bnfFile, List<BnfExpression> occurrences,
String tokenName,
String tokenText,
Set<String> tokenNames) throws StartMarkAction.AlreadyStartedException {
StartMarkAction startAction = StartMarkAction.start(editor, project, REFACTORING_NAME);
BnfListEntry entry = addTokenDefinition(project, bnfFile, tokenName, tokenText, tokenNames);
PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.getDocument());
TemplateBuilderImpl builder = new TemplateBuilderImpl(bnfFile);
PsiElement tokenId = Objects.requireNonNull(entry.getId());
PsiElement tokenValue = Objects.requireNonNull(entry.getLiteralExpression());
if (tokenName == null) {
builder.replaceElement(tokenId, "TokenName", new TextExpression(tokenId.getText()), true);
}
builder.replaceElement(tokenValue, "TokenText", new TextExpression(tokenValue.getText()), true);
for (BnfExpression occurrence : occurrences) {
builder.replaceElement(occurrence, "Other", new Expression() {
@Override
public @Nullable Result calculateResult(ExpressionContext context) {
TemplateState state = TemplateManagerImpl.getTemplateState(context.getEditor());
assert state != null;
TextResult text = Objects.requireNonNull(state.getVariableValue("TokenText"));
String curText = GrammarUtil.unquote(text.getText());
if (ParserGeneratorUtil.isRegexpToken(curText)) {
return state.getVariableValue("TokenName");
}
else {
return new TextResult("'" + curText + "'");
}
}
@Override
public @Nullable Result calculateQuickResult(ExpressionContext context) {
return calculateResult(context);
}
@Override
public LookupElement[] calculateLookupItems(ExpressionContext context) {
return LookupElement.EMPTY_ARRAY;
}
}, false);
}
RangeMarker caretMarker = editor.getDocument().createRangeMarker(0, editor.getCaretModel().getOffset());
caretMarker.setGreedyToRight(true);
editor.getCaretModel().moveToOffset(0);
Template template = builder.buildInlineTemplate();
template.setToShortenLongNames(false);
template.setToReformat(false);
TemplateManager.getInstance(project).startTemplate(editor, template, new TemplateEditingAdapter() {
@Override
public void templateFinished(@NotNull Template template, boolean brokenOff) {
handleTemplateFinished(project, editor, caretMarker, startAction);
}
@Override
public void templateCancelled(Template template) {
handleTemplateFinished(project, editor, caretMarker, startAction);
}
});
}
private static void handleTemplateFinished(Project project,
Editor editor,
RangeMarker caretMarker,
StartMarkAction startAction) {
try {
editor.getCaretModel().moveToOffset(caretMarker.getEndOffset());
editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
}
finally {
FinishMarkAction.finish(project, editor, startAction);
}
}
private static BnfListEntry addTokenDefinition(Project project,
BnfFile bnfFile,
String tokenName,
String tokenText,
Set<String> tokenNames) {
String fixedTokenName = new UniqueNameGenerator(tokenNames, null).generateUniqueName(StringUtil.notNullize(tokenName, "token"));
String newAttrText = "tokens = [\n " + fixedTokenName + "=" + StringUtil.notNullize(tokenText, "\"\"") + "\n ]";
BnfAttr newAttr = BnfElementFactory.createAttributeFromText(project, newAttrText);
BnfAttrs attrs = ContainerUtil.getFirstItem(bnfFile.getAttributes());
BnfAttr tokensAttr = null;
if (attrs == null) {
attrs = (BnfAttrs) bnfFile.addAfter(newAttr.getParent(), null);
bnfFile.addAfter(BnfElementFactory.createLeafFromText(project, "\n"), attrs);
tokensAttr = attrs.getAttrList().get(0);
BnfValueList attrExpr = (BnfValueList)Objects.requireNonNull(tokensAttr.getExpression());
return attrExpr.getListEntryList().get(0);
}
else {
for (BnfAttr attr : attrs.getAttrList()) {
if (KnownAttribute.TOKENS.getName().equals(attr.getName())) {
tokensAttr = attr;
}
}
if (tokensAttr == null) {
List<BnfAttr> attrList = attrs.getAttrList();
PsiElement anchor = attrList.isEmpty() ? attrs.getFirstChild() : attrList.get(attrList.size() - 1);
newAttr = (BnfAttr) attrs.addAfter(newAttr, anchor);
attrs.addAfter(BnfElementFactory.createLeafFromText(project, "\n "), anchor);
BnfValueList attrExpr = (BnfValueList)Objects.requireNonNull(newAttr.getExpression());
return attrExpr.getListEntryList().get(0);
}
else {
BnfExpression expression = tokensAttr.getExpression();
List<BnfListEntry> entryList = expression instanceof BnfValueList ? ((BnfValueList) expression).getListEntryList() : null;
if (entryList == null || entryList.isEmpty()) {
tokensAttr = (BnfAttr)tokensAttr.replace(newAttr);
BnfValueList attrExpr = (BnfValueList)Objects.requireNonNull(tokensAttr.getExpression());
return attrExpr.getListEntryList().get(0);
}
else {
for (BnfListEntry entry : entryList) {
PsiElement id = entry.getId();
if (id != null && id.getText().equals(tokenName)) {
return entry;
}
}
BnfValueList attrExpr = (BnfValueList)Objects.requireNonNull(newAttr.getExpression());
BnfListEntry newValue = attrExpr.getListEntryList().get(0);
PsiElement anchor = entryList.get(entryList.size() - 1);
newValue = (BnfListEntry) expression.addAfter(newValue, anchor);
expression.addAfter(BnfElementFactory.createLeafFromText(project, "\n "), anchor);
return newValue;
}
}
}
}
}