/* * Copyright 2013-2017 consulo.io * * 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 consulo.csharp.ide.refactoring.extractMethod; import java.util.List; import java.util.Set; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import consulo.csharp.ide.codeInsight.actions.MethodGenerateUtil; import consulo.csharp.ide.msil.representation.builder.CSharpStubBuilderVisitor; import consulo.csharp.ide.refactoring.changeSignature.CSharpMethodDescriptor; import consulo.csharp.lang.psi.CSharpFileFactory; import consulo.csharp.lang.psi.CSharpLocalVariable; import consulo.csharp.lang.psi.CSharpModifier; import consulo.csharp.lang.psi.CSharpRecursiveElementVisitor; import consulo.csharp.lang.psi.CSharpReferenceExpression; import consulo.csharp.lang.psi.CSharpSimpleLikeMethodAsElement; import consulo.csharp.lang.psi.UsefulPsiTreeUtil; import consulo.csharp.lang.psi.impl.light.builder.CSharpLightMethodDeclarationBuilder; import consulo.csharp.lang.psi.impl.light.builder.CSharpLightParameterBuilder; import consulo.csharp.lang.psi.impl.source.CSharpAssignmentExpressionImpl; import consulo.csharp.lang.psi.impl.source.CSharpBlockStatementImpl; import consulo.csharp.lang.psi.impl.source.CSharpReturnStatementImpl; import consulo.csharp.lang.psi.impl.source.resolve.type.CSharpTypeRefByQName; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.SelectionModel; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiParserFacade; import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.codeStyle.CodeStyleManager; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.refactoring.RefactoringActionHandler; import com.intellij.refactoring.RefactoringBundle; import com.intellij.refactoring.util.CommonRefactoringUtil; import com.intellij.util.ArrayUtil; import com.intellij.util.PairFunction; import com.intellij.util.Processor; import com.intellij.util.containers.ArrayListSet; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.MultiMap; import consulo.annotations.RequiredDispatchThread; import consulo.annotations.RequiredReadAction; import consulo.annotations.RequiredWriteAction; import consulo.dotnet.DotNetTypes; import consulo.dotnet.psi.DotNetExpression; import consulo.dotnet.psi.DotNetLikeMethodDeclaration; import consulo.dotnet.psi.DotNetLocalVariable; import consulo.dotnet.psi.DotNetModifierListOwner; import consulo.dotnet.psi.DotNetParameter; import consulo.dotnet.psi.DotNetQualifiedElement; import consulo.dotnet.psi.DotNetStatement; import consulo.dotnet.psi.DotNetVariable; import consulo.dotnet.resolve.DotNetTypeRef; import consulo.dotnet.resolve.DotNetTypeRefUtil; import consulo.internal.dotnet.msil.decompiler.textBuilder.block.StubBlock; import consulo.internal.dotnet.msil.decompiler.textBuilder.util.StubBlockUtil; /** * @author VISTALL * @since 07.11.2015 */ public class CSharpExtractMethodHandler implements RefactoringActionHandler { @Deprecated private static final DotNetStatement[] EMPTY_ARRAY = new DotNetStatement[0]; @Override public void invoke(@NotNull Project project, @NotNull PsiElement[] elements, DataContext dataContext) { } @Override @RequiredDispatchThread public void invoke(@NotNull final Project project, final Editor editor, final PsiFile file, DataContext dataContext) { PsiDocumentManager.getInstance(project).commitAllDocuments(); final SelectionModel selectionModel = editor.getSelectionModel(); if(!selectionModel.hasSelection()) { selectionModel.selectLineAtCaret(); } final DotNetStatement[] statements = getStatements(file, selectionModel.getSelectionStart(), selectionModel.getSelectionEnd()); if(statements.length == 0) { CommonRefactoringUtil.showErrorHint(project, editor, RefactoringBundle.getCannotRefactorMessage("No statements"), "Extract Method", null); return; } final CSharpSimpleLikeMethodAsElement methodAsElement = PsiTreeUtil.getParentOfType(statements[0], CSharpSimpleLikeMethodAsElement.class); if(methodAsElement == null) { CommonRefactoringUtil.showErrorHint(project, editor, RefactoringBundle.getCannotRefactorMessage("No parent method"), "Extract Method", null); return; } final DotNetQualifiedElement qualifiedElement = PsiTreeUtil.getParentOfType(statements[0], DotNetQualifiedElement.class); if(qualifiedElement == null) { CommonRefactoringUtil.showErrorHint(project, editor, RefactoringBundle.getCannotRefactorMessage("No parent method"), "Extract Method", null); return; } final TextRange extractRange = new TextRange(statements[0].getTextRange().getStartOffset(), statements[statements.length - 1].getTextRange().getEndOffset()); final MultiMap<DotNetVariable, CSharpReferenceExpression> variables = MultiMap.createLinkedSet(); final Set<DotNetVariable> assignmentVariables = new ArrayListSet<DotNetVariable>(); final Ref<DotNetTypeRef> returnTypeRef = Ref.create(); for(DotNetStatement statement : statements) { statement.accept(new CSharpRecursiveElementVisitor() { @Override public void visitReturnStatement(CSharpReturnStatementImpl statement) { DotNetExpression expression = statement.getExpression(); if(expression != null) { returnTypeRef.set(methodAsElement.getReturnTypeRef()); } } @Override public void visitAssignmentExpression(CSharpAssignmentExpressionImpl expression) { super.visitAssignmentExpression(expression); DotNetExpression[] parameterExpressions = expression.getParameterExpressions(); if(parameterExpressions.length > 0) { DotNetExpression parameterExpression = parameterExpressions[0]; if(parameterExpression instanceof CSharpReferenceExpression) { PsiElement resolvedElement = ((CSharpReferenceExpression) parameterExpression).resolve(); if(resolvedElement instanceof DotNetLocalVariable || resolvedElement instanceof DotNetParameter) { assignmentVariables.add((DotNetVariable) resolvedElement); } } } } @Override public void visitReferenceExpression(CSharpReferenceExpression expression) { super.visitReferenceExpression(expression); if(expression.getQualifier() != null) { return; } PsiElement resolvedElement = expression.resolve(); // parameters always extracted as new parameter if(resolvedElement instanceof DotNetParameter) { variables.putValue((DotNetVariable) resolvedElement, expression); } else if(resolvedElement instanceof CSharpLocalVariable) { if(!extractRange.contains(resolvedElement.getTextOffset())) { variables.putValue((DotNetVariable) resolvedElement, expression); } } } }); } CSharpLightMethodDeclarationBuilder builder = new CSharpLightMethodDeclarationBuilder(project); builder.withReturnType(returnTypeRef.get() == null ? new CSharpTypeRefByQName(file, DotNetTypes.System.Void) : returnTypeRef.get()); builder.addModifier(CSharpModifier.PRIVATE); if(qualifiedElement instanceof DotNetModifierListOwner && ((DotNetModifierListOwner) qualifiedElement).hasModifier(CSharpModifier.STATIC)) { builder.addModifier(CSharpModifier.STATIC); } builder.withName(""); for(DotNetVariable variable : variables.keySet()) { CSharpLightParameterBuilder parameterBuilder = new CSharpLightParameterBuilder(project); if(assignmentVariables.contains(variable)) { parameterBuilder.addModifier(CSharpModifier.REF); } parameterBuilder.withName(variable.getName()); parameterBuilder.withTypeRef(variable.toTypeRef(true)); builder.addParameter(parameterBuilder); } CSharpMethodDescriptor descriptor = new CSharpMethodDescriptor(builder); new CSharpExtractMethodDialog(project, descriptor, false, statements[0], new Processor<DotNetLikeMethodDeclaration>() { @Override public boolean process(final DotNetLikeMethodDeclaration builder) { final Document document = PsiDocumentManager.getInstance(project).getDocument(file); assert document != null; new WriteCommandAction.Simple<Object>(project, "Extract method", file) { @Override @RequiredWriteAction protected void run() throws Throwable { selectionModel.removeSelection(); String text = document.getText(extractRange); document.deleteString(extractRange.getStartOffset(), extractRange.getEndOffset()); StringBuilder callStatementBuilder = new StringBuilder(); if(returnTypeRef.get() != null && !(UsefulPsiTreeUtil.getNextSiblingSkippingWhiteSpacesAndComments(ArrayUtil.getLastElement(statements)) instanceof DotNetStatement)) { callStatementBuilder.append("return "); } callStatementBuilder.append(builder.getName()); callStatementBuilder.append("("); StubBlockUtil.join(callStatementBuilder, variables.keySet().toArray(new DotNetVariable[]{}), new PairFunction<StringBuilder, DotNetVariable, Void>() { @Nullable @Override public Void fun(StringBuilder stringBuilder, DotNetVariable o) { if(assignmentVariables.contains(o)) { stringBuilder.append("ref "); } stringBuilder.append(o.getName()); return null; } }, ", "); callStatementBuilder.append(");"); document.insertString(extractRange.getStartOffset(), callStatementBuilder); CharSequence methodText = buildText(builder, statements, text); // insert method PsiElement qualifiedParent = qualifiedElement.getParent(); DotNetLikeMethodDeclaration method = CSharpFileFactory.createMethod(project, methodText); PsiDocumentManager.getInstance(project).commitDocument(document); qualifiedParent.addAfter(PsiParserFacade.SERVICE.getInstance(file.getProject()).createWhiteSpaceFromText("\n\n"), qualifiedElement); PsiElement nextSibling = qualifiedElement.getNextSibling(); PsiElement newMethod = qualifiedParent.addAfter(method, nextSibling); PsiDocumentManager.getInstance(getProject()).doPostponedOperationsAndUnblockDocument(editor.getDocument()); PsiDocumentManager.getInstance(project).commitDocument(document); CodeStyleManager.getInstance(getProject()).reformat(newMethod); } }.execute(); return true; } }).show(); } @RequiredReadAction public static CharSequence buildText(@NotNull DotNetLikeMethodDeclaration methodDeclaration, DotNetStatement[] statements, @NotNull String statementsText) { List<StubBlock> stubBlocks = CSharpStubBuilderVisitor.buildBlocks(methodDeclaration, false); StringBuilder builder = (StringBuilder) DeprecatedStubBlockUtil.buildText(stubBlocks); builder.append("{\n"); builder.append(statementsText); if(!(statements[statements.length - 1] instanceof CSharpReturnStatementImpl) && !DotNetTypeRefUtil.isVmQNameEqual(methodDeclaration.getReturnTypeRef(), statements[0], DotNetTypes.System.Void)) { String defaultValueForType = MethodGenerateUtil.getDefaultValueForType(methodDeclaration.getReturnTypeRef(), statements[0]); if(defaultValueForType != null) { builder.append("\nreturn ").append(defaultValueForType).append(";"); } } builder.append("}"); return builder; } @RequiredReadAction private DotNetStatement[] getStatements(PsiFile file, int startOffset, int endOffset) { Set<DotNetStatement> set = new ArrayListSet<DotNetStatement>(); PsiElement element1 = file.findElementAt(startOffset); PsiElement element2 = file.findElementAt(endOffset - 1); if(element1 instanceof PsiWhiteSpace) { startOffset = element1.getTextRange().getEndOffset(); element1 = file.findElementAt(startOffset); } if(element2 instanceof PsiWhiteSpace) { endOffset = element2.getTextRange().getStartOffset(); element2 = file.findElementAt(endOffset - 1); } PsiElement statement1 = getTopmostParentOfType(element1, DotNetStatement.class); if(statement1 == null) { return EMPTY_ARRAY; } PsiElement statement2 = getTopmostParentOfType(element2, DotNetStatement.class); if(statement2 == null) { return EMPTY_ARRAY; } PsiElement temp = statement1; while(temp != null) { if(temp instanceof DotNetStatement) { set.add((DotNetStatement) temp); } if(temp == statement2) { return ContainerUtil.toArray(set, EMPTY_ARRAY); } temp = temp.getNextSibling(); } return EMPTY_ARRAY; } @Nullable @Contract("null, _ -> null") public static <T extends PsiElement> T getTopmostParentOfType(@Nullable PsiElement element, @NotNull Class<T> aClass) { T answer = PsiTreeUtil.getParentOfType(element, aClass); do { T next = PsiTreeUtil.getParentOfType(answer, aClass); if(next == null) { break; } if(next instanceof CSharpBlockStatementImpl && next.getParent() instanceof DotNetLikeMethodDeclaration) { return answer; } answer = next; } while(true); return answer; } }