| /* |
| * Copyright 2000-2013 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.codeInsight.completion; |
| |
| import com.intellij.codeInsight.ExpectedTypeInfo; |
| import com.intellij.codeInsight.ExpectedTypeInfoImpl; |
| import com.intellij.codeInsight.completion.impl.CompletionSorterImpl; |
| import com.intellij.codeInsight.completion.impl.LiftShorterItemsClassifier; |
| import com.intellij.codeInsight.lookup.*; |
| import com.intellij.openapi.roots.ProjectFileIndex; |
| import com.intellij.openapi.roots.ProjectRootManager; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.patterns.PsiJavaPatterns; |
| import com.intellij.psi.*; |
| import com.intellij.psi.codeStyle.JavaCodeStyleManager; |
| import com.intellij.psi.codeStyle.NameUtil; |
| import com.intellij.psi.codeStyle.VariableKind; |
| import com.intellij.psi.javadoc.PsiDocComment; |
| import com.intellij.psi.util.PsiTreeUtil; |
| import com.intellij.psi.util.PsiUtil; |
| import com.intellij.psi.util.TypeConversionUtil; |
| import com.intellij.util.Function; |
| import com.intellij.util.SmartList; |
| import com.intellij.util.containers.ContainerUtil; |
| import gnu.trove.THashSet; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| /** |
| * @author peter |
| */ |
| public class JavaCompletionSorting { |
| private JavaCompletionSorting() { |
| } |
| |
| public static CompletionResultSet addJavaSorting(final CompletionParameters parameters, CompletionResultSet result) { |
| final PsiElement position = parameters.getPosition(); |
| final ExpectedTypeInfo[] expectedTypes = PsiJavaPatterns.psiElement().beforeLeaf(PsiJavaPatterns.psiElement().withText(".")).accepts(position) ? ExpectedTypeInfo.EMPTY_ARRAY : JavaSmartCompletionContributor.getExpectedTypes(parameters); |
| final CompletionType type = parameters.getCompletionType(); |
| final boolean smart = type == CompletionType.SMART; |
| final boolean afterNew = JavaSmartCompletionContributor.AFTER_NEW.accepts(position); |
| |
| List<LookupElementWeigher> afterProximity = new ArrayList<LookupElementWeigher>(); |
| afterProximity.add(new PreferContainingSameWords(expectedTypes)); |
| afterProximity.add(new PreferShorter(expectedTypes)); |
| |
| CompletionSorter sorter = CompletionSorter.defaultSorter(parameters, result.getPrefixMatcher()); |
| if (!smart && afterNew) { |
| sorter = sorter.weighBefore("liftShorter", new PreferExpected(true, expectedTypes)); |
| } else if (PsiTreeUtil.getParentOfType(position, PsiReferenceList.class) == null) { |
| sorter = ((CompletionSorterImpl)sorter).withClassifier("liftShorterClasses", true, new LiftShorterClasses(position)); |
| } |
| if (smart) { |
| sorter = sorter.weighAfter("priority", new PreferDefaultTypeWeigher(expectedTypes, parameters)); |
| } |
| |
| List<LookupElementWeigher> afterPrefix = ContainerUtil.newArrayList(); |
| if (!smart) { |
| ContainerUtil.addIfNotNull(afterPrefix, preferStatics(position, expectedTypes)); |
| } |
| if (!smart && !afterNew) { |
| afterPrefix.add(new PreferExpected(false, expectedTypes)); |
| } |
| afterPrefix.add(new PreferByKindWeigher(type, position)); |
| ContainerUtil.addIfNotNull(afterPrefix, recursion(parameters, expectedTypes)); |
| Collections.addAll(afterPrefix, new PreferSimilarlyEnding(expectedTypes), |
| new PreferNonGeneric(), new PreferAccessible(position), new PreferSimple()); |
| |
| sorter = sorter.weighAfter("prefix", afterPrefix.toArray(new LookupElementWeigher[afterPrefix.size()])); |
| sorter = sorter.weighAfter("proximity", afterProximity.toArray(new LookupElementWeigher[afterProximity.size()])); |
| return result.withRelevanceSorter(sorter); |
| } |
| |
| @Nullable |
| private static LookupElementWeigher recursion(CompletionParameters parameters, final ExpectedTypeInfo[] expectedInfos) { |
| final PsiElement position = parameters.getPosition(); |
| final PsiMethodCallExpression expression = PsiTreeUtil.getParentOfType(position, PsiMethodCallExpression.class, true, PsiClass.class); |
| final PsiReferenceExpression reference = expression != null ? expression.getMethodExpression() : PsiTreeUtil.getParentOfType(position, PsiReferenceExpression.class); |
| if (reference == null) return null; |
| |
| return new RecursionWeigher(position, parameters.getCompletionType(), reference, expression, expectedInfos); |
| } |
| |
| @Nullable |
| private static LookupElementWeigher preferStatics(PsiElement position, final ExpectedTypeInfo[] infos) { |
| if (PsiTreeUtil.getParentOfType(position, PsiDocComment.class) != null) { |
| return null; |
| } |
| if (position.getParent() instanceof PsiReferenceExpression) { |
| final PsiReferenceExpression refExpr = (PsiReferenceExpression)position.getParent(); |
| final PsiElement qualifier = refExpr.getQualifier(); |
| if (qualifier == null) { |
| return null; |
| } |
| if (!(qualifier instanceof PsiJavaCodeReferenceElement) || !(((PsiJavaCodeReferenceElement)qualifier).resolve() instanceof PsiClass)) { |
| return null; |
| } |
| } |
| |
| return new LookupElementWeigher("statics") { |
| @NotNull |
| @Override |
| public Comparable weigh(@NotNull LookupElement element) { |
| final Object o = element.getObject(); |
| if (o instanceof PsiKeyword) return -3; |
| if (!(o instanceof PsiMember) || element.getUserData(JavaGenerateMemberCompletionContributor.GENERATE_ELEMENT) != null) { |
| return 0; |
| } |
| |
| if (((PsiMember)o).hasModifierProperty(PsiModifier.STATIC) && !hasNonVoid(infos)) { |
| if (o instanceof PsiMethod) return -5; |
| if (o instanceof PsiField) return -4; |
| } |
| |
| if (o instanceof PsiClass) return -3; |
| |
| //instance method or field |
| return -5; |
| } |
| }; |
| } |
| |
| private static ExpectedTypeMatching getExpectedTypeMatching(LookupElement item, ExpectedTypeInfo[] expectedInfos) { |
| PsiType itemType = JavaCompletionUtil.getLookupElementType(item); |
| |
| if (itemType != null) { |
| PsiUtil.ensureValidType(itemType); |
| |
| for (final ExpectedTypeInfo expectedInfo : expectedInfos) { |
| final PsiType defaultType = expectedInfo.getDefaultType(); |
| final PsiType expectedType = expectedInfo.getType(); |
| |
| assert expectedType.isValid(); |
| assert defaultType.isValid(); |
| |
| if (defaultType != expectedType && defaultType.isAssignableFrom(itemType)) { |
| return ExpectedTypeMatching.ofDefaultType; |
| } |
| if (expectedType.isAssignableFrom(itemType)) { |
| return ExpectedTypeMatching.expected; |
| } |
| } |
| } |
| |
| if (hasNonVoid(expectedInfos)) { |
| if (item.getObject() instanceof PsiKeyword) { |
| String keyword = ((PsiKeyword)item.getObject()).getText(); |
| if (PsiKeyword.NEW.equals(keyword) || PsiKeyword.NULL.equals(keyword)) { |
| return ExpectedTypeMatching.maybeExpected; |
| } |
| } |
| } |
| else if (expectedInfos.length > 0) { |
| return ExpectedTypeMatching.unexpected; |
| } |
| |
| return ExpectedTypeMatching.normal; |
| } |
| |
| private static boolean hasNonVoid(ExpectedTypeInfo[] expectedInfos) { |
| boolean hasNonVoid = false; |
| for (ExpectedTypeInfo info : expectedInfos) { |
| if (!PsiType.VOID.equals(info.getType())) { |
| hasNonVoid = true; |
| } |
| } |
| return hasNonVoid; |
| } |
| |
| @Nullable |
| private static String getLookupObjectName(Object o) { |
| if (o instanceof PsiVariable) { |
| final PsiVariable variable = (PsiVariable)o; |
| JavaCodeStyleManager codeStyleManager = JavaCodeStyleManager.getInstance(variable.getProject()); |
| VariableKind variableKind = codeStyleManager.getVariableKind(variable); |
| return codeStyleManager.variableNameToPropertyName(variable.getName(), variableKind); |
| } |
| if (o instanceof PsiMethod) { |
| return ((PsiMethod)o).getName(); |
| } |
| return null; |
| } |
| |
| private static int getNameEndMatchingDegree(final String name, ExpectedTypeInfo[] expectedInfos) { |
| int res = 0; |
| if (name != null && expectedInfos != null) { |
| final List<String> words = NameUtil.nameToWordsLowerCase(name); |
| final List<String> wordsNoDigits = NameUtil.nameToWordsLowerCase(truncDigits(name)); |
| int max1 = calcMatch(words, 0, expectedInfos); |
| max1 = calcMatch(wordsNoDigits, max1, expectedInfos); |
| res = max1; |
| } |
| |
| return res; |
| } |
| |
| private static String truncDigits(String name){ |
| int count = name.length() - 1; |
| while (count >= 0) { |
| char c = name.charAt(count); |
| if (!Character.isDigit(c)) break; |
| count--; |
| } |
| return name.substring(0, count + 1); |
| } |
| |
| private static int calcMatch(final List<String> words, int max, ExpectedTypeInfo[] myExpectedInfos) { |
| for (ExpectedTypeInfo myExpectedInfo : myExpectedInfos) { |
| String expectedName = ((ExpectedTypeInfoImpl)myExpectedInfo).getExpectedName(); |
| if (expectedName == null) continue; |
| max = calcMatch(expectedName, words, max); |
| max = calcMatch(truncDigits(expectedName), words, max); |
| } |
| return max; |
| } |
| |
| private static int calcMatch(final String expectedName, final List<String> words, int max) { |
| if (expectedName == null) return max; |
| |
| String[] expectedWords = NameUtil.nameToWords(expectedName); |
| int limit = Math.min(words.size(), expectedWords.length); |
| for (int i = 0; i < limit; i++) { |
| String word = words.get(words.size() - i - 1); |
| String expectedWord = expectedWords[expectedWords.length - i - 1]; |
| if (word.equalsIgnoreCase(expectedWord)) { |
| max = Math.max(max, i + 1); |
| } |
| else { |
| break; |
| } |
| } |
| return max; |
| } |
| |
| private static class PreferDefaultTypeWeigher extends LookupElementWeigher { |
| private final PsiTypeParameter myTypeParameter; |
| private final ExpectedTypeInfo[] myExpectedTypes; |
| private final CompletionParameters myParameters; |
| private final CompletionLocation myLocation; |
| |
| public PreferDefaultTypeWeigher(ExpectedTypeInfo[] expectedTypes, CompletionParameters parameters) { |
| super("defaultType"); |
| myExpectedTypes = expectedTypes == null ? null : ContainerUtil.map2Array(expectedTypes, ExpectedTypeInfo.class, new Function<ExpectedTypeInfo, ExpectedTypeInfo>() { |
| @Override |
| public ExpectedTypeInfo fun(ExpectedTypeInfo info) { |
| PsiType type = removeClassWildcard(info.getType()); |
| PsiType defaultType = removeClassWildcard(info.getDefaultType()); |
| if (type == info.getType() && defaultType == info.getDefaultType()) { |
| return info; |
| } |
| return new ExpectedTypeInfoImpl(type, info.getKind(), defaultType, info.getTailType(), null, ExpectedTypeInfoImpl.NULL); |
| } |
| }); |
| myParameters = parameters; |
| |
| final Pair<PsiClass,Integer> pair = TypeArgumentCompletionProvider.getTypeParameterInfo(parameters.getPosition()); |
| myTypeParameter = pair == null ? null : pair.first.getTypeParameters()[pair.second.intValue()]; |
| myLocation = new CompletionLocation(myParameters); |
| } |
| |
| @NotNull |
| @Override |
| public MyResult weigh(@NotNull LookupElement item) { |
| final Object object = item.getObject(); |
| |
| if (object instanceof PsiClass) { |
| if (object instanceof PsiTypeParameter) return MyResult.typeParameter; |
| |
| if (myTypeParameter != null && object.equals(PsiUtil.resolveClassInType(TypeConversionUtil.typeParameterErasure(myTypeParameter)))) { |
| return MyResult.exactlyExpected; |
| } |
| } |
| |
| if (myExpectedTypes == null) return MyResult.normal; |
| |
| PsiType itemType = JavaCompletionUtil.getLookupElementType(item); |
| if (itemType == null || !itemType.isValid()) return MyResult.normal; |
| |
| if (object instanceof PsiClass) { |
| for (final ExpectedTypeInfo info : myExpectedTypes) { |
| if (TypeConversionUtil.erasure(info.getType().getDeepComponentType()).equals(TypeConversionUtil.erasure(itemType))) { |
| return AbstractExpectedTypeSkipper.skips(item, myLocation) ? MyResult.expectedNoSelect : MyResult.exactlyExpected; |
| } |
| } |
| } |
| |
| for (final ExpectedTypeInfo expectedInfo : myExpectedTypes) { |
| final PsiType defaultType = expectedInfo.getDefaultType(); |
| final PsiType expectedType = expectedInfo.getType(); |
| if (!expectedType.isValid()) { |
| return MyResult.normal; |
| } |
| |
| if (defaultType != expectedType) { |
| if (defaultType.equals(itemType)) { |
| return MyResult.exactlyDefault; |
| } |
| |
| if (defaultType.isAssignableFrom(itemType)) { |
| return MyResult.ofDefaultType; |
| } |
| } |
| if (PsiType.VOID.equals(itemType) && PsiType.VOID.equals(expectedType)) { |
| return MyResult.exactlyExpected; |
| } |
| } |
| |
| return MyResult.normal; |
| } |
| |
| private static PsiType removeClassWildcard(PsiType type) { |
| if (type instanceof PsiClassType) { |
| final PsiClass psiClass = ((PsiClassType)type).resolve(); |
| if (psiClass != null && CommonClassNames.JAVA_LANG_CLASS.equals(psiClass.getQualifiedName())) { |
| PsiClassType erased = (PsiClassType)GenericsUtil.eliminateWildcards(type); |
| PsiType[] parameters = erased.getParameters(); |
| if (parameters.length == 1 && !parameters[0].equalsToText(CommonClassNames.JAVA_LANG_OBJECT)) { |
| return erased; |
| } |
| } |
| } |
| return type; |
| } |
| |
| private enum MyResult { |
| expectedNoSelect, |
| typeParameter, |
| exactlyDefault, |
| ofDefaultType, |
| exactlyExpected, |
| normal, |
| } |
| |
| } |
| |
| private enum ExpectedTypeMatching { |
| ofDefaultType, |
| expected, |
| maybeExpected, |
| normal, |
| unexpected, |
| } |
| |
| private static class PreferAccessible extends LookupElementWeigher { |
| private final PsiElement myPosition; |
| |
| public PreferAccessible(PsiElement position) { |
| super("accessible"); |
| myPosition = position; |
| } |
| |
| private enum MyEnum { |
| NORMAL, |
| DEPRECATED, |
| INACCESSIBLE, |
| } |
| |
| @NotNull |
| @Override |
| public Comparable weigh(@NotNull LookupElement element) { |
| final Object object = element.getObject(); |
| if (object instanceof PsiDocCommentOwner) { |
| final PsiDocCommentOwner member = (PsiDocCommentOwner)object; |
| if (!JavaPsiFacade.getInstance(member.getProject()).getResolveHelper().isAccessible(member, myPosition, null)) return MyEnum.INACCESSIBLE; |
| if (member.isDeprecated()) return MyEnum.DEPRECATED; |
| } |
| return MyEnum.NORMAL; |
| } |
| } |
| |
| private static class PreferNonGeneric extends LookupElementWeigher { |
| public PreferNonGeneric() { |
| super("nonGeneric"); |
| } |
| |
| @NotNull |
| @Override |
| public Comparable weigh(@NotNull LookupElement element) { |
| final Object object = element.getObject(); |
| if (object instanceof PsiMethod) { |
| PsiType type = ((PsiMethod)object).getReturnType(); |
| final JavaMethodCallElement callItem = element.as(JavaMethodCallElement.CLASS_CONDITION_KEY); |
| if (callItem != null) { |
| type = callItem.getSubstitutor().substitute(type); |
| } |
| |
| if (type instanceof PsiClassType && ((PsiClassType) type).resolve() instanceof PsiTypeParameter) return 1; |
| } |
| |
| return 0; |
| } |
| } |
| |
| private static class PreferSimple extends LookupElementWeigher { |
| public PreferSimple() { |
| super("simple"); |
| } |
| |
| @NotNull |
| @Override |
| public Comparable weigh(@NotNull LookupElement element) { |
| final PsiTypeLookupItem lookupItem = element.as(PsiTypeLookupItem.CLASS_CONDITION_KEY); |
| if (lookupItem != null) { |
| return lookupItem.getBracketsCount() * 10 + (lookupItem.isAddArrayInitializer() ? 1 : 0); |
| } |
| if (element.as(CastingLookupElementDecorator.CLASS_CONDITION_KEY) != null) { |
| return 239; |
| } |
| return 0; |
| } |
| } |
| |
| private static class PreferExpected extends LookupElementWeigher { |
| private final boolean myConstructorPossible; |
| private final ExpectedTypeInfo[] myExpectedTypes; |
| private final List<PsiType> myExpectedClasses = new SmartList<PsiType>(); |
| |
| public PreferExpected(boolean constructorPossible, ExpectedTypeInfo[] expectedTypes) { |
| super("expectedType"); |
| myConstructorPossible = constructorPossible; |
| myExpectedTypes = expectedTypes; |
| for (ExpectedTypeInfo info : expectedTypes) { |
| ContainerUtil.addIfNotNull(myExpectedClasses, PsiUtil.substituteTypeParameter(info.getDefaultType(), CommonClassNames.JAVA_LANG_CLASS, 0, false)); |
| } |
| } |
| |
| @NotNull |
| @Override |
| public ExpectedTypeMatching weigh(@NotNull LookupElement item) { |
| if (item.getObject() instanceof PsiClass && !myConstructorPossible) { |
| PsiType itemType = JavaCompletionUtil.getLookupElementType(item); |
| if (itemType != null) { |
| for (PsiType expectedClass : myExpectedClasses) { |
| if (expectedClass.isAssignableFrom(itemType)) { |
| return ExpectedTypeMatching.expected; |
| } |
| } |
| } |
| return ExpectedTypeMatching.normal; |
| } |
| |
| return getExpectedTypeMatching(item, myExpectedTypes); |
| } |
| } |
| |
| private static class PreferSimilarlyEnding extends LookupElementWeigher { |
| private final ExpectedTypeInfo[] myExpectedTypes; |
| |
| public PreferSimilarlyEnding(ExpectedTypeInfo[] expectedTypes) { |
| super("nameEnd"); |
| myExpectedTypes = expectedTypes; |
| } |
| |
| @NotNull |
| @Override |
| public Comparable weigh(@NotNull LookupElement element) { |
| final String name = getLookupObjectName(element.getObject()); |
| return -getNameEndMatchingDegree(name, myExpectedTypes); |
| } |
| } |
| |
| private static class PreferContainingSameWords extends LookupElementWeigher { |
| private final ExpectedTypeInfo[] myExpectedTypes; |
| |
| public PreferContainingSameWords(ExpectedTypeInfo[] expectedTypes) { |
| super("sameWords"); |
| myExpectedTypes = expectedTypes; |
| } |
| |
| @NotNull |
| @Override |
| public Comparable weigh(@NotNull LookupElement element) { |
| final Object object = element.getObject(); |
| |
| final String name = getLookupObjectName(object); |
| if (name != null) { |
| int max = 0; |
| final List<String> wordsNoDigits = NameUtil.nameToWordsLowerCase(truncDigits(name)); |
| for (ExpectedTypeInfo myExpectedInfo : myExpectedTypes) { |
| String expectedName = ((ExpectedTypeInfoImpl)myExpectedInfo).getExpectedName(); |
| if (expectedName != null) { |
| final THashSet<String> set = new THashSet<String>(NameUtil.nameToWordsLowerCase(truncDigits(expectedName))); |
| set.retainAll(wordsNoDigits); |
| max = Math.max(max, set.size()); |
| } |
| } |
| return -max; |
| } |
| return 0; |
| } |
| } |
| |
| private static class PreferShorter extends LookupElementWeigher { |
| private final ExpectedTypeInfo[] myExpectedTypes; |
| |
| public PreferShorter(ExpectedTypeInfo[] expectedTypes) { |
| super("shorter"); |
| myExpectedTypes = expectedTypes; |
| } |
| |
| @NotNull |
| @Override |
| public Comparable weigh(@NotNull LookupElement element) { |
| final Object object = element.getObject(); |
| final String name = getLookupObjectName(object); |
| |
| if (name != null && getNameEndMatchingDegree(name, myExpectedTypes) != 0) { |
| return NameUtil.nameToWords(name).length - 1000; |
| } |
| return 0; |
| } |
| } |
| |
| private static class LiftShorterClasses extends ClassifierFactory<LookupElement> { |
| final ProjectFileIndex fileIndex; |
| private final PsiElement myPosition; |
| |
| public LiftShorterClasses(PsiElement position) { |
| super("liftShorterClasses"); |
| myPosition = position; |
| fileIndex = ProjectRootManager.getInstance(myPosition.getProject()).getFileIndex(); |
| } |
| |
| @Override |
| public Classifier<LookupElement> createClassifier(Classifier<LookupElement> next) { |
| return new LiftShorterItemsClassifier("liftShorterClasses", next, new LiftShorterItemsClassifier.LiftingCondition() { |
| @Override |
| public boolean shouldLift(LookupElement shorterElement, LookupElement longerElement) { |
| Object object = shorterElement.getObject(); |
| if (object instanceof PsiClass && longerElement.getObject() instanceof PsiClass) { |
| PsiClass psiClass = (PsiClass)object; |
| PsiFile file = psiClass.getContainingFile(); |
| if (file != null) { |
| VirtualFile vFile = file.getOriginalFile().getVirtualFile(); |
| if (vFile != null && fileIndex.isInSource(vFile)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| }, true); |
| } |
| } |
| } |