/* * Copyright 2012-2014 Sergey Ignatov * * 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.erlang.refactoring.introduce; import com.intellij.codeInsight.CodeInsightUtilCore; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.application.Result; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.SelectionModel; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.Pass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiParserFacade; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.refactoring.RefactoringActionHandler; import com.intellij.refactoring.introduce.inplace.InplaceVariableIntroducer; import com.intellij.refactoring.introduce.inplace.OccurrencesChooser; import com.intellij.refactoring.util.CommonRefactoringUtil; import com.intellij.util.containers.ContainerUtil; import org.intellij.erlang.psi.*; import org.intellij.erlang.psi.impl.ErlangElementFactory; import org.intellij.erlang.psi.impl.ErlangPsiImplUtil; import org.intellij.erlang.refactoring.ErlangRefactoringUtil; import org.intellij.erlang.refactoring.VariableTextBuilder; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.LinkedHashSet; import java.util.List; public class ErlangIntroduceVariableHandler implements RefactoringActionHandler { private final static Logger LOG = Logger.getInstance(ErlangIntroduceVariableHandler.class); public enum ReplaceStrategy { ALL, SINGLE, ASK } private ReplaceStrategy myReplaceStrategy; public ErlangIntroduceVariableHandler(ReplaceStrategy replaceStrategy) { myReplaceStrategy = replaceStrategy; } public ErlangIntroduceVariableHandler() { this(ReplaceStrategy.ASK); } @Override public void invoke(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file, @Nullable DataContext dataContext) { if (!CommonRefactoringUtil.checkReadOnlyStatus(file)) { return; } SelectionModel selectionModel = editor.getSelectionModel(); if (selectionModel.hasSelection()) { Pair<PsiElement, PsiElement> pair = ErlangRefactoringUtil.selectionToElements(file, selectionModel); if (pair.first == null || pair.second == null) { showCannotPerformError(project, editor); return; } ErlangExpression selectedExpression = getSelectedExpression(pair.first, pair.second); if (selectedExpression == null) { showCannotPerformError(project, editor); } else { performOnElement(editor, selectedExpression); } return; } smartIntroduce(editor, file); } @Nullable private static ErlangExpression getSelectedExpression(@NotNull PsiElement element1, @NotNull PsiElement element2) { PsiElement parent = PsiTreeUtil.findCommonParent(element1, element2); if (parent == null) { return null; } if (parent instanceof ErlangExpression) { return (ErlangExpression) parent; } return PsiTreeUtil.getParentOfType(parent, ErlangExpression.class); } private void smartIntroduce(@NotNull Editor editor, @NotNull PsiFile file) { ErlangRefactoringUtil.smartIntroduce(editor, file, new ErlangRefactoringUtil.Extractor() { @Override public boolean checkContext(@NotNull PsiFile file, @NotNull Editor editor, @Nullable PsiElement element) { return checkIntroduceContext(file, editor, element); } @Override public void process(@NotNull Editor editor, @NotNull ErlangExpression expression) { performOnElement(editor, expression); } } ); } private void performActionOnElementOccurrences(@NotNull final Editor editor, @NotNull final ErlangExpression expression) { if (!editor.getSettings().isVariableInplaceRenameEnabled()) return; switch (myReplaceStrategy) { case ASK: { OccurrencesChooser.simpleChooser(editor).showChooser( expression, getOccurrences(expression), new Pass<OccurrencesChooser.ReplaceChoice>() { @Override public void pass(OccurrencesChooser.ReplaceChoice replaceChoice) { performInplaceIntroduce(editor, expression, replaceChoice == OccurrencesChooser.ReplaceChoice.ALL); } }); break; } case ALL: { performInplaceIntroduce(editor, expression, true); break; } case SINGLE: { performInplaceIntroduce(editor, expression, false); break; } } } private void performOnElement(@NotNull Editor editor, @NotNull ErlangExpression expression) { performActionOnElementOccurrences(editor, expression); } private static void performInplaceIntroduce(@NotNull Editor editor, @NotNull ErlangExpression expression, boolean replaceAll) { List<PsiElement> occurrences = replaceAll ? getOccurrences(expression) : ContainerUtil.<PsiElement>list(expression); PsiElement declaration = performElement(editor, expression, occurrences); ErlangQVar target = PsiTreeUtil.findChildOfType(declaration, ErlangQVar.class); if (target == null) { return; } editor.getCaretModel().moveToOffset(target.getTextRange().getStartOffset()); InplaceVariableIntroducer<PsiElement> introducer = new ErlangInplaceVariableIntroducer(target, editor, expression.getProject(), occurrences); introducer.performInplaceRefactoring(new LinkedHashSet<>()); } @Nullable private static PsiElement performElement(Editor editor, @NotNull ErlangExpression expression, @NotNull List<PsiElement> occurrences) { VariableTextBuilder builder = new VariableTextBuilder(); expression.accept(builder); String newName = builder.result(); ErlangExpression initializer = ErlangPsiImplUtil.getNotParenthesizedExpression(expression); String newText = initializer != null ? newName + " = " + initializer.getText() : null; Project project = expression.getProject(); if (PsiTreeUtil.hasErrorElements(expression)) { showCannotPerformError(project, editor, "Selected expression contains errors"); return null; } PsiElement declaration = null; try { declaration = newText != null ? ErlangElementFactory.createExpressionFromText(project, newText) : null; } catch (Exception e) { LOG.error("Can't create a new expression:\n" + newText + "\n", e); } if (declaration == null) { showCannotPerformError(project, editor); return null; } declaration = performReplace(newName, declaration, expression, occurrences); declaration = CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(declaration); return declaration; } private static void modifyDeclaration(@NotNull PsiElement declaration) { if (PsiTreeUtil.getParentOfType(declaration, ErlangArgumentDefinition.class) != null) return; PsiElement comma = ErlangElementFactory.createLeafFromText(declaration.getProject(), ",\n"); PsiElement newLineNode = PsiParserFacade.SERVICE.getInstance(declaration.getProject()).createWhiteSpaceFromText("\n"); PsiElement parent = declaration.getParent(); PsiElement psiElement = parent.addAfter(comma, declaration); parent.addAfter(newLineNode, psiElement); } private static PsiElement performReplace(@NotNull final String newName, @NotNull final PsiElement declaration, @NotNull ErlangExpression initializer, @NotNull final List<PsiElement> occurrences) { final Project project = declaration.getProject(); return new WriteCommandAction<PsiElement>(project, "Extract variable", initializer.getContainingFile()) { protected void run(@NotNull Result<PsiElement> result) throws Throwable { boolean alone = occurrences.size() == 1 && occurrences.get(0).getParent() instanceof ErlangClauseBody; PsiElement createdDeclaration = replaceLeftmostArgumentDefinition(declaration, occurrences); if (createdDeclaration == null) { createdDeclaration = addDeclaration(declaration, occurrences); } result.setResult(createdDeclaration); if (alone) { PsiElement firstItem = ContainerUtil.getFirstItem(occurrences); if (firstItem != null) firstItem.delete(); } else { if (createdDeclaration != null) { modifyDeclaration(createdDeclaration); } PsiElement newExpression = ErlangElementFactory.createQVarFromText(project, newName); for (PsiElement occurrence : occurrences) { ErlangPsiImplUtil.getOutermostParenthesizedExpression((ErlangExpression) occurrence).replace(newExpression); } } } }.execute().getResultObject(); } private static PsiElement addDeclaration(@NotNull PsiElement declaration, @NotNull List<PsiElement> occurrences) { PsiElement anchor = findAnchor(occurrences); assert anchor != null; PsiElement parent = anchor.getParent(); return parent.addBefore(declaration, anchor); } @Nullable private static PsiElement replaceLeftmostArgumentDefinition(@NotNull PsiElement declaration, @NotNull List<PsiElement> occurrences) { PsiElement argDef = extractLeftmostArgumentDefinition(occurrences); return argDef == null ? null : argDef.replace(declaration); } @Nullable private static PsiElement extractLeftmostArgumentDefinition(@NotNull List<PsiElement> occurrences) { int occurrenceOffset = Integer.MAX_VALUE; int occurrenceIndex = -1; int currentOccurrenceIndex = 0; for (PsiElement occurrence : occurrences) { ErlangArgumentDefinition argDef = PsiTreeUtil.getParentOfType(occurrence, ErlangArgumentDefinition.class); if (argDef != null) { int startOffset = argDef.getTextRange().getStartOffset(); if (startOffset < occurrenceOffset) { occurrenceOffset = startOffset; occurrenceIndex = currentOccurrenceIndex; } } currentOccurrenceIndex++; } return occurrenceIndex == -1 ? null : occurrences.remove(occurrenceIndex); } private static boolean checkIntroduceContext(@NotNull PsiFile file, @NotNull Editor editor, @Nullable PsiElement element) { if (!isValidIntroduceContext(element)) { showCannotPerformError(file.getProject(), editor); return false; } return true; } private static void showCannotPerformError(@NotNull Project project, @NotNull Editor editor) { showCannotPerformError(project, editor, "Cannot Perform Refactoring"); } private static void showCannotPerformError(@NotNull Project project, @NotNull Editor editor, @NotNull String message) { CommonRefactoringUtil.showErrorHint(project, editor, message, "Cannot Perform Refactoring", "refactoring.extractVariable"); } private static boolean isValidIntroduceContext(@Nullable PsiElement element) { return PsiTreeUtil.getParentOfType(element, ErlangClauseBody.class) != null; } @Nullable private static PsiElement findAnchor(@NotNull List<PsiElement> occurrences) { PsiElement anchor = occurrences.get(0); next: do { //noinspection unchecked ErlangCompositeElement clause = PsiTreeUtil.getParentOfType(anchor, ErlangClauseBody.class, ErlangTryExpressionsClause.class); int minOffset = Integer.MAX_VALUE; for (PsiElement element : occurrences) { minOffset = Math.min(minOffset, element.getTextOffset()); if (!PsiTreeUtil.isAncestor(clause, element, true)) { if (clause == null) return null; anchor = clause; continue next; } } if (clause == null) { return null; } PsiElement child = null; PsiElement[] children = clause.getChildren(); for (PsiElement aChildren : children) { child = aChildren; if (child.getTextRange().contains(minOffset)) { break; } } return child; } while (true); } @NotNull private static List<PsiElement> getOccurrences(@NotNull ErlangExpression expression) { ErlangFunctionClause function = PsiTreeUtil.getParentOfType(expression, ErlangFunctionClause.class); return ErlangRefactoringUtil.getOccurrences(expression, function); } @Override public void invoke(@NotNull Project project, @NotNull PsiElement[] elements, DataContext dataContext) { } private static class ErlangInplaceVariableIntroducer extends InplaceVariableIntroducer<PsiElement> { private ErlangQVar myTarget; public ErlangInplaceVariableIntroducer(ErlangQVar target, Editor editor, Project project, @NotNull List<PsiElement> occurrences) { super(target, editor, project, "Introduce Variable", occurrences.toArray(new PsiElement[occurrences.size()]), null); myTarget = target; } @Override protected PsiElement checkLocalScope() { return myTarget.getContainingFile(); } } }