/*
* Copyright 2011-2014 Gregory Shrago
*
* 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 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.ObjectUtils;
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.*;
/**
* @author greg
*/
public class BnfIntroduceTokenHandler implements RefactoringActionHandler {
public static final String REFACTORING_NAME = "Introduce Token";
@Override
public void invoke(final @NotNull Project project, @NotNull PsiElement[] elements, DataContext dataContext) {
// do not support this case
}
@Override
public void invoke(@NotNull final Project project,
final Editor editor,
final PsiFile file,
@Nullable DataContext dataContext) {
if (!(file instanceof BnfFile)) return;
final BnfFile bnfFile = (BnfFile) file;
final Map<String, String> tokenMap = RuleGraphHelper.getTokenMap(bnfFile);
final String tokenText;
final 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();
String existingKey = null;
for (String key : tokenMap.keySet()) {
if (tokenName.equals(tokenMap.get(key))) {
existingKey = key;
break;
}
}
tokenText = existingKey;
}
else if (target instanceof BnfStringLiteralExpression) {
if (PsiTreeUtil.getParentOfType(target, BnfAttrs.class) != null) return;
tokenText = target.getText();
tokenName = tokenMap.get(StringUtil.unquoteString(tokenText));
}
else return;
final ArrayList<BnfExpression> allOccurrences = new ArrayList<BnfExpression>();
final LinkedHashMap<OccurrencesChooser.ReplaceChoice, List<BnfExpression>> occurrencesMap = new LinkedHashMap<OccurrencesChooser.ReplaceChoice, List<BnfExpression>>();
occurrencesMap.put(OccurrencesChooser.ReplaceChoice.NO, Collections.singletonList(target));
occurrencesMap.put(OccurrencesChooser.ReplaceChoice.ALL, allOccurrences);
GrammarUtil.visitRecursively(file, true, new BnfVisitor() {
@Override
public void visitStringLiteralExpression(@NotNull BnfStringLiteralExpression o) {
if (tokenText != null && tokenText.equals(o.getText())) {
allOccurrences.add(o);
}
}
@Override
public void visitReferenceOrToken(@NotNull BnfReferenceOrToken o) {
if (GrammarUtil.isExternalReference(o)) return;
if (tokenName != null && tokenName.equals(o.getText())) {
allOccurrences.add(o);
}
}
});
if (occurrencesMap.get(OccurrencesChooser.ReplaceChoice.ALL).size() <= 1 && !ApplicationManager.getApplication().isUnitTestMode()) {
occurrencesMap.remove(OccurrencesChooser.ReplaceChoice.ALL);
}
final Pass<OccurrencesChooser.ReplaceChoice> callback = new Pass<OccurrencesChooser.ReplaceChoice>() {
@Override
public void pass(final OccurrencesChooser.ReplaceChoice choice) {
new WriteCommandAction(project, REFACTORING_NAME, file) {
@Override
protected void run(com.intellij.openapi.application.Result result) throws Throwable {
buildTemplateAndRun(project, editor, bnfFile, occurrencesMap.get(choice), tokenName, tokenText, tokenMap);
}
}.execute();
}
};
if (ApplicationManager.getApplication().isUnitTestMode()) {
callback.pass(OccurrencesChooser.ReplaceChoice.ALL);
}
else {
new OccurrencesChooser<BnfExpression>(editor) {
@Override
protected TextRange getOccurrenceRange(BnfExpression occurrence) {
return occurrence.getTextRange();
}
}.showChooser(callback, occurrencesMap);
}
}
private void buildTemplateAndRun(final Project project,
final Editor editor,
BnfFile bnfFile, List<BnfExpression> occurrences,
String tokenName,
String tokenText,
Map<String, String> tokenMap) throws StartMarkAction.AlreadyStartedException {
final StartMarkAction startAction = StartMarkAction.start(editor, project, REFACTORING_NAME);
BnfListEntry entry = addTokenDefinition(project, bnfFile, tokenName, tokenText, tokenMap);
PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.getDocument());
TemplateBuilderImpl builder = new TemplateBuilderImpl(bnfFile);
PsiElement tokenId = ObjectUtils.assertNotNull(entry.getId());
PsiElement tokenValue = ObjectUtils.assertNotNull(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() {
@Nullable
@Override
public Result calculateResult(ExpressionContext context) {
TemplateState state = TemplateManagerImpl.getTemplateState(context.getEditor());
assert state != null;
TextResult text = ObjectUtils.assertNotNull(state.getVariableValue("TokenText"));
String curText = StringUtil.unquoteString(text.getText());
if (ParserGeneratorUtil.isRegexpToken(curText)) {
return state.getVariableValue("TokenName");
}
else {
return new TextResult("'" + curText + "'");
}
}
@Nullable
@Override
public Result calculateQuickResult(ExpressionContext context) {
return calculateResult(context);
}
@Nullable
@Override
public LookupElement[] calculateLookupItems(ExpressionContext context) {
return LookupElement.EMPTY_ARRAY;
}
}, false);
}
final 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(Template template, boolean brokenOff) {
handleTemplateFinished(project, editor, caretMarker, startAction);
}
@Override
public void templateCancelled(Template template) {
handleTemplateFinished(project, editor, caretMarker, startAction);
}
});
}
private 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,
Map<String, String> tokenMap) {
String fixedTokenName = new UniqueNameGenerator(tokenMap.values(), 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);
tokensAttr = attrs.getAttrList().get(0);
return ((BnfValueList) tokensAttr.getExpression()).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);
return ((BnfValueList) newAttr.getExpression()).getListEntryList().get(0);
}
else {
BnfExpression expression = tokensAttr.getExpression();
List<BnfListEntry> entryList = expression instanceof BnfValueList ? ((BnfValueList) expression).getListEntryList() : null;
if (entryList == null || entryList.isEmpty()) {
expression.replace(newAttr.getParent());
return ((BnfValueList) tokensAttr.getExpression()).getListEntryList().get(0);
}
else {
for (BnfListEntry entry : entryList) {
PsiElement id = entry.getId();
if (id != null && id.getText().equals(tokenName)) {
return entry;
}
}
BnfListEntry newValue = ((BnfValueList) newAttr.getExpression()).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;
}
}
}
}
}