Cleanup: NotNull/Nullable
[idea/community.git] / java / java-impl / src / com / intellij / codeInsight / completion / JavaGenerateMemberCompletionContributor.java
1 // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.codeInsight.completion;
3
4 import com.intellij.codeInsight.completion.impl.CamelHumpMatcher;
5 import com.intellij.codeInsight.generation.*;
6 import com.intellij.codeInsight.lookup.LookupElement;
7 import com.intellij.codeInsight.lookup.LookupElementBuilder;
8 import com.intellij.codeInspection.ex.GlobalInspectionContextBase;
9 import com.intellij.icons.AllIcons;
10 import com.intellij.openapi.application.AccessToken;
11 import com.intellij.openapi.command.CommandProcessor;
12 import com.intellij.openapi.util.Iconable;
13 import com.intellij.openapi.util.Key;
14 import com.intellij.openapi.util.TextRange;
15 import com.intellij.openapi.util.text.StringUtil;
16 import com.intellij.psi.*;
17 import com.intellij.psi.infos.CandidateInfo;
18 import com.intellij.psi.util.MethodSignature;
19 import com.intellij.psi.util.PsiTreeUtil;
20 import com.intellij.ui.RowIcon;
21 import com.intellij.util.ObjectUtils;
22 import com.intellij.util.VisibilityUtil;
23 import com.intellij.util.containers.ContainerUtil;
24 import com.intellij.util.containers.FList;
25 import org.jetbrains.annotations.NotNull;
26 import org.jetbrains.annotations.Nullable;
27 import org.jetbrains.java.generate.exception.GenerateCodeException;
28
29 import javax.swing.*;
30 import java.util.ArrayList;
31 import java.util.Collections;
32 import java.util.List;
33 import java.util.Set;
34
35 import static com.intellij.patterns.PlatformPatterns.psiElement;
36
37 /**
38  * @author peter
39  */
40 public class JavaGenerateMemberCompletionContributor {
41   static final Key<Boolean> GENERATE_ELEMENT = Key.create("GENERATE_ELEMENT");
42
43   public static void fillCompletionVariants(CompletionParameters parameters, CompletionResultSet result) {
44     if (parameters.getCompletionType() != CompletionType.BASIC && parameters.getCompletionType() != CompletionType.SMART) {
45       return;
46     }
47
48     PsiElement position = parameters.getPosition();
49     if (psiElement(PsiIdentifier.class).withParents(PsiJavaCodeReferenceElement.class, PsiTypeElement.class, PsiClass.class).
50       andNot(JavaKeywordCompletion.AFTER_DOT).accepts(position)) {
51       PsiElement prevLeaf = PsiTreeUtil.prevVisibleLeaf(position);
52       PsiModifierList modifierList = PsiTreeUtil.getParentOfType(prevLeaf, PsiModifierList.class);
53       if (modifierList != null) {
54         String fileText = position.getContainingFile().getText();
55         result = result.withPrefixMatcher(new NoMiddleMatchesAfterSpace(
56           fileText.substring(modifierList.getTextRange().getStartOffset(), parameters.getOffset())));
57       }
58       suggestGeneratedMethods(result, position, modifierList);
59     } else if (psiElement(PsiIdentifier.class)
60       .withParents(PsiJavaCodeReferenceElement.class, PsiAnnotation.class, PsiModifierList.class, PsiClass.class).accepts(position)) {
61       PsiAnnotation annotation = ObjectUtils.assertNotNull(PsiTreeUtil.getParentOfType(position, PsiAnnotation.class));
62       int annoStart = annotation.getTextRange().getStartOffset();
63       suggestGeneratedMethods(
64         result.withPrefixMatcher(new NoMiddleMatchesAfterSpace(annotation.getText().substring(0, parameters.getOffset() - annoStart))),
65         position,
66         (PsiModifierList)annotation.getParent());
67     }
68
69   }
70
71   private static void suggestGeneratedMethods(CompletionResultSet result, PsiElement position, @Nullable PsiModifierList modifierList) {
72     PsiClass parent = CompletionUtil.getOriginalElement(ObjectUtils.assertNotNull(PsiTreeUtil.getParentOfType(position, PsiClass.class)));
73     if (parent != null) {
74       Set<MethodSignature> addedSignatures = ContainerUtil.newHashSet();
75       addGetterSetterElements(result, parent, addedSignatures);
76       boolean generateDefaultMethods = modifierList != null && modifierList.hasModifierProperty(PsiModifier.DEFAULT);
77       addSuperSignatureElements(parent, true, result, addedSignatures, generateDefaultMethods);
78       addSuperSignatureElements(parent, false, result, addedSignatures, generateDefaultMethods);
79     }
80   }
81
82   private static void addGetterSetterElements(CompletionResultSet result, PsiClass parent, Set<? super MethodSignature> addedSignatures) {
83     int count = 0;
84     for (PsiField field : parent.getFields()) {
85       if (isConstant(field)) continue;
86
87       List<PsiMethod> prototypes = ContainerUtil.newSmartList();
88       try {
89         Collections.addAll(prototypes, GetterSetterPrototypeProvider.generateGetterSetters(field, true, false));
90         if (!field.hasModifierProperty(PsiModifier.FINAL)) {
91           Collections.addAll(prototypes, GetterSetterPrototypeProvider.generateGetterSetters(field, false, false));
92         }
93       }
94       catch (GenerateCodeException ignore) { }
95       for (final PsiMethod prototype : prototypes) {
96         if (parent.findMethodBySignature(prototype, false) == null && addedSignatures.add(prototype.getSignature(PsiSubstitutor.EMPTY))) {
97           Icon icon = prototype.getIcon(Iconable.ICON_FLAG_VISIBILITY);
98           result.addElement(createGenerateMethodElement(prototype, PsiSubstitutor.EMPTY, icon, "", new InsertHandler<LookupElement>() {
99             @Override
100             public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) {
101               removeLookupString(context);
102
103               insertGenerationInfos(context, Collections.singletonList(new PsiGenerationInfo<>(prototype)));
104             }
105           }, false, parent));
106           
107           if (count++ > 100) return;
108         }
109       }
110     }
111   }
112
113   private static boolean isConstant(PsiField field) {
114     return field.hasModifierProperty(PsiModifier.FINAL) && field.hasModifierProperty(PsiModifier.PUBLIC) && field.hasModifierProperty(PsiModifier.STATIC);
115   }
116
117   private static void removeLookupString(InsertionContext context) {
118     context.getDocument().deleteString(context.getStartOffset(), context.getTailOffset());
119     context.commitDocument();
120   }
121
122   private static void addSuperSignatureElements(PsiClass parent, boolean implemented, CompletionResultSet result, Set<? super MethodSignature> addedSignatures, boolean generateDefaultMethods) {
123     for (CandidateInfo candidate : OverrideImplementExploreUtil.getMethodsToOverrideImplement(parent, implemented)) {
124       PsiMethod baseMethod = (PsiMethod)candidate.getElement();
125       PsiClass baseClass = baseMethod.getContainingClass();
126       PsiSubstitutor substitutor = candidate.getSubstitutor();
127       if (!baseMethod.isConstructor() && baseClass != null && addedSignatures.add(baseMethod.getSignature(substitutor))) {
128         result.addElement(createOverridingLookupElement(implemented, baseMethod, baseClass, substitutor, generateDefaultMethods, parent));
129       }
130     }
131   }
132
133   private static LookupElement createOverridingLookupElement(boolean implemented,
134                                                              PsiMethod baseMethod,
135                                                              PsiClass baseClass, PsiSubstitutor substitutor, boolean generateDefaultMethods, PsiClass targetClass) {
136
137     RowIcon icon = new RowIcon(baseMethod.getIcon(0), implemented ? AllIcons.Gutter.ImplementingMethod : AllIcons.Gutter.OverridingMethod);
138     return createGenerateMethodElement(baseMethod, substitutor, icon, baseClass.getName(), new InsertHandler<LookupElement>() {
139       @Override
140       public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) {
141         removeLookupString(context);
142
143         final PsiClass parent = PsiTreeUtil.findElementOfClassAtOffset(context.getFile(), context.getStartOffset(), PsiClass.class, false);
144         if (parent == null) return;
145
146         try (AccessToken ignored = generateDefaultMethods ? forceDefaultMethodsInside() : AccessToken.EMPTY_ACCESS_TOKEN) {
147           List<PsiMethod> prototypes = OverrideImplementUtil.overrideOrImplementMethod(parent, baseMethod, false);
148           insertGenerationInfos(context, OverrideImplementUtil.convert2GenerationInfos(prototypes));
149         }
150       }
151
152     }, generateDefaultMethods, targetClass);
153   }
154
155   private static AccessToken forceDefaultMethodsInside() {
156     CommandProcessor instance = CommandProcessor.getInstance();
157     String commandName = instance.getCurrentCommandName();
158     instance.setCurrentCommandName(OverrideImplementUtil.IMPLEMENT_COMMAND_MARKER);
159     return new AccessToken() {
160       @Override
161       public void finish() {
162         instance.setCurrentCommandName(commandName);
163       }
164     };
165   }
166
167   private static void insertGenerationInfos(InsertionContext context, List<PsiGenerationInfo<PsiMethod>> infos) {
168     List<PsiGenerationInfo<PsiMethod>> newInfos = GenerateMembersUtil
169       .insertMembersAtOffset(context.getFile(), context.getStartOffset(), infos);
170     if (!newInfos.isEmpty()) {
171       final List<PsiElement> elements = new ArrayList<>();
172       for (GenerationInfo member : newInfos) {
173         if (!(member instanceof TemplateGenerationInfo)) {
174           ContainerUtil.addIfNotNull(elements, member.getPsiMember());
175         }
176       }
177
178       newInfos.get(0).positionCaret(context.getEditor(), true);
179       GlobalInspectionContextBase.cleanupElements(context.getProject(), null, elements.toArray(PsiElement.EMPTY_ARRAY));
180     }
181   }
182
183   private static LookupElement createGenerateMethodElement(PsiMethod prototype,
184                                                            PsiSubstitutor substitutor,
185                                                            Icon icon,
186                                                            String typeText, InsertHandler<LookupElement> insertHandler,
187                                                            boolean generateDefaultMethod,
188                                                            PsiClass targetClass) {
189     String methodName = prototype.getName();
190
191     String visibility = VisibilityUtil.getVisibilityModifier(prototype.getModifierList());
192     String modifiers = (visibility == PsiModifier.PACKAGE_LOCAL || visibility == PsiModifier.PUBLIC && targetClass.isInterface() ? "" : visibility + " ");
193     if (generateDefaultMethod) {
194       modifiers = "default " + modifiers;
195     }
196
197     PsiType type = substitutor.substitute(prototype.getReturnType());
198     String signature = modifiers + (type == null ? "" : type.getPresentableText() + " ") + methodName;
199
200     String parameters = "(" + StringUtil.join(prototype.getParameterList().getParameters(),
201                                               p -> getShortParameterName(substitutor, p) + " " + p.getName(),
202                                               ", ") + ")";
203
204     String overrideSignature = " @Override " + signature; // leading space to make it a middle match, under all annotation suggestions
205     LookupElementBuilder element = LookupElementBuilder.create(prototype, signature).withLookupString(methodName).
206       withLookupString(signature).withLookupString(overrideSignature).withInsertHandler(insertHandler).
207       appendTailText(parameters, false).appendTailText(" {...}", true).withTypeText(typeText).withIcon(icon);
208     if (prototype.isDeprecated()) {
209       element = element.withStrikeoutness(true);
210     }
211     element.putUserData(GENERATE_ELEMENT, true);
212     return PrioritizedLookupElement.withPriority(element, -1);
213   }
214
215   @NotNull
216   private static String getShortParameterName(PsiSubstitutor substitutor, PsiParameter p) {
217     return PsiNameHelper.getShortClassName(substitutor.substitute(p.getType()).getPresentableText(false));
218   }
219
220   private static class NoMiddleMatchesAfterSpace extends CamelHumpMatcher {
221     NoMiddleMatchesAfterSpace(String prefix) {
222       super(prefix);
223     }
224
225     @Override
226     public boolean prefixMatches(@NotNull LookupElement element) {
227       if (!super.prefixMatches(element)) return false;
228
229       if (!myPrefix.contains(" ")) return true;
230
231       String signature = element.getLookupString();
232       FList<TextRange> fragments = matchingFragments(signature);
233       return fragments == null || fragments.stream().noneMatch(f -> isMiddleMatch(signature, f));
234
235     }
236
237     private static boolean isMiddleMatch(String signature, TextRange fragment) {
238       int start = fragment.getStartOffset();
239       return start > 0 &&
240              Character.isJavaIdentifierPart(signature.charAt(start)) &&
241              Character.isJavaIdentifierPart(signature.charAt(start - 1));
242     }
243   }
244 }