java completion: suggest guava/protobuf builder variants (IDEA-130386)

This commit is contained in:
peter
2017-05-10 10:13:55 +02:00
parent 55d1ea20db
commit a9db058d17
12 changed files with 221 additions and 28 deletions
@@ -53,6 +53,7 @@ import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -409,7 +410,7 @@ public class JavaMethodCallElement extends LookupItem<PsiMethod> implements Type
private void insertExplicitTypeParameters(InsertionContext context, OffsetKey refStart) {
context.commitDocument();
final String typeParams = getTypeParamsText(false);
final String typeParams = getTypeParamsText(false, getObject(), getInferenceSubstitutor());
if (typeParams != null) {
context.getDocument().insertString(context.getOffset(refStart), typeParams);
JavaCompletionUtil.shortenReference(context.getFile(), context.getOffset(refStart));
@@ -435,30 +436,19 @@ public class JavaMethodCallElement extends LookupItem<PsiMethod> implements Type
}
@Nullable
private String getTypeParamsText(boolean presentable) {
final PsiMethod method = getObject();
final PsiSubstitutor substitutor = getInferenceSubstitutor();
final PsiTypeParameter[] parameters = method.getTypeParameters();
assert parameters.length > 0;
final StringBuilder builder = new StringBuilder("<");
boolean first = true;
for (final PsiTypeParameter parameter : parameters) {
if (!first) builder.append(", ");
first = false;
public static String getTypeParamsText(boolean presentable, PsiTypeParameterListOwner owner, PsiSubstitutor substitutor) {
PsiTypeParameter[] parameters = owner.getTypeParameters();
if (parameters.length == 0) return null;
List<PsiType> substituted = ContainerUtil.map(parameters, parameter -> {
PsiType type = substitutor.substitute(parameter);
if (type instanceof PsiWildcardType) {
type = ((PsiWildcardType)type).getExtendsBound();
}
return type instanceof PsiWildcardType ? ((PsiWildcardType)type).getExtendsBound() : type;
});
if (ContainerUtil.exists(substituted, t -> t == null || t instanceof PsiCapturedWildcardType)) return null;
if (substituted.equals(ContainerUtil.map(parameters, TypeConversionUtil::typeParameterErasure))) return null;
if (type == null || type instanceof PsiCapturedWildcardType) return null;
if (type.equals(TypeConversionUtil.typeParameterErasure(parameter))) return null;
final String text = presentable ? type.getPresentableText() : type.getCanonicalText();
if (text.indexOf('?') >= 0) return null;
builder.append(text);
}
return builder.append(">").toString();
String result = "<" + StringUtil.join(substituted, presentable ? PsiType::getPresentableText : PsiType::getCanonicalText, ", ") + ">";
return result.contains("?") ? null : result;
}
@Override
@@ -479,7 +469,7 @@ public class JavaMethodCallElement extends LookupItem<PsiMethod> implements Type
}
if (shouldInsertTypeParameters()) {
String typeParamsText = getTypeParamsText(true);
String typeParamsText = getTypeParamsText(true, getObject(), getInferenceSubstitutor());
if (typeParamsText != null) {
if (typeParamsText.length() > 10) {
typeParamsText = typeParamsText.substring(0, 10) + "...>";
@@ -20,7 +20,6 @@ import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.Consumer;
import com.intellij.util.IncorrectOperationException;
@@ -99,9 +98,7 @@ public class ToArrayConversion {
@Override
public void handleInsert(InsertionContext context) {
FeatureUsageTracker.getInstance().triggerFeatureUsed(JavaCompletionFeatures.SECOND_SMART_COMPLETION_TOAR);
context.commitDocument();
JavaCodeStyleManager.getInstance(context.getProject()).shortenClassReferences(context.getFile(), context.getStartOffset(), context.getTailOffset());
super.handleInsert(context);
}
});
}
@@ -15,7 +15,9 @@
*/
package com.intellij.codeInsight.lookup;
import com.intellij.codeInsight.completion.InsertionContext;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.util.PlatformIcons;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
@@ -75,6 +77,12 @@ public class ExpressionLookupItem extends LookupElement implements TypedLookupIt
presentation.setTypeText(type == null ? null : type.getPresentableText());
}
@Override
public void handleInsert(InsertionContext context) {
context.commitDocument();
JavaCodeStyleManager.getInstance(context.getProject()).shortenClassReferences(context.getFile(), context.getStartOffset(), context.getTailOffset());
}
@Override
public PsiType getType() {
return myExpression.getType();
@@ -0,0 +1,92 @@
/*
* 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.psi.filters.getters
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.completion.JavaMethodCallElement
import com.intellij.codeInsight.lookup.ExpressionLookupItem
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.psi.*
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.PsiUtil
import com.intellij.util.IncorrectOperationException
import com.intellij.util.PlatformIcons
import java.util.*
/**
* @author peter
*/
internal class BuilderCompletion(private val expectedType: PsiClassType, private val expectedClass: PsiClass, private val place: PsiElement) {
fun suggestBuilderVariants(): List<LookupElement> {
val result = ArrayList<LookupElement>()
for (builderClass in expectedClass.innerClasses.filter { it.name?.contains("Builder") == true }) {
for (buildMethods in methodsReturning(expectedClass, builderClass, true)) {
for (createMethods in methodsReturning(builderClass, expectedClass, false)) {
createBuilderCall(buildMethods, createMethods)?.let { result.add(it) }
}
}
}
return result
}
private fun methodsReturning(containingClass: PsiClass, returnedClass: PsiClass, isStatic: Boolean): Collection<List<PsiMethod>> {
return containingClass.methods
.filter { it.hasModifierProperty(PsiModifier.STATIC) == isStatic &&
returnedClass == PsiUtil.resolveClassInClassTypeOnly(it.returnType) &&
PsiUtil.isAccessible(it, place, null) }
.groupBy { it.name }
.values
}
private fun showOverloads(methods: List<PsiMethod>) = if (methods.any { it.parameterList.parametersCount > 0 }) "..." else ""
private fun createBuilderCall(buildOverloads: List<PsiMethod>, createOverloads: List<PsiMethod>): LookupElement? {
val classQname = expectedClass.qualifiedName ?: return null
val classShortName = expectedClass.name ?: return null
val buildName = buildOverloads.first().name
val createName = createOverloads.first().name
val typeArgs = JavaMethodCallElement.getTypeParamsText(false, expectedClass, expectedType.resolveGenerics().substitutor) ?: ""
val canonicalText = "$classQname.$typeArgs$buildName().$createName()"
val presentableText = "$classShortName.$buildName(${showOverloads(buildOverloads)}).$createName(${showOverloads(createOverloads)})"
val expr =
try { JavaPsiFacade.getElementFactory(expectedClass.project).createExpressionFromText(canonicalText, place) }
catch(e: IncorrectOperationException) { return null }
if (expr.type != expectedType) return null
return object: ExpressionLookupItem(expr, PlatformIcons.METHOD_ICON, presentableText, canonicalText, presentableText, buildName, createName) {
override fun handleInsert(context: InsertionContext) {
super.handleInsert(context)
positionCaret(context)
}
}
}
}
private fun positionCaret(context: InsertionContext) {
val createCall = PsiTreeUtil.findElementOfClassAtOffset(context.file, context.tailOffset - 1, PsiMethodCallExpression::class.java, false)
val buildCall = createCall?.methodExpression?.qualifierExpression as? PsiMethodCallExpression ?: return
val hasParams = buildCall.methodExpression.multiResolve(true).any { ((it.element as? PsiMethod)?.parameterList?.parametersCount ?: 0) > 0 }
val argRange = buildCall.argumentList.textRange
context.editor.caretModel.moveToOffset(if (hasParams) (argRange.startOffset + argRange.endOffset) / 2 else argRange.endOffset)
}
@@ -62,6 +62,10 @@ public class JavaMembersGetter extends MembersGetter {
final PsiClass psiClass = PsiUtil.resolveClassInType(myExpectedType);
processMembers(results, psiClass, PsiTreeUtil.getParentOfType(myPlace, PsiAnnotation.class) == null, searchInheritors);
if (psiClass != null && myExpectedType instanceof PsiClassType) {
new BuilderCompletion((PsiClassType)myExpectedType, psiClass, myPlace).suggestBuilderVariants().forEach(results::consume);
}
}
private void addConstantsFromReferencedClassesInSwitch(final Consumer<LookupElement> results) {
@@ -0,0 +1,15 @@
public class S {
{
Map m = Map.build()<caret>.get();
}
}
class Map {
static Builder build() {}
static class Builder {
Map get() {}
}
}
@@ -0,0 +1,15 @@
public class S {
{
Map m = bui<caret>
}
}
class Map {
static Builder build() {}
static class Builder {
Map get() {}
}
}
@@ -0,0 +1,15 @@
public class S {
{
Map m = Map.build(<caret>).get();
}
}
class Map {
static Builder build(int param) {}
static class Builder {
Map get() {}
}
}
@@ -0,0 +1,15 @@
public class S {
{
Map m = bui<caret>
}
}
class Map {
static Builder build(int param) {}
static class Builder {
Map get() {}
}
}
@@ -0,0 +1,16 @@
public class S {
{
Map<String, Object> m = Map.<String, Object>builder()<caret>.get();
}
}
class Map<K,V> {
static <K,V> Builder<K,V> builder() {}
static class Builder<K,V> {
Map<K,V> get() {}
Map<K,V> get(int x) {}
}
}
@@ -0,0 +1,16 @@
public class S {
{
Map<String, Object> m = bui<caret>x
}
}
class Map<K,V> {
static <K,V> Builder<K,V> builder() {}
static class Builder<K,V> {
Map<K,V> get() {}
Map<K,V> get(int x) {}
}
}
@@ -1255,4 +1255,14 @@ public class SmartTypeCompletionTest extends LightFixtureCompletionTestCase {
configureByTestName();
myFixture.assertPreferredCompletionItems(0, "String", "StringBuffer", "StringBuilder");
}
public void testStaticBuilder() { doTest(); }
public void testStaticBuilderWithArguments() { doTest(); }
public void testStaticBuilderWithGenerics() {
configureByTestName();
assertEquals("Map.builder().get(...)", LookupElementPresentation.renderElement(myItems[0]).getItemText());
myFixture.type('\t');
checkResultByTestName();
}
}