148b9afbb49267aa4c4980bdfe061bd8a51c6b3f
[idea/community.git] / platform / analysis-impl / src / com / intellij / codeInsight / completion / CompletionUtil.java
1 // Copyright 2000-2019 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
3 package com.intellij.codeInsight.completion;
4
5 import com.intellij.codeInsight.TailType;
6 import com.intellij.codeInsight.lookup.Lookup;
7 import com.intellij.codeInsight.lookup.LookupElement;
8 import com.intellij.codeInsight.lookup.LookupValueWithPsiElement;
9 import com.intellij.diagnostic.ThreadDumper;
10 import com.intellij.featureStatistics.FeatureUsageTracker;
11 import com.intellij.lang.Language;
12 import com.intellij.openapi.diagnostic.Attachment;
13 import com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments;
14 import com.intellij.openapi.editor.Document;
15 import com.intellij.openapi.editor.Editor;
16 import com.intellij.openapi.fileTypes.FileType;
17 import com.intellij.openapi.project.Project;
18 import com.intellij.patterns.CharPattern;
19 import com.intellij.patterns.ElementPattern;
20 import com.intellij.psi.PsiDocumentManager;
21 import com.intellij.psi.PsiElement;
22 import com.intellij.psi.PsiFile;
23 import com.intellij.psi.filters.TrueFilter;
24 import com.intellij.util.UnmodifiableIterator;
25 import org.jetbrains.annotations.ApiStatus;
26 import org.jetbrains.annotations.NonNls;
27 import org.jetbrains.annotations.NotNull;
28 import org.jetbrains.annotations.Nullable;
29
30 import java.util.Collections;
31 import java.util.ConcurrentModificationException;
32 import java.util.Iterator;
33 import java.util.List;
34
35 public class CompletionUtil {
36
37   private static final CompletionData ourGenericCompletionData = new CompletionData() {
38     {
39       final CompletionVariant variant = new CompletionVariant(PsiElement.class, TrueFilter.INSTANCE);
40       variant.addCompletionFilter(TrueFilter.INSTANCE, TailType.NONE);
41       registerVariant(variant);
42     }
43   };
44   public static final @NonNls String DUMMY_IDENTIFIER = CompletionInitializationContext.DUMMY_IDENTIFIER;
45   public static final @NonNls String DUMMY_IDENTIFIER_TRIMMED = DUMMY_IDENTIFIER.trim();
46
47   @Nullable
48   public static CompletionData getCompletionDataByElement(@Nullable final PsiElement position, @NotNull PsiFile originalFile) {
49     if (position == null) return null;
50
51     PsiElement parent = position.getParent();
52     Language language = parent == null ? position.getLanguage() : parent.getLanguage();
53     final FileType fileType = language.getAssociatedFileType();
54     if (fileType != null) {
55       final CompletionData mainData = getCompletionDataByFileType(fileType);
56       if (mainData != null) {
57         return mainData;
58       }
59     }
60
61     final CompletionData mainData = getCompletionDataByFileType(originalFile.getFileType());
62     return mainData != null ? mainData : ourGenericCompletionData;
63   }
64
65   @Nullable
66   private static CompletionData getCompletionDataByFileType(FileType fileType) {
67     for(CompletionDataEP ep: CompletionDataEP.EP_NAME.getExtensionList()) {
68       if (ep.fileType.equals(fileType.getName())) {
69         return ep.getHandler();
70       }
71     }
72     return null;
73   }
74
75   public static boolean shouldShowFeature(CompletionParameters parameters, @NonNls final String id) {
76     return shouldShowFeature(parameters.getPosition().getProject(), id);
77   }
78
79   public static boolean shouldShowFeature(Project project, @NonNls String id) {
80     if (FeatureUsageTracker.getInstance().isToBeAdvertisedInLookup(id, project)) {
81       FeatureUsageTracker.getInstance().triggerFeatureShown(id);
82       return true;
83     }
84     return false;
85   }
86
87   public static String findJavaIdentifierPrefix(CompletionParameters parameters) {
88     return findJavaIdentifierPrefix(parameters.getPosition(), parameters.getOffset());
89   }
90
91   public static String findJavaIdentifierPrefix(final PsiElement insertedElement, final int offset) {
92     return findIdentifierPrefix(insertedElement, offset, CharPattern.javaIdentifierPartCharacter(), CharPattern.javaIdentifierStartCharacter());
93   }
94
95   public static String findReferenceOrAlphanumericPrefix(CompletionParameters parameters) {
96     String prefix = findReferencePrefix(parameters);
97     return prefix == null ? findAlphanumericPrefix(parameters) : prefix;
98   }
99
100   public static String findAlphanumericPrefix(CompletionParameters parameters) {
101     return findIdentifierPrefix(parameters.getPosition().getContainingFile(), parameters.getOffset(), CharPattern.letterOrDigitCharacter(), CharPattern.letterOrDigitCharacter());
102   }
103
104   public static String findIdentifierPrefix(PsiElement insertedElement, int offset, ElementPattern<Character> idPart,
105                                             ElementPattern<Character> idStart) {
106     if (insertedElement == null) return "";
107     int startOffset = insertedElement.getTextRange().getStartOffset();
108     return findInText(offset, startOffset, idPart, idStart, insertedElement.getNode().getChars());
109   }
110
111   @SuppressWarnings("unused") // used in Rider
112   public static String findIdentifierPrefix(@NotNull Document document, int offset, ElementPattern<Character> idPart,
113                                             ElementPattern<Character> idStart) {
114     final String text = document.getText();
115     return findInText(offset, 0, idPart, idStart, text);
116   }
117
118   @NotNull
119   private static String findInText(int offset, int startOffset, ElementPattern<Character> idPart, ElementPattern<Character> idStart, CharSequence text) {
120     final int offsetInElement = offset - startOffset;
121     int start = offsetInElement - 1;
122     while (start >=0) {
123       if (!idPart.accepts(text.charAt(start))) break;
124       --start;
125     }
126     while (start + 1 < offsetInElement && !idStart.accepts(text.charAt(start + 1))) {
127       start++;
128     }
129
130     return text.subSequence(start + 1, offsetInElement).toString().trim();
131   }
132
133   @Nullable
134   public static String findReferencePrefix(CompletionParameters parameters) {
135     return CompletionData.getReferencePrefix(parameters.getPosition(), parameters.getOffset());
136   }
137
138
139   public static InsertionContext emulateInsertion(InsertionContext oldContext, int newStart, final LookupElement item) {
140     final InsertionContext newContext = newContext(oldContext, item);
141     emulateInsertion(item, newStart, newContext);
142     return newContext;
143   }
144
145   private static InsertionContext newContext(InsertionContext oldContext, LookupElement forElement) {
146     final Editor editor = oldContext.getEditor();
147     return new InsertionContext(new OffsetMap(editor.getDocument()), Lookup.AUTO_INSERT_SELECT_CHAR, new LookupElement[]{forElement}, oldContext.getFile(), editor,
148                                 oldContext.shouldAddCompletionChar());
149   }
150
151   public static InsertionContext newContext(InsertionContext oldContext, LookupElement forElement, int startOffset, int tailOffset) {
152     final InsertionContext context = newContext(oldContext, forElement);
153     setOffsets(context, startOffset, tailOffset);
154     return context;
155   }
156
157   public static void emulateInsertion(LookupElement item, int offset, InsertionContext context) {
158     setOffsets(context, offset, offset);
159
160     final Editor editor = context.getEditor();
161     final Document document = editor.getDocument();
162     final String lookupString = item.getLookupString();
163
164     document.insertString(offset, lookupString);
165     editor.getCaretModel().moveToOffset(context.getTailOffset());
166     PsiDocumentManager.getInstance(context.getProject()).commitDocument(document);
167     item.handleInsert(context);
168     PsiDocumentManager.getInstance(context.getProject()).doPostponedOperationsAndUnblockDocument(document);
169   }
170
171   private static void setOffsets(InsertionContext context, int offset, final int tailOffset) {
172     final OffsetMap offsetMap = context.getOffsetMap();
173     offsetMap.addOffset(CompletionInitializationContext.START_OFFSET, offset);
174     offsetMap.addOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET, tailOffset);
175     offsetMap.addOffset(CompletionInitializationContext.SELECTION_END_OFFSET, tailOffset);
176     context.setTailOffset(tailOffset);
177   }
178
179   @Nullable
180   public static PsiElement getTargetElement(LookupElement lookupElement) {
181     PsiElement psiElement = lookupElement.getPsiElement();
182     if (psiElement != null && psiElement.isValid()) {
183       return getOriginalElement(psiElement);
184     }
185
186     Object object = lookupElement.getObject();
187     if (object instanceof LookupValueWithPsiElement) {
188       final PsiElement element = ((LookupValueWithPsiElement)object).getElement();
189       if (element != null && element.isValid()) return getOriginalElement(element);
190     }
191
192     return null;
193   }
194
195   @Nullable
196   public static <T extends PsiElement> T getOriginalElement(@NotNull T psi) {
197     return CompletionUtilCoreImpl.getOriginalElement(psi);
198   }
199
200   @NotNull
201   public static <T extends PsiElement> T getOriginalOrSelf(@NotNull T psi) {
202     final T element = getOriginalElement(psi);
203     return element == null ? psi : element;
204   }
205
206   public static Iterable<String> iterateLookupStrings(@NotNull final LookupElement element) {
207     return new Iterable<String>() {
208       @NotNull
209       @Override
210       public Iterator<String> iterator() {
211         final Iterator<String> original = element.getAllLookupStrings().iterator();
212         return new UnmodifiableIterator<String>(original) {
213           @Override
214           public boolean hasNext() {
215             try {
216               return super.hasNext();
217             }
218             catch (ConcurrentModificationException e) {
219               throw handleCME(e);
220             }
221           }
222
223           @Override
224           public String next() {
225             try {
226               return super.next();
227             }
228             catch (ConcurrentModificationException e) {
229               throw handleCME(e);
230             }
231           }
232
233           private RuntimeException handleCME(ConcurrentModificationException cme) {
234             RuntimeExceptionWithAttachments ewa = new RuntimeExceptionWithAttachments(
235               "Error while traversing lookup strings of " + element + " of " + element.getClass(),
236               (String)null,
237               new Attachment("threadDump.txt", ThreadDumper.dumpThreadsToString()));
238             ewa.initCause(cme);
239             return ewa;
240           }
241         };
242       }
243     };
244   }
245
246   @NotNull
247   @ApiStatus.Internal
248   public static CompletionAssertions.WatchingInsertionContext createInsertionContext(@Nullable List<LookupElement> lookupItems,
249                                                                                      LookupElement item,
250                                                                                      char completionChar,
251                                                                                      Editor editor,
252                                                                                      PsiFile psiFile,
253                                                                                      int caretOffset,
254                                                                                      int idEndOffset,
255                                                                                      OffsetMap offsetMap) {
256     int initialStartOffset = Math.max(0, caretOffset - item.getLookupString().length());
257
258     offsetMap.addOffset(CompletionInitializationContext.START_OFFSET, initialStartOffset);
259     offsetMap.addOffset(CompletionInitializationContext.SELECTION_END_OFFSET, caretOffset);
260     offsetMap.addOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET, idEndOffset);
261
262     List<LookupElement> items = lookupItems == null ? Collections.emptyList() : lookupItems;
263
264     return new CompletionAssertions.WatchingInsertionContext(offsetMap, psiFile, completionChar, items, editor);
265   }
266
267   @ApiStatus.Internal
268   public static int calcIdEndOffset(OffsetMap offsetMap, Editor editor, Integer initOffset) {
269     return offsetMap.containsOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET) ?
270            offsetMap.getOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET) :
271            CompletionInitializationContext.calcDefaultIdentifierEnd(editor, initOffset);
272   }
273
274   @ApiStatus.Internal
275   public static int calcIdEndOffset(CompletionProcessEx indicator) {
276     return calcIdEndOffset(indicator.getOffsetMap(), indicator.getEditor(), indicator.getCaret().getOffset());
277   }
278
279 }