diff --git a/java/java-impl/src/com/intellij/lang/java/JavaRefactoringSupportProvider.java b/java/java-impl/src/com/intellij/lang/java/JavaRefactoringSupportProvider.java index 7c62f3d89c92..685744a8c4f1 100644 --- a/java/java-impl/src/com/intellij/lang/java/JavaRefactoringSupportProvider.java +++ b/java/java-impl/src/com/intellij/lang/java/JavaRefactoringSupportProvider.java @@ -33,6 +33,7 @@ import com.intellij.refactoring.extractclass.ExtractClassHandler; import com.intellij.refactoring.introduceField.IntroduceConstantHandler; import com.intellij.refactoring.introduceField.IntroduceFieldHandler; import com.intellij.refactoring.introduceParameter.IntroduceParameterHandler; +import com.intellij.refactoring.introduceVariable.IntroduceFunctionalVariableHandler; import com.intellij.refactoring.introduceVariable.IntroduceVariableHandler; import com.intellij.refactoring.memberPullUp.JavaPullUpHandler; import com.intellij.refactoring.memberPushDown.JavaPushDownHandler; @@ -92,6 +93,11 @@ public class JavaRefactoringSupportProvider extends RefactoringSupportProvider { return new IntroduceFunctionalParameterHandler(); } + @Override + public RefactoringActionHandler getIntroduceFunctionalVariableHandler() { + return new IntroduceFunctionalVariableHandler(); + } + @Override public RefactoringActionHandler getPullUpHandler() { return new JavaPullUpHandler(); diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/ExtractMethodDialog.java b/java/java-impl/src/com/intellij/refactoring/extractMethod/ExtractMethodDialog.java index e69233b24e3e..083df697eb10 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractMethod/ExtractMethodDialog.java +++ b/java/java-impl/src/com/intellij/refactoring/extractMethod/ExtractMethodDialog.java @@ -124,6 +124,7 @@ public class ExtractMethodDialog extends DialogWrapper implements AbstractExtrac if (canBeChainedConstructor) { myCbChainedConstructor = new NonFocusableCheckBox(RefactoringBundle.message("extract.chained.constructor.checkbox")); } + myInputVariables = myVariableData.getInputVariables().toArray(new VariableData[myVariableData.getInputVariables().size()]); init(); } @@ -308,28 +309,7 @@ public class ExtractMethodDialog extends DialogWrapper implements AbstractExtrac //optionsPanel.add(new JLabel("Options: ")); - if (myStaticFlag || myCanBeStatic) { - myMakeStatic.setEnabled(!myStaticFlag); - myMakeStatic.setSelected(myStaticFlag); - if (myVariableData.hasInstanceFields()) { - myMakeStatic.setText(RefactoringBundle.message("declare.static.pass.fields.checkbox")); - } - myMakeStatic.addItemListener(e -> { - if (myVariableData.hasInstanceFields()) { - myVariableData.setPassFields(myMakeStatic.isSelected()); - myInputVariables = myVariableData.getInputVariables().toArray(new VariableData[myVariableData.getInputVariables().size()]); - updateVarargsEnabled(); - createParametersPanel(); - } - updateSignature(); - }); - optionsPanel.add(myMakeStatic); - } else { - myMakeStatic.setSelected(false); - myMakeStatic.setEnabled(false); - } - final Border emptyBorder = IdeBorderFactory.createEmptyBorder(5, 0, 5, 4); - myMakeStatic.setBorder(emptyBorder); + createStaticOptions(optionsPanel, RefactoringBundle.message("declare.static.pass.fields.checkbox")); myFoldParameters.setSelected(myVariableData.isFoldingSelectedByDefault()); myFoldParameters.setVisible(myVariableData.isFoldable()); @@ -344,6 +324,7 @@ public class ExtractMethodDialog extends DialogWrapper implements AbstractExtrac updateSignature(); }); optionsPanel.add(myFoldParameters); + final Border emptyBorder = IdeBorderFactory.createEmptyBorder(5, 0, 5, 4); myFoldParameters.setBorder(emptyBorder); boolean canBeVarargs = false; @@ -394,6 +375,30 @@ public class ExtractMethodDialog extends DialogWrapper implements AbstractExtrac return optionsPanel; } + protected void createStaticOptions(JPanel optionsPanel, String passFieldsAsParamsLabel) { + if (myStaticFlag || myCanBeStatic) { + myMakeStatic.setEnabled(!myStaticFlag); + myMakeStatic.setSelected(myStaticFlag); + if (myVariableData.hasInstanceFields()) { + myMakeStatic.setText(passFieldsAsParamsLabel); + } + myMakeStatic.addItemListener(e -> { + if (myVariableData.hasInstanceFields()) { + myVariableData.setPassFields(myMakeStatic.isSelected()); + myInputVariables = myVariableData.getInputVariables().toArray(new VariableData[myVariableData.getInputVariables().size()]); + updateVarargsEnabled(); + createParametersPanel(); + } + updateSignature(); + }); + optionsPanel.add(myMakeStatic); + } else { + myMakeStatic.setSelected(false); + myMakeStatic.setEnabled(false); + } + myMakeStatic.setBorder(IdeBorderFactory.createEmptyBorder(5, 0, 5, 4)); + } + private ComboBoxVisibilityPanel createVisibilityPanel() { final JavaComboBoxVisibilityPanel panel = new JavaComboBoxVisibilityPanel(); final PsiMethod containingMethod = getContainingMethod(); @@ -432,7 +437,8 @@ public class ExtractMethodDialog extends DialogWrapper implements AbstractExtrac @Override @NotNull public String getVisibility() { - return myTargetClass.isInterface() ? PsiModifier.PUBLIC : ObjectUtils.notNull(myVisibilityPanel.getVisibility(), PsiModifier.PUBLIC); + return myTargetClass.isInterface() || myVisibilityPanel == null + ? PsiModifier.PUBLIC : ObjectUtils.notNull(myVisibilityPanel.getVisibility(), PsiModifier.PUBLIC); } @Override diff --git a/java/java-impl/src/com/intellij/refactoring/extractMethod/ExtractMethodProcessor.java b/java/java-impl/src/com/intellij/refactoring/extractMethod/ExtractMethodProcessor.java index 302bd6c5f96b..79c79d81e084 100644 --- a/java/java-impl/src/com/intellij/refactoring/extractMethod/ExtractMethodProcessor.java +++ b/java/java-impl/src/com/intellij/refactoring/extractMethod/ExtractMethodProcessor.java @@ -549,8 +549,7 @@ public class ExtractMethodProcessor implements MatchProvider { } protected AbstractExtractDialog createExtractMethodDialog(final boolean direct) { - final List variables = myInputVariables.getInputVariables(); - myVariableDatum = variables.toArray(new VariableData[variables.size()]); + setDataFromInputVariables(); myNullness = initNullness(); myArtificialOutputVariable = PsiType.VOID.equals(myReturnType) ? getArtificialOutputVariable() : null; final PsiType returnType = myArtificialOutputVariable != null ? myArtificialOutputVariable.getType() : myReturnType; @@ -608,6 +607,11 @@ public class ExtractMethodProcessor implements MatchProvider { }; } + public void setDataFromInputVariables() { + final List variables = myInputVariables.getInputVariables(); + myVariableDatum = variables.toArray(new VariableData[variables.size()]); + } + public PsiExpression[] findOccurrences() { if (myExpression != null) { return new PsiExpression[] {myExpression}; @@ -1350,7 +1354,7 @@ public class ExtractMethodProcessor implements MatchProvider { } list.add(parm); } - else { + else if (defineVariablesForUnselectedParameters()){ @NonNls StringBuilder buffer = new StringBuilder(); if (isFinal) { buffer.append("final "); @@ -1382,6 +1386,10 @@ public class ExtractMethodProcessor implements MatchProvider { return (PsiMethod)myStyleManager.reformat(newMethod); } + protected boolean defineVariablesForUnselectedParameters() { + return true; + } + private void copyParamAnnotations(PsiParameter parm) { final PsiVariable variable = PsiResolveHelper.SERVICE.getInstance(myProject).resolveReferencedVariable(parm.getName(), myElements[0]); if (variable instanceof PsiParameter) { @@ -1735,6 +1743,10 @@ public class ExtractMethodProcessor implements MatchProvider { return myExtractedMethod; } + public void setMethodName(String methodName) { + myMethodName = methodName; + } + public Boolean hasDuplicates() { List duplicates = getDuplicates(); if (duplicates != null && !duplicates.isEmpty()) { @@ -1831,4 +1843,8 @@ public class ExtractMethodProcessor implements MatchProvider { public PsiVariable[] getOutputVariables() { return myOutputVariables; } + + public void setMethodVisibility(String methodVisibility) { + myMethodVisibility = methodVisibility; + } } diff --git a/java/java-impl/src/com/intellij/refactoring/introduceParameter/IntroduceParameterHandler.java b/java/java-impl/src/com/intellij/refactoring/introduceParameter/IntroduceParameterHandler.java index 1cbc47bc510c..e8521d613eac 100644 --- a/java/java-impl/src/com/intellij/refactoring/introduceParameter/IntroduceParameterHandler.java +++ b/java/java-impl/src/com/intellij/refactoring/introduceParameter/IntroduceParameterHandler.java @@ -548,20 +548,7 @@ public class IntroduceParameterHandler extends IntroduceHandlerBase { return false; } - final PsiElement[] elementsCopy; - if (!elements[0].isPhysical()) { - elementsCopy = elements; - } - else { - final PsiFile copy = PsiFileFactory.getInstance(project) - .createFileFromText(file.getName(), file.getFileType(), file.getText(), file.getModificationStamp(), false); - final TextRange range = new TextRange(elements[0].getTextRange().getStartOffset(), - elements[elements.length - 1].getTextRange().getEndOffset()); - final PsiExpression exprInRange = CodeInsightUtil.findExpressionInRange(copy, range.getStartOffset(), range.getEndOffset()); - elementsCopy = exprInRange != null - ? new PsiElement[]{exprInRange} - : CodeInsightUtil.findStatementsInRange(copy, range.getStartOffset(), range.getEndOffset()); - } + final PsiElement[] elementsCopy = getElementsInCopy(project, file, elements); final PsiMethod containingMethodCopy = Util.getContainingMethod(elementsCopy[0]); LOG.assertTrue(containingMethodCopy != null); final List enclosingMethodsInCopy = getEnclosingMethods(containingMethodCopy); @@ -613,6 +600,28 @@ public class IntroduceParameterHandler extends IntroduceHandlerBase { return false; } + public static PsiElement[] getElementsInCopy(Project project, PsiFile file, PsiElement[] elements) { + final PsiElement[] elementsCopy; + if (!elements[0].isPhysical()) { + elementsCopy = elements; + } + else { + final PsiFile copy = PsiFileFactory.getInstance(project) + .createFileFromText(file.getName(), file.getFileType(), file.getText(), file.getModificationStamp(), false); + final TextRange range = new TextRange(elements[0].getTextRange().getStartOffset(), + elements[elements.length - 1].getTextRange().getEndOffset()); + final PsiExpression exprInRange = CodeInsightUtil.findExpressionInRange(copy, range.getStartOffset(), range.getEndOffset()); + elementsCopy = exprInRange != null + ? new PsiElement[]{exprInRange} + : CodeInsightUtil.findStatementsInRange(copy, range.getStartOffset(), range.getEndOffset()); + } + if (elementsCopy.length == 1 && elementsCopy[0].getUserData(ElementToWorkOn.PARENT) == null) { + elementsCopy[0].putUserData(ElementToWorkOn.REPLACE_NON_PHYSICAL, true); + } + + return elementsCopy; + } + private void functionalInterfaceSelected(final PsiType selectedType, final List enclosingMethods, final Project project, @@ -628,7 +637,7 @@ public class IntroduceParameterHandler extends IntroduceHandlerBase { PsiMethod methodToSearchFor, Editor editor, final Project project, final PsiType selectedType, - final MyExtractMethodProcessor processor, + final ExtractMethodProcessor processor, final PsiElement[] elements) { final PsiElement commonParent = findCommonParent(elements); if (commonParent == null) { @@ -657,11 +666,6 @@ public class IntroduceParameterHandler extends IntroduceHandlerBase { LOG.assertTrue(method != null); final String interfaceMethodName = method.getName(); processor.setMethodName(interfaceMethodName); - - if (copyElements.length == 1 && copyElements[0].getUserData(ElementToWorkOn.PARENT) == null) { - copyElements[0].putUserData(ElementToWorkOn.REPLACE_NON_PHYSICAL, true); - } - processor.doExtract(); final PsiMethod extractedMethod = processor.getExtractedMethod(); @@ -730,10 +734,6 @@ public class IntroduceParameterHandler extends IntroduceHandlerBase { return false; } - public void setMethodName(String methodName) { - myMethodName = methodName; - } - @Override public Boolean hasDuplicates() { return false; diff --git a/java/java-impl/src/com/intellij/refactoring/introduceVariable/IntroduceFunctionalVariableHandler.java b/java/java-impl/src/com/intellij/refactoring/introduceVariable/IntroduceFunctionalVariableHandler.java new file mode 100644 index 000000000000..c47ddc0ef168 --- /dev/null +++ b/java/java-impl/src/com/intellij/refactoring/introduceVariable/IntroduceFunctionalVariableHandler.java @@ -0,0 +1,320 @@ +/* + * Copyright 2000-2017 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 com.intellij.refactoring.introduceVariable; + +import com.intellij.codeInsight.FunctionalInterfaceSuggester; +import com.intellij.codeInsight.navigation.NavigationUtil; +import com.intellij.ide.util.PsiClassListCellRenderer; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pass; +import com.intellij.psi.*; +import com.intellij.psi.codeStyle.JavaCodeStyleManager; +import com.intellij.psi.codeStyle.SuggestedNameInfo; +import com.intellij.psi.search.PsiElementProcessor; +import com.intellij.psi.util.PsiFormatUtil; +import com.intellij.psi.util.PsiFormatUtilBase; +import com.intellij.psi.util.PsiUtil; +import com.intellij.refactoring.HelpID; +import com.intellij.refactoring.RefactoringBundle; +import com.intellij.refactoring.actions.IntroduceFunctionalVariableAction; +import com.intellij.refactoring.extractMethod.*; +import com.intellij.refactoring.introduceParameter.IntroduceParameterHandler; +import com.intellij.refactoring.rename.inplace.VariableInplaceRenamer; +import com.intellij.refactoring.util.CommonRefactoringUtil; +import com.intellij.refactoring.util.RefactoringUtil; +import com.intellij.util.ObjectUtils; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +public class IntroduceFunctionalVariableHandler extends IntroduceVariableHandler { + + @Override + public void invoke(@NotNull final Project project, final Editor editor, final PsiFile file, DataContext dataContext) { + ExtractMethodHandler.selectAndPass(project, editor, file, new Pass() { + @Override + public void pass(PsiElement[] elements) { + PsiElement anchorStatement = RefactoringUtil.getParentStatement(elements[0], false); + PsiElement tempContainer = checkAnchorStatement(project, editor, anchorStatement); + if (tempContainer == null) return; + + PsiElement[] elementsInCopy = IntroduceParameterHandler.getElementsInCopy(project, file, elements); + MyExtractMethodProcessor processor = + new MyExtractMethodProcessor(project, editor, elementsInCopy, null, IntroduceFunctionalVariableAction.REFACTORING_NAME, null, + HelpID.INTRODUCE_VARIABLE); + try { + processor.prepare(); + } + catch (PrepareFailedException e) { + showErrorMessage(project, editor); + } + + if (!processor.showDialog()) return; + + final PsiMethod emptyMethod = JavaPsiFacade.getElementFactory(project) + .createMethodFromText(processor.generateEmptyMethod("name").getText(), elements[0]); + final Collection types = FunctionalInterfaceSuggester.suggestFunctionalInterfaces(emptyMethod); + if (types.isEmpty()) { + showErrorMessage(project, editor, "No applicable functional interfaces found"); + return; + } + if (types.size() == 1 || ApplicationManager.getApplication().isUnitTestMode()) { + functionalInterfaceSelected(ContainerUtil.getFirstItem(types), project, editor, processor, elements, anchorStatement); + } + else { + final Map classes = new LinkedHashMap<>(); + for (PsiType type : types) { + classes.put(PsiUtil.resolveClassInType(type), type); + } + final PsiClass[] psiClasses = classes.keySet().toArray(new PsiClass[classes.size()]); + final String methodSignature = + PsiFormatUtil.formatMethod(emptyMethod, PsiSubstitutor.EMPTY, PsiFormatUtilBase.SHOW_PARAMETERS, PsiFormatUtilBase.SHOW_TYPE); + final PsiType returnType = emptyMethod.getReturnType(); + assert returnType != null; + final String title = "Choose Applicable Functional Interface: " + methodSignature + " -> " + returnType.getPresentableText(); + NavigationUtil.getPsiElementPopup(psiClasses, new PsiClassListCellRenderer(), title, + new PsiElementProcessor() { + @Override + public boolean execute(@NotNull PsiClass psiClass) { + functionalInterfaceSelected(classes.get(psiClass), project, editor, processor, elements, + anchorStatement); + return true; + } + }).showInBestPositionFor(editor); + } + } + }); + } + + private static void functionalInterfaceSelected(PsiType type, + Project project, + Editor editor, + MyExtractMethodProcessor processor, + PsiElement[] elements, + PsiElement anchorStatement) { + if (!CommonRefactoringUtil.checkReadOnlyStatus(project, elements[0])) return; + PsiMethodCallExpression functionalExpression = createReplacement(project, type, processor, elements); + + PsiExpression qualifier = functionalExpression.getMethodExpression().getQualifierExpression(); + + assert qualifier != null; + + SuggestedNameInfo uniqueNames = getSuggestedName(type, qualifier, anchorStatement); + + WriteCommandAction.runWriteCommandAction(project, () -> { + PsiDeclarationStatement declaration = + replaceSelectionWithFunctionalCall(type, elements, anchorStatement, functionalExpression, qualifier, uniqueNames.names[0]); + + PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.getDocument()); + + PsiLocalVariable localVariable = (PsiLocalVariable)declaration.getDeclaredElements()[0]; + PsiIdentifier nameIdentifier = localVariable.getNameIdentifier(); + final int textOffset = ObjectUtils.notNull(nameIdentifier, localVariable).getTextOffset(); + editor.getCaretModel().moveToOffset(textOffset); + new VariableInplaceRenamer(localVariable, editor) { + @Override + protected boolean shouldSelectAll() { + return true; + } + + @Override + protected void moveOffsetAfter(boolean success) { + super.moveOffsetAfter(success); + if (success) { + final PsiNamedElement renamedVariable = getVariable(); + if (renamedVariable != null) { + editor.getCaretModel().moveToOffset(renamedVariable.getTextRange().getEndOffset()); + } + } + } + }.performInplaceRename(); + }); + } + + private static PsiDeclarationStatement replaceSelectionWithFunctionalCall(PsiType type, + PsiElement[] elements, + PsiElement anchorStatement, + PsiMethodCallExpression functionalExpression, + PsiExpression qualifier, + String variableName) { + PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(functionalExpression.getProject()); + PsiElement tempContainer = anchorStatement.getParent(); + + boolean singleExpression = elements.length == 1 && elements[0] instanceof PsiExpression; + PsiDeclarationStatement declaration = elementFactory + .createVariableDeclarationStatement(variableName, type, qualifier, anchorStatement); + String callExpressionText = variableName + "." + + functionalExpression.getMethodExpression().getReferenceName() + + functionalExpression.getArgumentList().getText(); + if (singleExpression) { + elements[0].replace(elementFactory.createExpressionFromText(callExpressionText, declaration)); + } + if (RefactoringUtil.isLoopOrIf(tempContainer)) { + declaration = (PsiDeclarationStatement)RefactoringUtil.putStatementInLoopBody(declaration, tempContainer, anchorStatement, !singleExpression); + tempContainer = declaration.getParent(); + } + else { + declaration = (PsiDeclarationStatement)tempContainer.addBefore(declaration, anchorStatement); + if (!singleExpression) { + tempContainer.deleteChildRange(elements[0], elements[elements.length - 1]); + } + } + + if (!singleExpression) { + tempContainer.addAfter(elementFactory.createStatementFromText(callExpressionText + ";", declaration), declaration); + } + return (PsiDeclarationStatement)JavaCodeStyleManager.getInstance(declaration.getProject()).shortenClassReferences(declaration); + } + + private static PsiMethodCallExpression createReplacement(Project project, + PsiType selectedType, + ExtractMethodProcessor processor, + PsiElement[] elements) { + final PsiClassType.ClassResolveResult resolveResult = PsiUtil.resolveGenericsClassInType(selectedType); + final PsiClass wrapperClass = resolveResult.getElement(); + + final PsiElementFactory factory = JavaPsiFacade.getElementFactory(project); + final PsiMethod method = LambdaUtil.getFunctionalInterfaceMethod(wrapperClass); + assert method != null : "not functional class"; + final String interfaceMethodName = method.getName(); + processor.setMethodName(interfaceMethodName); + processor.doExtract(); + + final PsiMethod extractedMethod = processor.getExtractedMethod(); + final PsiParameter[] parameters = extractedMethod.getParameterList().getParameters(); + final PsiParameter[] interfaceParameters = method.getParameterList().getParameters(); + final PsiSubstitutor substitutor = resolveResult.getSubstitutor(); + for (int i = 0; i < interfaceParameters.length; i++) { + final PsiTypeElement typeAfterInterface = factory.createTypeElement(substitutor.substitute(interfaceParameters[i].getType())); + final PsiTypeElement typeElement = parameters[i].getTypeElement(); + if (typeElement != null) { + typeElement.replace(typeAfterInterface); + } + } + final PsiMethodCallExpression methodCall = processor.getMethodCall(); + PsiExpression psiExpression = factory + .createExpressionFromText("new " + selectedType.getCanonicalText() + "() {" + extractedMethod.getText() + "}." + methodCall.getText(), + elements[0]); + return (PsiMethodCallExpression)JavaCodeStyleManager.getInstance(project).shortenClassReferences(psiExpression); + } + + + @Override + protected void showErrorMessage(Project project, Editor editor, String message) { + CommonRefactoringUtil + .showErrorHint(project, editor, message, IntroduceFunctionalVariableAction.REFACTORING_NAME, HelpID.INTRODUCE_VARIABLE); + } + + private void showErrorMessage(@NotNull Project project, Editor editor) { + final String message = RefactoringBundle + .getCannotRefactorMessage( + RefactoringBundle.message("is.not.supported.in.the.current.context", IntroduceFunctionalVariableAction.REFACTORING_NAME)); + showErrorMessage(project, editor, message); + } + + protected void setupProcessorWithoutDialog(ExtractMethodProcessor processor, InputVariables inputVariables) { + processor.setDataFromInputVariables(); + processor.setMethodVisibility(PsiModifier.PUBLIC); + } + + private class MyExtractMethodProcessor extends ExtractMethodProcessor { + + public MyExtractMethodProcessor(Project project, + Editor editor, + PsiElement[] elements, + PsiType forcedReturnType, + String refactoringName, String initialMethodName, String helpId) { + super(project, editor, elements, forcedReturnType, refactoringName, initialMethodName, helpId); + } + + @Override + public boolean isStatic() { + return false; + } + + @Override + protected boolean isFoldingApplicable() { + return false; + } + + @Override + protected AbstractExtractDialog createExtractMethodDialog(boolean direct) { + setDataFromInputVariables(); + return new ExtractMethodDialog(myProject, myTargetClass, myInputVariables, null, getTypeParameterList(), + getThrownExceptions(), isStatic(), isCanBeStatic(), false, + IntroduceFunctionalVariableAction.REFACTORING_NAME, HelpID.INTRODUCE_VARIABLE, null, myElements) { + @Override + protected JComponent createNorthPanel() { + if (!myInputVariables.hasInstanceFields()) { + return null; + } + JPanel optionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 5)); + createStaticOptions(optionsPanel, "Pass fields as params"); + return optionsPanel; + } + + @Override + public JComponent getPreferredFocusedComponent() { + return myParamTable; + } + + @Override + protected String getSignature() { + String parametersList = + Arrays.stream(getChosenParameters()) + .filter(data -> data.passAsParameter) + .map(data -> data.type.getPresentableText()) + .reduce((result, item) -> result + ", " + item) + .orElse(""); + String returnTypeString = myReturnType == null || PsiType.VOID.equals(myReturnType) + ? "{}" : myReturnType.getPresentableText(); + return "(" + parametersList + ") -> " + returnTypeString; + } + + @NotNull + @Override + public String getVisibility() { + return PsiModifier.PUBLIC; + } + }; + } + + @Override + public boolean showDialog() { + if (!myInputVariables.hasInstanceFields() && myInputVariables.getInputVariables().isEmpty() || + ApplicationManager.getApplication().isUnitTestMode()) { + setupProcessorWithoutDialog(this, myInputVariables); + return true; + } + return super.showDialog(); + } + + @Override + protected boolean defineVariablesForUnselectedParameters() { + return false; + } + } +} diff --git a/java/java-impl/src/com/intellij/refactoring/introduceVariable/IntroduceVariableBase.java b/java/java-impl/src/com/intellij/refactoring/introduceVariable/IntroduceVariableBase.java index 32effde01998..4a64cd4bdaeb 100644 --- a/java/java-impl/src/com/intellij/refactoring/introduceVariable/IntroduceVariableBase.java +++ b/java/java-impl/src/com/intellij/refactoring/introduceVariable/IntroduceVariableBase.java @@ -613,18 +613,8 @@ public abstract class IntroduceVariableBase extends IntroduceHandlerBase { final PsiElement anchorStatement = RefactoringUtil.getParentStatement(physicalElement != null ? physicalElement : expr, false); - if (anchorStatement == null) { - return parentStatementNotFound(project, editor); - } - if (checkAnchorBeforeThisOrSuper(project, editor, anchorStatement, REFACTORING_NAME, HelpID.INTRODUCE_VARIABLE)) return false; - - final PsiElement tempContainer = anchorStatement.getParent(); - - if (!(tempContainer instanceof PsiCodeBlock) && !RefactoringUtil.isLoopOrIf(tempContainer) && !(tempContainer instanceof PsiLambdaExpression) && (tempContainer.getParent() instanceof PsiLambdaExpression)) { - String message = RefactoringBundle.message("refactoring.is.not.supported.in.the.current.context", REFACTORING_NAME); - showErrorMessage(project, editor, message); - return false; - } + PsiElement tempContainer = checkAnchorStatement(project, editor, anchorStatement); + if (tempContainer == null) return false; final PsiFile file = anchorStatement.getContainingFile(); LOG.assertTrue(file != null, "expr.getContainingFile() == null"); @@ -766,6 +756,24 @@ public abstract class IntroduceVariableBase extends IntroduceHandlerBase { return wasSucceed[0]; } + protected PsiElement checkAnchorStatement(Project project, Editor editor, PsiElement anchorStatement) { + if (anchorStatement == null) { + String message = RefactoringBundle.message("refactoring.is.not.supported.in.the.current.context", REFACTORING_NAME); + showErrorMessage(project, editor, message); + return null; + } + if (checkAnchorBeforeThisOrSuper(project, editor, anchorStatement, REFACTORING_NAME, HelpID.INTRODUCE_VARIABLE)) return null; + + final PsiElement tempContainer = anchorStatement.getParent(); + + if (!(tempContainer instanceof PsiCodeBlock) && !RefactoringUtil.isLoopOrIf(tempContainer) && !(tempContainer instanceof PsiLambdaExpression) && (tempContainer.getParent() instanceof PsiLambdaExpression)) { + String message = RefactoringBundle.message("refactoring.is.not.supported.in.the.current.context", REFACTORING_NAME); + showErrorMessage(project, editor, message); + return null; + } + return tempContainer; + } + protected JavaReplaceChoice getOccurrencesChoice() { return null; } @@ -1110,12 +1118,6 @@ public abstract class IntroduceVariableBase extends IntroduceHandlerBase { : factory.createExpressionFromText(text, parent); } - private boolean parentStatementNotFound(final Project project, Editor editor) { - String message = RefactoringBundle.message("refactoring.is.not.supported.in.the.current.context", REFACTORING_NAME); - showErrorMessage(project, editor, message); - return false; - } - protected boolean invokeImpl(Project project, PsiLocalVariable localVariable, Editor editor) { throw new UnsupportedOperationException(); } diff --git a/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterExpressionInLoopNoVars.java b/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterExpressionInLoopNoVars.java new file mode 100644 index 000000000000..623367381340 --- /dev/null +++ b/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterExpressionInLoopNoVars.java @@ -0,0 +1,14 @@ +import java.util.function.Supplier; + +class Test { + void foo() { + if (true) { + Supplier stringSupplier = new Supplier() { + public String get() { + return "Hello, world"; + } + }; + System.out.println(stringSupplier.get()); + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterExpressionNoVarsSelected.java b/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterExpressionNoVarsSelected.java new file mode 100644 index 000000000000..6b0cf66e8d5e --- /dev/null +++ b/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterExpressionNoVarsSelected.java @@ -0,0 +1,12 @@ +import java.util.function.Supplier; + +class Test { + void foo() { + Supplier stringSupplier = new Supplier() { + public String get() { + return "Hello, world"; + } + }; + System.out.println(stringSupplier.get()); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterPassFieldsAsParameters.java b/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterPassFieldsAsParameters.java new file mode 100644 index 000000000000..efc2cc7829b6 --- /dev/null +++ b/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterPassFieldsAsParameters.java @@ -0,0 +1,15 @@ +import java.util.function.Consumer; + +class Test { + String myName; + void foo() { + if (true) { + Consumer stringConsumer = new Consumer() { + public void accept(String myName) { + System.out.println("Hello, world " + myName); + } + }; + stringConsumer.accept(myName); + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterSkipUsedLocals.java b/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterSkipUsedLocals.java new file mode 100644 index 000000000000..bbf61ac0ffb9 --- /dev/null +++ b/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterSkipUsedLocals.java @@ -0,0 +1,11 @@ +class Test { + void foo(String name) { + System.out.println("Hello, "); + Runnable runnable = new Runnable() { + public void run() { + System.out.println(name); + } + }; + runnable.run(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterStatementInLoop.java b/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterStatementInLoop.java new file mode 100644 index 000000000000..d37ee07557f9 --- /dev/null +++ b/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterStatementInLoop.java @@ -0,0 +1,14 @@ +import java.util.function.Consumer; + +class Test { + void foo(String s) { + if (true) { + Consumer stringConsumer = new Consumer() { + public void accept(String s) { + System.out.println("Hello, world " + s); + } + }; + stringConsumer.accept(s); + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterStatements.java b/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterStatements.java new file mode 100644 index 000000000000..a92ac6ec3dfe --- /dev/null +++ b/java/java-tests/testData/refactoring/introduceFunctionalVariable/afterStatements.java @@ -0,0 +1,17 @@ +import java.util.function.Consumer; + +class Test { + void foo(String s) { + if (true) { + Consumer stringConsumer = new Consumer() { + public void accept(String s) { + System.out.println("Hello, world " + s); + System.out.println(); + } + }; + stringConsumer.accept(s); + + System.out.println(); + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforeExpressionInLoopNoVars.java b/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforeExpressionInLoopNoVars.java new file mode 100644 index 000000000000..74675b60043b --- /dev/null +++ b/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforeExpressionInLoopNoVars.java @@ -0,0 +1,5 @@ +class Test { + void foo() { + if (true) System.out.println("Hello, world"); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforeExpressionNoVarsSelected.java b/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforeExpressionNoVarsSelected.java new file mode 100644 index 000000000000..c755dfd23d3d --- /dev/null +++ b/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforeExpressionNoVarsSelected.java @@ -0,0 +1,5 @@ +class Test { + void foo() { + System.out.println("Hello, world"); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforePassFieldsAsParameters.java b/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforePassFieldsAsParameters.java new file mode 100644 index 000000000000..1f18528ac212 --- /dev/null +++ b/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforePassFieldsAsParameters.java @@ -0,0 +1,6 @@ +class Test { + String myName; + void foo() { + if (true) System.out.println("Hello, world " + myName); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforeSkipUsedLocals.java b/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforeSkipUsedLocals.java new file mode 100644 index 000000000000..a6c4549b0e0e --- /dev/null +++ b/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforeSkipUsedLocals.java @@ -0,0 +1,6 @@ +class Test { + void foo(String name) { + System.out.println("Hello, "); + System.out.println(name); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforeStatementInLoop.java b/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforeStatementInLoop.java new file mode 100644 index 000000000000..46fdef821b81 --- /dev/null +++ b/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforeStatementInLoop.java @@ -0,0 +1,5 @@ +class Test { + void foo(String s) { + if (true) System.out.println("Hello, world " + s); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforeStatements.java b/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforeStatements.java new file mode 100644 index 000000000000..238ba4690a0e --- /dev/null +++ b/java/java-tests/testData/refactoring/introduceFunctionalVariable/beforeStatements.java @@ -0,0 +1,10 @@ +class Test { + void foo(String s) { + if (true) { + System.out.println("Hello, world " + s); + System.out.println(); + + System.out.println(); + } + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/refactoring/IntroduceFunctionalVariableTest.java b/java/java-tests/testSrc/com/intellij/java/refactoring/IntroduceFunctionalVariableTest.java new file mode 100644 index 000000000000..21aaaa4c4269 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/java/refactoring/IntroduceFunctionalVariableTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2000-2017 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 com.intellij.java.refactoring; + +import com.intellij.JavaTestUtil; +import com.intellij.pom.java.LanguageLevel; +import com.intellij.refactoring.extractMethod.ExtractMethodProcessor; +import com.intellij.refactoring.extractMethod.InputVariables; +import com.intellij.refactoring.introduceVariable.IntroduceFunctionalVariableHandler; +import com.intellij.testFramework.MapDataContext; +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.annotations.NotNull; + +@TestDataPath("$CONTENT_ROOT/testData") +public class IntroduceFunctionalVariableTest extends LightRefactoringTestCase { + + @NotNull + @Override + protected String getTestDataPath() { + return JavaTestUtil.getJavaTestDataPath(); + } + + @Override + protected LanguageLevel getLanguageLevel() { + return LanguageLevel.JDK_1_8; + } + + public void testExpressionNoVarsSelected() throws Exception { + doTest(); + } + + public void testExpressionInLoopNoVars() throws Exception { + doTest(); + } + + public void testStatementInLoop() throws Exception { + doTest(); + } + + public void testStatements() throws Exception { + doTest(); + } + + public void testPassFieldsAsParameters() throws Exception { + doTest(); + } + + public void testSkipUsedLocals() throws Exception { + doTest(0); + } + + private void doTest(int... disableParams) { + boolean enabled = true; + try { + configureByFile("/refactoring/introduceFunctionalVariable/before" + getTestName(false) + ".java"); + enabled = getEditor().getSettings().isVariableInplaceRenameEnabled(); + getEditor().getSettings().setVariableInplaceRenameEnabled(false); + new IntroduceFunctionalVariableHandler() { + @Override + protected void setupProcessorWithoutDialog(ExtractMethodProcessor processor, InputVariables inputVariables) { + inputVariables.setPassFields(true); + super.setupProcessorWithoutDialog(processor, inputVariables); + for (int i : disableParams) { + processor.doNotPassParameter(i); + } + } + }.invoke(getProject(), getEditor(), getFile(), new MapDataContext()); + checkResultByFile("/refactoring/introduceFunctionalVariable/after" + getTestName(false) + ".java"); + } finally { + getEditor().getSettings().setVariableInplaceRenameEnabled(enabled); + } + } +} diff --git a/platform/lang-api/src/com/intellij/lang/refactoring/RefactoringSupportProvider.java b/platform/lang-api/src/com/intellij/lang/refactoring/RefactoringSupportProvider.java index 449a62555746..6531a85ddfa9 100644 --- a/platform/lang-api/src/com/intellij/lang/refactoring/RefactoringSupportProvider.java +++ b/platform/lang-api/src/com/intellij/lang/refactoring/RefactoringSupportProvider.java @@ -101,6 +101,15 @@ public abstract class RefactoringSupportProvider { return null; } + /** + * @return handler for introducing functional locals in this language + * @see ContextAwareActionHandler + * @see RefactoringActionHandler + */ + public RefactoringActionHandler getIntroduceFunctionalVariableHandler() { + return null; + } + /** * @return handler for pulling up members in this language * @see com.intellij.refactoring.RefactoringActionHandler diff --git a/platform/lang-impl/src/com/intellij/refactoring/actions/IntroduceFunctionalVariableAction.java b/platform/lang-impl/src/com/intellij/refactoring/actions/IntroduceFunctionalVariableAction.java new file mode 100644 index 000000000000..e2f5cef8b860 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/refactoring/actions/IntroduceFunctionalVariableAction.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2017 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 com.intellij.refactoring.actions; + +import com.intellij.lang.refactoring.RefactoringSupportProvider; +import com.intellij.psi.PsiElement; +import com.intellij.refactoring.RefactoringActionHandler; +import com.intellij.refactoring.RefactoringBundle; +import org.jetbrains.annotations.NotNull; + +public class IntroduceFunctionalVariableAction extends BasePlatformRefactoringAction { + public static final String REFACTORING_NAME = RefactoringBundle.message("introduce.functional.variable.title"); + + @Override + protected boolean isAvailableInEditorOnly() { + return true; + } + + @Override + protected boolean isEnabledOnElements(@NotNull PsiElement[] elements) { + return false; + } + + @Override + protected RefactoringActionHandler getRefactoringHandler(@NotNull RefactoringSupportProvider provider) { + return provider.getIntroduceFunctionalVariableHandler(); + } +} diff --git a/platform/platform-resources-en/src/messages/ActionsBundle.properties b/platform/platform-resources-en/src/messages/ActionsBundle.properties index f03581f0c9ca..30302cda656e 100644 --- a/platform/platform-resources-en/src/messages/ActionsBundle.properties +++ b/platform/platform-resources-en/src/messages/ActionsBundle.properties @@ -674,7 +674,9 @@ action.IntroduceConstant.description=Replace selected expression with a constant action.IntroduceParameter.text=_Parameter... action.IntroduceParameter.description=Turn the selected expression into method parameter action.IntroduceFunctionalParameter.text=Functiona_l Parameter... -action.IntroduceFunctionalParameter.description=Replace selected statements with a call to new functional method parameter +action.IntroduceFunctionalParameter.description=Replace selected statements with a call to a new functional method parameter +action.IntroduceFunctionalVariable.text=Functional Variable... +action.IntroduceFunctionalVariable.description=Replace selected statements with a call to a new functional variable action.ExtractInterface.text=_Interface... action.ExtractInterface.description=Extract interface from the selected class action.ExtractModule.text=_Module... diff --git a/platform/platform-resources-en/src/messages/RefactoringBundle.properties b/platform/platform-resources-en/src/messages/RefactoringBundle.properties index f43667a758ba..d8951a15ad03 100644 --- a/platform/platform-resources-en/src/messages/RefactoringBundle.properties +++ b/platform/platform-resources-en/src/messages/RefactoringBundle.properties @@ -805,6 +805,7 @@ enter.new.project.name=Enter new project name: rename.project=Rename Project renames.project=Renames project introduce.functional.parameter.title=Extract Functional Parameter +introduce.functional.variable.title=Extract Functional Variable introduce.parameter.convert.lambda=&Convert to functional expression expand.method.reference.warning=Method is used in method reference. Proceeding would result in conversion to lambda expression diff --git a/resources/src/idea/JavaActions.xml b/resources/src/idea/JavaActions.xml index b5d9b81f1566..676ef3dbc23f 100644 --- a/resources/src/idea/JavaActions.xml +++ b/resources/src/idea/JavaActions.xml @@ -79,6 +79,10 @@ + + + +