IDEA-282576 [bookmark] Add a workaround for Kotlin file bookmarks
[idea/community.git] / java / java-impl / src / com / intellij / codeInsight / completion / JavaClassNameInsertHandler.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.AutoPopupController;
5 import com.intellij.codeInsight.ExpectedTypesProvider;
6 import com.intellij.codeInsight.NullableNotNullManager;
7 import com.intellij.codeInsight.daemon.impl.analysis.HighlightingFeature;
8 import com.intellij.codeInsight.lookup.Lookup;
9 import com.intellij.codeInsight.lookup.LookupElement;
10 import com.intellij.codeInsight.lookup.PsiTypeLookupItem;
11 import com.intellij.codeInspection.dataFlow.DfaPsiUtil;
12 import com.intellij.openapi.editor.Editor;
13 import com.intellij.openapi.editor.EditorModificationUtilEx;
14 import com.intellij.openapi.project.Project;
15 import com.intellij.pom.java.LanguageLevel;
16 import com.intellij.psi.*;
17 import com.intellij.psi.codeStyle.JavaCodeStyleManager;
18 import com.intellij.psi.codeStyle.JavaCodeStyleSettings;
19 import com.intellij.psi.filters.FilterPositionUtil;
20 import com.intellij.psi.impl.source.codeStyle.ImportHelper;
21 import com.intellij.psi.javadoc.PsiDocComment;
22 import com.intellij.psi.javadoc.PsiDocTag;
23 import com.intellij.psi.util.PsiTreeUtil;
24 import com.intellij.psi.util.PsiUtil;
25 import com.intellij.util.containers.ContainerUtil;
26 import com.siyeh.ig.psiutils.MethodCallUtils;
27 import org.jetbrains.annotations.NotNull;
28 import org.jetbrains.annotations.Nullable;
29
30 import static com.intellij.psi.codeStyle.JavaCodeStyleSettings.*;
31
32 /**
33 * @author peter
34 */
35 class JavaClassNameInsertHandler implements InsertHandler<JavaPsiClassReferenceElement> {
36   static final InsertHandler<JavaPsiClassReferenceElement> JAVA_CLASS_INSERT_HANDLER = new JavaClassNameInsertHandler();
37
38   @Override
39   public void handleInsert(@NotNull final InsertionContext context, @NotNull final JavaPsiClassReferenceElement item) {
40     int offset = context.getTailOffset() - 1;
41     final PsiFile file = context.getFile();
42     PsiImportStatementBase importStatement = PsiTreeUtil.findElementOfClassAtOffset(file, offset, PsiImportStatementBase.class, false);
43     if (importStatement != null) {
44       PsiJavaCodeReferenceElement ref = findJavaReference(file, offset);
45       String qname = item.getQualifiedName();
46       if (qname != null && (ref == null || !qname.equals(ref.getCanonicalText()))) {
47         AllClassesGetter.INSERT_FQN.handleInsert(context, item);
48       }
49       if (importStatement instanceof PsiImportStaticStatement) {
50         context.setAddCompletionChar(false);
51         EditorModificationUtilEx.insertStringAtCaret(context.getEditor(), ".");
52       }
53       return;
54     }
55
56     PsiElement position = file.findElementAt(offset);
57     PsiJavaCodeReferenceElement ref = position != null && position.getParent() instanceof PsiJavaCodeReferenceElement ?
58                                       (PsiJavaCodeReferenceElement) position.getParent() : null;
59     PsiClass psiClass = item.getObject();
60     SmartPsiElementPointer<PsiClass> classPointer = SmartPointerManager.createPointer(psiClass);
61     final Project project = context.getProject();
62
63     final Editor editor = context.getEditor();
64     final char c = context.getCompletionChar();
65     if (c == '#') {
66       context.setLaterRunnable(() -> new CodeCompletionHandlerBase(CompletionType.BASIC).invokeCompletion(project, editor));
67     } else if (c == '.' && PsiTreeUtil.getParentOfType(position, PsiParameterList.class) == null) {
68       AutoPopupController.getInstance(context.getProject()).autoPopupMemberLookup(context.getEditor(), null);
69     }
70
71     String qname = psiClass.getQualifiedName();
72     if (qname != null && PsiTreeUtil.getParentOfType(position, PsiDocComment.class, false) != null &&
73         (ref == null || !ref.isQualified()) &&
74         shouldInsertFqnInJavadoc(item, file)) {
75       context.getDocument().replaceString(context.getStartOffset(), context.getTailOffset(), qname);
76       return;
77     }
78
79     if (ref != null && PsiTreeUtil.getParentOfType(position, PsiDocTag.class) != null && ref.isReferenceTo(psiClass)) {
80       return;
81     }
82
83     OffsetKey refEnd = context.trackOffset(context.getTailOffset(), true);
84
85     boolean fillTypeArgs = context.getCompletionChar() == '<';
86     if (fillTypeArgs) {
87       context.setAddCompletionChar(false);
88     }
89
90     PsiTypeLookupItem.addImportForItem(context, psiClass);
91     if (!context.getOffsetMap().containsOffset(refEnd)) {
92       return;
93     }
94
95     context.setTailOffset(context.getOffset(refEnd));
96     refEnd = context.trackOffset(context.getTailOffset(), false);
97
98     context.commitDocument();
99
100     // Restore elements after commit
101     position = file.findElementAt(context.getTailOffset() - 1);
102     ref = position != null && position.getParent() instanceof PsiJavaCodeReferenceElement ?
103           (PsiJavaCodeReferenceElement)position.getParent() : null;
104
105     if (c == '!' || c == '?') {
106       context.setAddCompletionChar(false);
107       if (ref != null && !(ref instanceof PsiReferenceExpression) &&
108           !ref.textContains('@') && !(ref.getParent() instanceof PsiAnnotation)) {
109         NullableNotNullManager manager = NullableNotNullManager.getInstance(project);
110         String annoName = c == '!' ? manager.getDefaultNotNull() : manager.getDefaultNullable();
111         PsiClass cls = JavaPsiFacade.getInstance(project).findClass(annoName, file.getResolveScope());
112         if (cls != null) {
113           PsiJavaCodeReferenceElement newRef =
114             JavaPsiFacade.getElementFactory(project).createReferenceFromText('@' + annoName + ' ' + ref.getText(), ref);
115           JavaCodeStyleManager.getInstance(project).shortenClassReferences(ref.replace(newRef));
116         }
117       }
118     }
119
120     if (ref != null && HighlightingFeature.PATTERNS.isAvailable(ref) && psiClass.getTypeParameters().length > 0) {
121       PsiExpression instanceOfOperand = JavaCompletionUtil.getInstanceOfOperand(ref);
122       if (instanceOfOperand != null) {
123         PsiClassType origType = JavaPsiFacade.getElementFactory(project).createType(psiClass);
124         PsiType generified = DfaPsiUtil.tryGenerify(instanceOfOperand, origType);
125         if (generified != null && generified != origType) {
126           String completeTypeText = generified.getCanonicalText();
127           PsiJavaCodeReferenceElement newRef =
128             JavaPsiFacade.getElementFactory(project).createReferenceFromText(completeTypeText, ref);
129           PsiElement resultingRef = JavaCodeStyleManager.getInstance(project).shortenClassReferences(ref.replace(newRef));
130           context.getEditor().getCaretModel().moveToOffset(resultingRef.getTextRange().getEndOffset());
131           return;
132         }
133       }
134     }
135
136     psiClass = classPointer.dereference();
137
138     if (item.getUserData(JavaChainLookupElement.CHAIN_QUALIFIER) == null &&
139         shouldInsertParentheses(file.findElementAt(context.getTailOffset() - 1))) {
140       if (context.getCompletionChar() == Lookup.REPLACE_SELECT_CHAR) {
141         overwriteTopmostReference(context);
142         context.commitDocument();
143       }
144       if (psiClass != null && ConstructorInsertHandler.insertParentheses(context, item, psiClass, false)) {
145         fillTypeArgs |= psiClass.hasTypeParameters() && PsiUtil.getLanguageLevel(file).isAtLeast(LanguageLevel.JDK_1_5);
146       }
147     }
148     else if (insertingAnnotation(context, item)) {
149       if (psiClass != null && shouldHaveAnnotationParameters(psiClass)) {
150         JavaCompletionUtil.insertParentheses(context, item, false, true);
151       }
152       if (context.getCompletionChar() == Lookup.NORMAL_SELECT_CHAR || context.getCompletionChar() == Lookup.COMPLETE_STATEMENT_SELECT_CHAR) {
153         CharSequence text = context.getDocument().getCharsSequence();
154         int tail = context.getTailOffset();
155         if (text.length() > tail && Character.isLetter(text.charAt(tail))) {
156           context.getDocument().insertString(tail, " ");
157         }
158       }
159     }
160
161     if (fillTypeArgs && context.getCompletionChar() != '(') {
162       JavaCompletionUtil.promptTypeArgs(context, context.getOffset(refEnd));
163     }
164     else if (context.getCompletionChar() == Lookup.COMPLETE_STATEMENT_SELECT_CHAR &&
165              psiClass != null && psiClass.getTypeParameters().length == 1 &&
166              PsiUtil.getLanguageLevel(file).isAtLeast(LanguageLevel.JDK_1_5)) {
167       wrapFollowingTypeInGenerics(context, context.getOffset(refEnd));
168     }
169   }
170
171   private static void wrapFollowingTypeInGenerics(InsertionContext context, int refEnd) {
172     PsiTypeElement typeElem = PsiTreeUtil.findElementOfClassAtOffset(context.getFile(), refEnd, PsiTypeElement.class, false);
173     if (typeElem != null) {
174       int typeEnd = typeElem.getTextRange().getEndOffset();
175       context.getDocument().insertString(typeEnd, ">");
176       context.getEditor().getCaretModel().moveToOffset(typeEnd + 1);
177       context.getDocument().insertString(refEnd, "<");
178       context.setAddCompletionChar(false);
179     }
180   }
181
182   @Nullable static PsiJavaCodeReferenceElement findJavaReference(@NotNull PsiFile file, int offset) {
183     return PsiTreeUtil.findElementOfClassAtOffset(file, offset, PsiJavaCodeReferenceElement.class, false);
184   }
185
186   static void overwriteTopmostReference(InsertionContext context) {
187     context.commitDocument();
188     PsiJavaCodeReferenceElement ref = findJavaReference(context.getFile(), context.getTailOffset() - 1);
189     if (ref != null) {
190       while (ref.getParent() instanceof PsiJavaCodeReferenceElement) ref = (PsiJavaCodeReferenceElement)ref.getParent();
191       context.getDocument().deleteString(context.getTailOffset(), ref.getTextRange().getEndOffset());
192     }
193   }
194
195   private static boolean shouldInsertFqnInJavadoc(@NotNull JavaPsiClassReferenceElement item, @NotNull PsiFile file) {
196     JavaCodeStyleSettings javaSettings = getInstance(file);
197     
198     switch (javaSettings.CLASS_NAMES_IN_JAVADOC) {
199       case FULLY_QUALIFY_NAMES_ALWAYS:
200         return true;
201       case SHORTEN_NAMES_ALWAYS_AND_ADD_IMPORT:
202         return false;
203       case FULLY_QUALIFY_NAMES_IF_NOT_IMPORTED:
204         if (file instanceof PsiJavaFile) {
205           PsiJavaFile javaFile = ((PsiJavaFile)file);
206           return item.getQualifiedName() != null && !ImportHelper.isAlreadyImported(javaFile, item.getQualifiedName());
207         }
208       default:
209         return false;
210     }
211   }
212
213   private static boolean shouldInsertParentheses(PsiElement position) {
214     final PsiJavaCodeReferenceElement ref = PsiTreeUtil.getParentOfType(position, PsiJavaCodeReferenceElement.class);
215     if (ref == null) {
216       return false;
217     }
218
219     final PsiReferenceParameterList parameterList = ref.getParameterList();
220     if (parameterList != null && parameterList.getTextLength() > 0) {
221       return false;
222     }
223
224     final PsiElement prevElement = FilterPositionUtil.searchNonSpaceNonCommentBack(ref);
225     if (prevElement != null && prevElement.getParent() instanceof PsiNewExpression) {
226       return !isArrayTypeExpected((PsiExpression)prevElement.getParent());
227     }
228
229     return false;
230   }
231
232   static boolean isArrayTypeExpected(PsiExpression expr) {
233     return ContainerUtil.exists(ExpectedTypesProvider.getExpectedTypes(expr, true),
234                                 info -> {
235                                   if (info.getType() instanceof PsiArrayType) {
236                                     PsiMethod method = info.getCalledMethod();
237                                     return method == null || !method.isVarArgs() || !(expr.getParent() instanceof PsiExpressionList) ||
238                                            MethodCallUtils.getParameterForArgument(expr) != null;
239                                   }
240                                   return false;
241                                 });
242   }
243
244   private static boolean insertingAnnotation(InsertionContext context, LookupElement item) {
245     final Object obj = item.getObject();
246     if (!(obj instanceof PsiClass) || !((PsiClass)obj).isAnnotationType()) return false;
247
248     PsiElement leaf = context.getFile().findElementAt(context.getStartOffset());
249     PsiAnnotation anno = PsiTreeUtil.getParentOfType(leaf, PsiAnnotation.class);
250     return anno != null && PsiTreeUtil.isAncestor(anno.getNameReferenceElement(), leaf, false);
251   }
252
253   static boolean shouldHaveAnnotationParameters(PsiClass annoClass) {
254     for (PsiMethod m : annoClass.getMethods()) {
255       if (!PsiUtil.isAnnotationMethod(m)) continue;
256       if (((PsiAnnotationMethod)m).getDefaultValue() == null) return true;
257     }
258     return false;
259   }
260 }