/*
* Copyright 2013 Guidewire Software, Inc.
*/
package gw.plugin.ij.codeInsight;
import gw.plugin.ij.util.GosuModuleUtil;
import gw.lang.reflect.TypeSystem;
import com.intellij.psi.PsiFile;
import com.intellij.psi.SmartPsiElementPointer;
import com.intellij.codeInsight.generation.OverrideImplementUtil;
import com.intellij.codeInsight.generation.PsiMethodMember;
import com.intellij.ide.util.MemberChooser;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.impl.source.PsiClassReferenceType;
import com.intellij.psi.impl.source.tree.LeafPsiElement;
import com.intellij.psi.infos.CandidateInfo;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.containers.hash.LinkedHashMap;
import gw.plugin.ij.lang.parser.GosuElementTypes;
import gw.plugin.ij.lang.psi.IGosuPsiElement;
import gw.plugin.ij.lang.psi.api.statements.typedef.IGosuTypeDefinition;
import gw.plugin.ij.util.ClassLord;
import gw.plugin.ij.util.GosuProperties;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
public class GosuOverrideImplementUtil {
private static final Logger LOG = Logger.getInstance("gw.plugin.ij.codeInsight.GosuOverrideImplementUtil");
private static final String JAVA_LANG_OVERRIDE = "java.lang.Override";
private static int adjustOffsetForLBrace(@NotNull IGosuTypeDefinition aClass, int offset) {
LeafPsiElement lBrace = aClass.getGosuLBrace();
if (lBrace == null) {
// throw new UnsupportedOperationException("men at work");
return offset;
}
int lBraceOffset = lBrace.getTextOffset();
return offset < lBraceOffset ? lBraceOffset + 1 : offset;
}
private static void moveCaret(@NotNull Editor editor, int offset) {
editor.getCaretModel().moveToOffset(offset);
editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
editor.getSelectionModel().removeSelection();
}
public static void invokeOverrideImplement(@NotNull final Editor editor, @NotNull final PsiFile file, final boolean isImplement) {
final int offset = editor.getCaretModel().getOffset();
final IGosuTypeDefinition aClass = PsiTreeUtil.findElementOfClassAtOffset(file, offset, IGosuTypeDefinition.class, false);
if (aClass != null) {
final int usableOffset = adjustOffsetForLBrace(aClass, offset);
final ASTNode locationNode = findImmediateChildAtOffset(aClass.getNode(), usableOffset);
final PsiElement location = locationNode != null ? locationNode.getPsi() : null;
if (location != null) {
invokeOverrideImplement(editor, aClass, location, isImplement);
}
}
}
public static void invokeOverrideImplement(@NotNull final Editor editor, @NotNull final IGosuTypeDefinition aClass, final boolean isImplement) {
ASTNode locationNode = findLastChildBefore(aClass.getNode(), GosuElementTypes.METHOD_DEFINITION, GosuElementTypes.CLASS_DEFINITION);
locationNode = findPriorNonDocNonWSNode(locationNode);
PsiElement location = locationNode != null ? locationNode.getPsi() : null;
// XXX should/can 'isImplement' be dynamically computed?
if (location != null) {
invokeOverrideImplement(editor, aClass, location, true);
}
}
public static void invokeOverrideImplement(@NotNull final Editor editor, @NotNull final IGosuTypeDefinition aClass, @NotNull PsiElement location, final boolean isImplement) {
if (isImplement && aClass.isInterface()) {
return;
}
Collection<CandidateInfo> candidates = getMethodsToOverrideImplement(aClass, isImplement);
Collection<CandidateInfo> secondary = isImplement ? Collections.<CandidateInfo>emptyList() : getMethodsToOverrideImplement(aClass, !isImplement);
final MemberChooser<PsiMethodMember> chooser = OverrideImplementUtil.showOverrideImplementChooser(editor, aClass, isImplement, candidates, secondary);
if (chooser == null) {
return;
}
final List<PsiMethodMember> selectedElements = chooser.getSelectedElements();
if (selectedElements == null || selectedElements.isEmpty()) {
return;
}
// //========
// for (PsiMethodMember methodMember : selectedElements) {
// final PsiMethod method = methodMember.getElement();
// IModule module = GosuModuleUtil.findModuleForPsiElement(method);
// final GosuMethodBaseImpl methodDecl = (GosuMethodBaseImpl) GosuPsiParseUtil.parseDeclaration("function test() {}", method.getManager(), module);
//
// CodeEditUtil.setOldIndentation(methodDecl.getNode(), 1);
//
//
// methodDecl.getNameIdentifier().replace(method.getNameIdentifier().copy());
//
//
// for(PsiParameter param : method.getParameterList().getParameters()) {
// //methodDecl.getParameterList().addParameterToHead();
// }
//
//
//
// ApplicationManager.getApplication().runWriteAction(new Runnable() {
// public void run() {
// aClass.add(methodDecl);
//
// CodeStyleManager manager = CodeStyleManager.getInstance(method.getProject());
// manager.reformat(methodDecl);
//
// }
// });
// }
final PsiFile file = aClass.getContainingFile();
final StringBuilder sb = new StringBuilder();
final Map<String, String> imports = new LinkedHashMap<>();
// IGosuFile gosuFile = (IGosuFile) aClass.getContainingFile();
// for(String fqn : imports) {
// gosuFile.addImport(fqn);
// }
// XXX entire location computation should be inside a write action...
// XXX replace this with true psi node insertion, coming soon.
final int offset = location.getTextRange().getStartOffset();
final int prefix = location.getNode().getTreePrev().getTextLength();
final int suffix = location.getTextRange().getLength();
final PsiElement[] caretNode = new PsiElement[1];
ApplicationManager.getApplication().runWriteAction(new Runnable() {
public void run() {
sb.append("\n");
for (PsiMethodMember methodMember : selectedElements) {
sb.append(generateImplementation(editor, file, aClass, methodMember.getElement(), methodMember.getSubstitutor(), imports));
}
sb.setLength(sb.length() - 2);
editor.getDocument().insertString(offset, sb.toString());
PsiDocumentManager.getInstance(file.getProject()).commitDocument(editor.getDocument());
ASTNode firstMethodNode = PsiTreeUtil.findElementOfClassAtOffset(file, offset + 1, PsiElement.class, true).getNode();
ASTNode statementList = firstMethodNode.getTreeParent().findChildByType(GosuElementTypes.ELEM_TYPE_StatementList);
caretNode[0] = statementList.getFirstChildNode().getPsi();
}
});
ApplicationManager.getApplication().runWriteAction(new Runnable() {
public void run() {
//reformat only the inserted region plus adjacent elements on either side.
CodeStyleManager.getInstance(file.getProject()).reformatRange(file, offset - prefix, offset + sb.length() + suffix);
PsiDocumentManager.getInstance(file.getProject()).commitDocument(editor.getDocument());
moveCaret(editor, caretNode[0].getTextOffset() + 1);
TypeSystem.getByFullNameIfValid(aClass.getQualifiedName(), GosuModuleUtil.findModuleForPsiElement(file));
for (String fqn : imports.values()) {
ClassLord.doImport(fqn, file);
}
}
});
}
public static String generateImplementation(@Nullable final Editor editor,
final PsiFile file,
final IGosuTypeDefinition aClass,
@NotNull final PsiMethod method,
@NotNull final PsiSubstitutor substitutor,
final Map<String, String> imports) {
final boolean isAbstract = method.hasModifierProperty(PsiModifier.ABSTRACT);
final StringBuilder sb = new StringBuilder();
sb.append("override ");
final PsiModifierList modifierList = method.getModifierList();
for (Object o : modifierList.getAnnotations()) {
//TODO cgross implement annotations
}
//TODO cgross should match up with introspector
// Name
final String getterName = GosuProperties.getGetterName(method);
final String setterName = GosuProperties.getSetterName(method);
if (getterName != null) {
sb.append("property get ");
sb.append(getterName);
} else if (setterName != null) {
sb.append("property set ");
sb.append(setterName);
} else {
sb.append("function ");
sb.append(method.getName());
}
// Type Parameters
final PsiTypeParameter[] typeParameters = method.getTypeParameters();
if (typeParameters.length > 0) {
sb.append('<');
for (int i = 0; i < typeParameters.length; ++i) {
if (i > 0) {
sb.append(',');
}
sb.append(typeParameters[i].getName());
}
sb.append('>');
}
// Parameters
sb.append("(");
final PsiParameter[] parameters = method.getParameterList().getParameters();
for (int i = 0; i < parameters.length; i++) {
if (i > 0) {
sb.append(',');
}
final PsiParameter parameter = parameters[i];
final String parameterName = parameter.getName();
sb.append(parameterName != null ? parameterName : "p" + i);
sb.append(':');
sb.append(simplifyTypes(substitutor.substitute(parameter.getType()), file, imports).replace("...", "[]"));
}
sb.append(")");
// Return Type
final PsiType returnType = substitutor.substitute(method.getReturnType());
boolean isVoid = returnType == null || returnType.equals(PsiType.VOID);
if (!isVoid) {
sb.append(':');
sb.append(simplifyTypes(returnType, file, imports));
}
// Body
sb.append("{\n");
if (!isVoid) {
if (returnType instanceof PsiPrimitiveType) {
if (returnType.equals(PsiType.BOOLEAN)) {
sb.append("return false\n");
} else {
sb.append("return 0\n");
}
} else {
sb.append("return null\n");
}
}
sb.append("}\n\n");
return sb.toString();
}
private static String simplifyTypes(PsiType type, PsiElement context, Map<String, String> imports) {
if (context instanceof IGosuPsiElement) {
return ClassLord.simplifyTypes(type.getCanonicalText(), (IGosuPsiElement) context, imports);
} else {
return type.getCanonicalText();
}
}
private static String getTypeRefString(@NotNull PsiType type, @NotNull Set<String> imports) {
// todo what about enums?
// todo honor settings wrt/ inner class imports
String result = type.getCanonicalText();
if (type instanceof PsiClassReferenceType) {
PsiClass resolvedType = ((PsiClassReferenceType) type).resolve();
if (resolvedType != null && canImport(resolvedType)) {
imports.add(resolvedType.getQualifiedName());
result = resolvedType.getName();
}
}
return result;
}
private static boolean canImport(PsiClass type) {
return true;
}
@NotNull
public static Collection<CandidateInfo> getMethodsToOverrideImplement(@NotNull IGosuTypeDefinition aClass, boolean isImplement) {
final Collection<CandidateInfo> methods = OverrideImplementUtil.getMethodsToOverrideImplement(aClass, isImplement);
final Iterator<CandidateInfo> it = methods.iterator();
while (it.hasNext()) {
if (isImplementedViaProperty(aClass, it.next().getElement())) {
it.remove();
}
}
return methods;
}
private static boolean isImplementedViaProperty(@NotNull IGosuTypeDefinition klass, PsiElement element) {
if (element instanceof PsiMethod) {
final PsiMethod method = (PsiMethod) element;
final String getterName = GosuProperties.getGetterName(method);
if (getterName != null) {
return hasPropertyGetter(klass, getterName);
}
final String setterName = GosuProperties.getSetterName(method);
if (setterName != null) {
return hasPropertySetter(klass, setterName);
}
}
return false;
}
private static boolean hasPropertyGetter(@NotNull IGosuTypeDefinition klass, @NotNull String property) {
for (PsiMethod method : klass.getAllMethods()) {
if (!method.hasModifierProperty(PsiModifier.ABSTRACT) && property.equals(GosuProperties.getGetterName(method))) {
return true;
}
}
return false;
}
private static boolean hasPropertySetter(@NotNull IGosuTypeDefinition klass, @NotNull String property) {
for (PsiMethod method : klass.getAllMethods()) {
if (!method.hasModifierProperty(PsiModifier.ABSTRACT) && property.equals(GosuProperties.getSetterName(method))) {
return true;
}
}
return false;
}
// private static void positionCaret(Editor editor, GrMethod result) {
// final GrOpenBlock body = result.getBlock();
// if (body == null) return;
//
// final PsiElement lBrace = body.getLBrace();
//
// assert lBrace != null;
// PsiElement l = lBrace.getNextSibling();
// assert l != null;
//
// final PsiElement rBrace = body.getRBrace();
//
// assert rBrace != null;
// PsiElement r = rBrace.getPrevSibling();
// assert r != null;
//
// LOG.assertTrue(!PsiDocumentManager.getInstance(result.getProject()).isUncommited(editor.getDocument()));
// String text = editor.getDocument().getText();
//
// int start = l.getTextRange().getStartOffset();
// start = CharArrayUtil.shiftForward(text, start, "\n\t ");
// int end = r.getTextRange().getEndOffset();
// end = CharArrayUtil.shiftBackward(text, end - 1, "\n\t ") + 1;
//
// editor.getCaretModel().moveToOffset(Math.min(start, end));
// editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
// if (start < end) {
// //Not an empty body
// editor.getSelectionModel().setSelection(start, end);
// }
// }
//
// private static boolean writeMethodModifiers(StringBuffer text, PsiModifierList modifierList, String[] modifiers) {
// boolean wasAddedModifiers = false;
// for (String modifierType : modifiers) {
// if (modifierList.hasModifierProperty(modifierType) && modifierType != PsiModifier.PUBLIC) {
// text.append(modifierType);
// text.append(" ");
// wasAddedModifiers = true;
// }
// }
// return wasAddedModifiers;
// }
//
//
// @NotNull
// private static String callSuper(PsiMethod superMethod, PsiMethod overriding) {
// @NonNls StringBuilder buffer = new StringBuilder();
// if (!superMethod.isConstructor() && superMethod.getReturnType() != PsiType.VOID) {
// buffer.append("return ");
// }
// buffer.append("super");
// PsiParameter[] parms = overriding.getParameterList().getParameters();
// if (!superMethod.isConstructor()) {
// buffer.append(".");
// buffer.append(superMethod.getName());
// }
// buffer.append("(");
// for (int i = 0; i < parms.length; i++) {
// String name = parms[i].getName();
// if (i > 0) buffer.append(",");
// buffer.append(name);
// }
// buffer.append(")");
// return buffer.toString();
// }
private static ASTNode findImmediateChildAtOffset(@NotNull ASTNode parent, int offset) {
// XXX need to adjust for javadoc and annotation nodes.
ASTNode candidate = parent.getFirstChildNode();
for (ASTNode element = candidate; element != null; element = element.getTreeNext()) {
int startOffset = element.getStartOffset();
if (offset < startOffset) {
// XXX how do we get here?
break;
}
if (offset == startOffset) {
candidate = element;
break;
}
int endOffset = startOffset + element.getTextLength();
if (offset <= endOffset) {
// XXX what to return if <next> is null?
candidate = element.getTreeNext();
break;
}
}
return candidate;
}
// Jumps before existing whitespace and doc nodes attached to node.
private static ASTNode findPriorNonDocNonWSNode(ASTNode node) {
ASTNode follower = node;
if (follower != null) {
ASTNode candidate = follower.getTreePrev();
while (candidate instanceof PsiWhiteSpace || candidate instanceof PsiComment) {
follower = candidate;
candidate = candidate.getTreePrev();
}
}
return follower;
}
@Nullable
private static ASTNode findLastChildBefore(@NotNull ASTNode parent, IElementType typeToFind, IElementType typeToPrecede) {
ASTNode candidate = null;
ASTNode findMatch = null;
ASTNode precedeMatch = null;
for (ASTNode child = parent.getFirstChildNode(); child != null; child = child.getTreeNext()) {
IElementType candidateType = child.getElementType();
if (candidateType == typeToPrecede) {
precedeMatch = child;
break;
}
if (candidateType == typeToFind) {
findMatch = child;
}
}
if (findMatch != null) {
candidate = findMatch.getTreeNext();
} else if (precedeMatch != null) {
candidate = precedeMatch.getTreePrev();
if (!(candidate instanceof PsiWhiteSpace)) {
// XXX is it possible to back over the open curly bracket?
candidate = precedeMatch;
}
}
if (candidate == null) {
candidate = parent.getLastChildNode();
}
return candidate;
}
private GosuOverrideImplementUtil() {
}
}