/*
* 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.introduceVariable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import consulo.annotations.RequiredDispatchThread;
import consulo.annotations.RequiredReadAction;
import consulo.csharp.ide.refactoring.util.CSharpNameSuggesterUtil;
import consulo.csharp.ide.refactoring.util.CSharpRefactoringUtil;
import consulo.csharp.lang.psi.CSharpFile;
import consulo.csharp.lang.psi.CSharpFileFactory;
import consulo.csharp.lang.psi.CSharpLocalVariable;
import consulo.csharp.lang.psi.CSharpLocalVariableDeclarationStatement;
import consulo.csharp.lang.psi.CSharpReferenceExpression;
import consulo.csharp.lang.psi.CSharpTokens;
import consulo.csharp.lang.psi.UsefulPsiTreeUtil;
import consulo.csharp.lang.psi.impl.source.CSharpExpressionStatementImpl;
import consulo.csharp.lang.psi.impl.source.CSharpMethodCallExpressionImpl;
import consulo.dotnet.DotNetTypes;
import consulo.dotnet.psi.DotNetCodeBlockOwner;
import consulo.dotnet.psi.DotNetExpression;
import consulo.dotnet.psi.DotNetStatement;
import consulo.dotnet.psi.DotNetVariable;
import consulo.dotnet.resolve.DotNetTypeRef;
import consulo.dotnet.resolve.DotNetTypeRefUtil;
import com.intellij.codeInsight.CodeInsightUtilCore;
import com.intellij.codeInsight.template.impl.TemplateManagerImpl;
import com.intellij.codeInsight.template.impl.TemplateState;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.CaretModel;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.editor.SelectionModel;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pass;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiNamedElement;
import com.intellij.psi.PsiParserFacade;
import com.intellij.psi.PsiRecursiveElementVisitor;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.refactoring.IntroduceTargetChooser;
import com.intellij.refactoring.RefactoringActionHandler;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.refactoring.introduce.inplace.InplaceVariableIntroducer;
import com.intellij.refactoring.introduce.inplace.OccurrencesChooser;
import com.intellij.refactoring.util.CommonRefactoringUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Function;
@SuppressWarnings("MethodMayBeStatic")
public abstract class CSharpIntroduceHandler implements RefactoringActionHandler
{
@Nullable
protected static PsiElement findAnchor(PsiElement occurrence)
{
DotNetStatement statement = PsiTreeUtil.getParentOfType(occurrence, DotNetStatement.class);
if(statement != null)
{
return statement;
}
return findAnchor(Arrays.asList(occurrence));
}
@Nullable
protected static PsiElement findAnchor(List<PsiElement> occurrences)
{
if(occurrences.isEmpty())
{
return null;
}
int minOffset = Integer.MAX_VALUE;
for(PsiElement element : occurrences)
{
minOffset = Math.min(minOffset, element.getTextOffset());
}
DotNetStatement statements = findContainingStatements(occurrences);
if(statements == null)
{
return null;
}
PsiElement child = null;
PsiElement[] children = statements.getChildren();
for(PsiElement aChildren : children)
{
child = aChildren;
if(child.getTextRange().contains(minOffset))
{
break;
}
}
return child;
}
@Nullable
private static DotNetStatement findContainingStatements(List<PsiElement> occurrences)
{
DotNetStatement result = PsiTreeUtil.getParentOfType(occurrences.get(0), DotNetStatement.class, true);
while(result != null && !UsefulPsiTreeUtil.isAncestor(result, occurrences, true))
{
result = PsiTreeUtil.getParentOfType(result, DotNetStatement.class, true);
}
return result;
}
protected final String myDialogTitle;
public CSharpIntroduceHandler(@NotNull final String dialogTitle)
{
myDialogTitle = dialogTitle;
}
@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file, DataContext dataContext)
{
performAction(new CSharpIntroduceOperation(project, editor, file, null));
}
@Override
public void invoke(@NotNull Project project, @NotNull PsiElement[] elements, DataContext dataContext)
{
}
@RequiredReadAction
public void performAction(CSharpIntroduceOperation operation)
{
final PsiFile file = operation.getFile();
if(!CommonRefactoringUtil.checkReadOnlyStatus(file))
{
return;
}
final Editor editor = operation.getEditor();
if(editor.getSettings().isVariableInplaceRenameEnabled())
{
final TemplateState templateState = TemplateManagerImpl.getTemplateState(operation.getEditor());
if(templateState != null && !templateState.isFinished())
{
return;
}
}
PsiElement element1 = null;
PsiElement element2 = null;
final SelectionModel selectionModel = editor.getSelectionModel();
if(selectionModel.hasSelection())
{
element1 = file.findElementAt(selectionModel.getSelectionStart());
element2 = file.findElementAt(selectionModel.getSelectionEnd() - 1);
if(element1 instanceof PsiWhiteSpace)
{
int startOffset = element1.getTextRange().getEndOffset();
element1 = file.findElementAt(startOffset);
}
if(element2 instanceof PsiWhiteSpace)
{
int endOffset = element2.getTextRange().getStartOffset();
element2 = file.findElementAt(endOffset - 1);
}
}
else
{
if(smartIntroduce(operation))
{
return;
}
final CaretModel caretModel = editor.getCaretModel();
final Document document = editor.getDocument();
int lineNumber = document.getLineNumber(caretModel.getOffset());
if((lineNumber >= 0) && (lineNumber < document.getLineCount()))
{
element1 = file.findElementAt(document.getLineStartOffset(lineNumber));
element2 = file.findElementAt(document.getLineEndOffset(lineNumber) - 1);
}
}
final Project project = operation.getProject();
if(element1 == null || element2 == null)
{
showCannotPerformError(project, editor);
return;
}
element1 = CSharpRefactoringUtil.getSelectedExpression(project, file, element1, element2);
if(element1 == null)
{
showCannotPerformError(project, editor);
return;
}
if(!checkIntroduceContext(file, editor, element1))
{
return;
}
operation.setElement(element1);
performActionOnElement(operation);
}
protected boolean checkIntroduceContext(PsiFile file, Editor editor, PsiElement element)
{
return true;
}
private void showCannotPerformError(Project project, Editor editor)
{
CommonRefactoringUtil.showErrorHint(project, editor, RefactoringBundle.message("refactoring.introduce.selection.error"), myDialogTitle, "refactoring.extractMethod");
}
@RequiredReadAction
private boolean smartIntroduce(final CSharpIntroduceOperation operation)
{
final Editor editor = operation.getEditor();
final PsiFile file = operation.getFile();
int offset = editor.getCaretModel().getOffset();
PsiElement temp = file.findElementAt(offset);
assert temp != null;
if(!checkIntroduceContext(file, editor, temp))
{
return true;
}
if(temp instanceof PsiWhiteSpace)
{
temp = PsiTreeUtil.prevLeaf(temp);
}
final List<DotNetExpression> expressions = new ArrayList<DotNetExpression>();
// int var = 1;<caret>
if(PsiUtilCore.getElementType(temp) == CSharpTokens.SEMICOLON)
{
PsiElement parent = temp.getParent();
if(parent instanceof CSharpLocalVariableDeclarationStatement)
{
CSharpLocalVariable[] variables = ((CSharpLocalVariableDeclarationStatement) parent).getVariables();
CSharpLocalVariable lastElement = ArrayUtil.getLastElement(variables);
if(lastElement != null)
{
temp = lastElement.getInitializer();
}
}
else if(parent instanceof CSharpExpressionStatementImpl)
{
temp = ((CSharpExpressionStatementImpl) parent).getExpression();
}
}
while(temp != null)
{
if(temp instanceof CSharpFile)
{
break;
}
if(temp instanceof DotNetExpression)
{
if(temp instanceof CSharpReferenceExpression)
{
CSharpReferenceExpression.ResolveToKind kind = ((CSharpReferenceExpression) temp).kind();
if(kind == CSharpReferenceExpression.ResolveToKind.TYPE_LIKE || kind == CSharpReferenceExpression.ResolveToKind.CONSTRUCTOR)
{
temp = temp.getParent();
continue;
}
PsiElement parent = temp.getParent();
if(parent instanceof CSharpMethodCallExpressionImpl && ((CSharpMethodCallExpressionImpl) parent).getCallExpression() == temp)
{
temp = temp.getParent();
continue;
}
}
DotNetTypeRef typeRef = ((DotNetExpression) temp).toTypeRef(true);
if(DotNetTypeRefUtil.isVmQNameEqual(typeRef, file, DotNetTypes.System.Void))
{
break;
}
expressions.add((DotNetExpression) temp);
}
temp = temp.getParent();
}
if(expressions.isEmpty())
{
PsiElement someElement = UsefulPsiTreeUtil.getPrevSiblingSkipWhiteSpaces(file.findElementAt(offset), false);
if(someElement instanceof CSharpExpressionStatementImpl)
{
expressions.add(((CSharpExpressionStatementImpl) someElement).getExpression());
}
}
if(expressions.isEmpty())
{
showCannotPerformError(file.getProject(), editor);
return false;
}
if(expressions.size() == 1 || ApplicationManager.getApplication().isUnitTestMode())
{
operation.setElement(expressions.get(0));
performActionOnElement(operation);
return true;
}
else if(expressions.size() > 1)
{
IntroduceTargetChooser.showChooser(editor, expressions, new Pass<DotNetExpression>()
{
@Override
public void pass(DotNetExpression expression)
{
operation.setElement(expression);
performActionOnElement(operation);
}
}, new Function<DotNetExpression, String>()
{
@Override
public String fun(DotNetExpression expression)
{
return expression.getText();
}
}
);
return true;
}
return false;
}
private void performActionOnElement(CSharpIntroduceOperation operation)
{
if(!checkEnabled(operation))
{
return;
}
final PsiElement element = operation.getElement();
final DotNetExpression initializer = (DotNetExpression) element;
operation.setInitializer(initializer);
operation.setOccurrences(getOccurrences(element, initializer));
operation.setSuggestedNames(getSuggestedNames(initializer));
if(operation.getOccurrences().size() == 0)
{
operation.setReplaceAll(false);
}
performActionOnElementOccurrences(operation);
}
@NotNull
@RequiredReadAction
protected Collection<String> getSuggestedNames(@NotNull DotNetExpression initializer)
{
return CSharpNameSuggesterUtil.getSuggestedNames(initializer);
}
protected void performActionOnElementOccurrences(final CSharpIntroduceOperation operation)
{
final Editor editor = operation.getEditor();
if(editor.getSettings().isVariableInplaceRenameEnabled())
{
ensureName(operation);
if(operation.isReplaceAll() || operation.getOccurrences().isEmpty())
{
performInplaceIntroduce(operation);
}
else
{
OccurrencesChooser.simpleChooser(editor).showChooser(operation.getElement(), operation.getOccurrences(), new Pass<OccurrencesChooser.ReplaceChoice>()
{
@Override
public void pass(OccurrencesChooser.ReplaceChoice replaceChoice)
{
operation.setReplaceAll(replaceChoice == OccurrencesChooser.ReplaceChoice.ALL);
performInplaceIntroduce(operation);
}
});
}
}
else
{
performIntroduceWithDialog(operation);
}
}
protected boolean checkEnabled(CSharpIntroduceOperation operation)
{
return true;
}
protected static void ensureName(CSharpIntroduceOperation operation)
{
if(operation.getName() == null)
{
final Collection<String> suggestedNames = operation.getSuggestedNames();
if(suggestedNames.size() > 0)
{
operation.setName(suggestedNames.iterator().next());
}
else
{
operation.setName("x");
}
}
}
protected List<PsiElement> getOccurrences(PsiElement element, @NotNull final DotNetExpression expression)
{
PsiElement context = PsiTreeUtil.getParentOfType(element, DotNetCodeBlockOwner.class);
if(context == null)
{
context = element;
}
return CSharpRefactoringUtil.getOccurrences(expression, context);
}
protected void performIntroduceWithDialog(CSharpIntroduceOperation operation)
{
final Project project = operation.getProject();
if(operation.getName() == null)
{
CSharpIntroduceDialog dialog = new CSharpIntroduceDialog(project, myDialogTitle, operation);
dialog.show();
if(!dialog.isOK())
{
return;
}
operation.setName(dialog.getName());
operation.setReplaceAll(dialog.doReplaceAllOccurrences());
}
PsiElement declaration = performRefactoring(operation);
if(declaration == null)
{
return;
}
final Editor editor = operation.getEditor();
editor.getCaretModel().moveToOffset(declaration.getTextRange().getEndOffset());
editor.getSelectionModel().removeSelection();
}
protected void performInplaceIntroduce(CSharpIntroduceOperation operation)
{
final PsiElement statement = performRefactoring(operation);
final CSharpLocalVariable target = PsiTreeUtil.findChildOfType(statement, CSharpLocalVariable.class);
final PsiElement nameIdentifier = target != null ? target.getNameIdentifier() : null;
if(nameIdentifier == null)
{
return;
}
final List<PsiElement> occurrences = operation.getOccurrences();
operation.getEditor().getCaretModel().moveToOffset(nameIdentifier.getTextOffset());
final InplaceVariableIntroducer<PsiElement> introducer = createVariableIntroducer(target, operation, occurrences);
introducer.performInplaceRefactoring(new LinkedHashSet<String>(operation.getSuggestedNames()));
}
@NotNull
protected abstract InplaceVariableIntroducer<PsiElement> createVariableIntroducer(CSharpLocalVariable target, CSharpIntroduceOperation operation, List<PsiElement> occurrences);
@Nullable
protected PsiElement performRefactoring(@NotNull CSharpIntroduceOperation operation)
{
PsiElement anchor = operation.isReplaceAll() ? findAnchor(operation.getOccurrences()) : findAnchor(operation.getInitializer());
if(anchor == null)
{
CommonRefactoringUtil.showErrorHint(operation.getProject(), operation.getEditor(), RefactoringBundle.getCannotRefactorMessage(null), RefactoringBundle.getCannotRefactorMessage(null),
null);
return null;
}
PsiElement declaration = createDeclaration(operation);
if(declaration == null)
{
showCannotPerformError(operation.getProject(), operation.getEditor());
return null;
}
declaration = performReplace(declaration, operation);
if(declaration != null)
{
declaration = CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(declaration);
}
return declaration;
}
@Nullable
@RequiredReadAction
public PsiElement createDeclaration(CSharpIntroduceOperation operation)
{
final Project project = operation.getProject();
final DotNetExpression initializer = operation.getInitializer();
InitializerTextBuilder builder = new InitializerTextBuilder();
initializer.accept(builder);
String assignmentText = getDeclarationString(operation, builder.result());
return CSharpFileFactory.createStatement(project, assignmentText);
}
@NotNull
@RequiredReadAction
protected abstract String getDeclarationString(CSharpIntroduceOperation operation, String initExpression);
@Nullable
@RequiredDispatchThread
private PsiElement performReplace(@NotNull final PsiElement declaration, final CSharpIntroduceOperation operation)
{
final DotNetExpression initializer = operation.getInitializer();
final Project project = operation.getProject();
PsiDocumentManager.getInstance(project).commitAllDocuments();
return new WriteCommandAction<PsiElement>(project, declaration.getContainingFile())
{
@Override
protected void run(final Result<PsiElement> result) throws Throwable
{
PsiElement createdDeclaration = addDeclaration(operation, declaration);
boolean needReferenceOfVariable = isNeedReferenceOfVariable(initializer);
if(needReferenceOfVariable)
{
if(createdDeclaration != null)
{
createdDeclaration = modifyDeclaration(createdDeclaration);
}
}
result.setResult(createdDeclaration);
PsiElement newExpression = createExpression(project, operation.getName());
if(operation.isReplaceAll())
{
List<PsiElement> newOccurrences = new ArrayList<PsiElement>();
List<PsiElement> occurrences = operation.getOccurrences();
if(occurrences.size() == 1 && !needReferenceOfVariable)
{
occurrences.get(0).delete();
}
else
{
for(PsiElement occurrence : occurrences)
{
final PsiElement replaced = replaceExpression(occurrence, newExpression, operation);
if(replaced != null)
{
newOccurrences.add(replaced);
}
}
operation.setOccurrences(newOccurrences);
}
}
else
{
if(needReferenceOfVariable)
{
final PsiElement replaced = replaceExpression(initializer, newExpression, operation);
operation.setOccurrences(Collections.singletonList(replaced));
}
else
{
initializer.delete();
}
}
postRefactoring(operation.getElement());
}
}.execute().getResultObject();
}
protected PsiElement modifyDeclaration(@NotNull PsiElement declaration)
{
PsiElement parent = declaration.getParent();
parent.addAfter(PsiParserFacade.SERVICE.getInstance(declaration.getProject()).createWhiteSpaceFromText("\n"), declaration);
return declaration;
}
private boolean isNeedReferenceOfVariable(@NotNull DotNetExpression expression)
{
PsiElement parent = expression.getParent();
if(parent instanceof CSharpExpressionStatementImpl)
{
return ((CSharpExpressionStatementImpl) expression.getParent()).getExpression() != expression;
}
return true;
}
@Nullable
protected DotNetExpression createExpression(Project project, String name)
{
return CSharpFileFactory.createExpression(project, name);
}
@Nullable
protected PsiElement replaceExpression(PsiElement expression, PsiElement newExpression, CSharpIntroduceOperation operation)
{
return expression.replace(newExpression);
}
protected void postRefactoring(PsiElement element)
{
}
@Nullable
public PsiElement addDeclaration(CSharpIntroduceOperation operation, PsiElement declaration)
{
PsiElement anchor = operation.isReplaceAll() ? findAnchor(operation.getOccurrences()) : findAnchor(operation.getInitializer());
if(anchor == null)
{
CommonRefactoringUtil.showErrorHint(operation.getProject(), operation.getEditor(), RefactoringBundle.getCannotRefactorMessage(null), RefactoringBundle.getCannotRefactorMessage(null),
null);
return null;
}
final PsiElement parent = anchor.getParent();
PsiElement psiElement = parent.addBefore(declaration, anchor);
CodeStyleManager.getInstance(declaration.getProject()).reformat(psiElement);
return psiElement;
}
protected static class CSharpInplaceVariableIntroducer extends InplaceVariableIntroducer<PsiElement>
{
public CSharpInplaceVariableIntroducer(CSharpLocalVariable target, CSharpIntroduceOperation operation, List<PsiElement> occurrences)
{
super(target, operation.getEditor(), operation.getProject(), "Introduce Variable", occurrences.toArray(new PsiElement[occurrences.size()]), null);
}
@Override
@RequiredReadAction
protected void moveOffsetAfter(boolean success)
{
super.moveOffsetAfter(success);
if(success)
{
PsiNamedElement variable = getVariable();
if(variable instanceof DotNetVariable && variable.isValid())
{
myEditor.getCaretModel().moveToOffset(getVariableEndOffset((DotNetVariable) variable));
myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
}
}
}
@RequiredReadAction
protected int getVariableEndOffset(DotNetVariable variable)
{
return variable.getTextRange().getEndOffset();
}
@Override
protected PsiElement checkLocalScope()
{
return myElementToRename.getContainingFile();
}
}
private static class InitializerTextBuilder extends PsiRecursiveElementVisitor
{
private final StringBuilder myResult = new StringBuilder();
@Override
public void visitWhiteSpace(PsiWhiteSpace space)
{
myResult.append(space.getText().replace('\n', ' '));
}
@Override
public void visitElement(PsiElement element)
{
if(element.getChildren().length == 0)
{
myResult.append(element.getText());
}
else
{
super.visitElement(element);
}
}
public String result()
{
return myResult.toString();
}
}
}