summaryrefslogtreecommitdiff
path: root/platform/analysis-impl/src/com/intellij/codeInsight/completion/CompletionUtil.java
blob: df0c47c9771262a0e0af83265cd09969c46fda97 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
// Copyright 2000-2020 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.
package com.intellij.codeInsight.completion;

import com.intellij.codeInsight.TailType;
import com.intellij.codeInsight.lookup.Lookup;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupValueWithPsiElement;
import com.intellij.diagnostic.ThreadDumper;
import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.openapi.diagnostic.Attachment;
import com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.patterns.CharPattern;
import com.intellij.patterns.ElementPattern;
import com.intellij.psi.*;
import com.intellij.psi.filters.TrueFilter;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.util.UnmodifiableIterator;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;

public final class CompletionUtil {

  private static final CompletionData ourGenericCompletionData = new CompletionData() {
    {
      final CompletionVariant variant = new CompletionVariant(PsiElement.class, TrueFilter.INSTANCE);
      variant.addCompletionFilter(TrueFilter.INSTANCE, TailType.NONE);
      registerVariant(variant);
    }
  };
  public static final @NonNls String DUMMY_IDENTIFIER = CompletionInitializationContext.DUMMY_IDENTIFIER;
  public static final @NonNls String DUMMY_IDENTIFIER_TRIMMED = DUMMY_IDENTIFIER.trim();

  @Nullable
  public static CompletionData getCompletionDataByElement(@Nullable final PsiElement position, @NotNull PsiFile originalFile) {
    if (position == null) return null;
    return ourGenericCompletionData;
  }

  public static boolean shouldShowFeature(CompletionParameters parameters, @NonNls final String id) {
    return shouldShowFeature(parameters.getPosition().getProject(), id);
  }

  public static boolean shouldShowFeature(Project project, @NonNls String id) {
    if (FeatureUsageTracker.getInstance().isToBeAdvertisedInLookup(id, project)) {
      FeatureUsageTracker.getInstance().triggerFeatureShown(id);
      return true;
    }
    return false;
  }

  /**
   * @return a prefix for completion matching, calculated from the given parameters.
   * The prefix is the longest substring from inside {@code parameters.getPosition()}'s text,
   * ending at {@code parameters.getOffset()}, being a valid Java identifier.
   */
  @NotNull
  public static String findJavaIdentifierPrefix(@NotNull CompletionParameters parameters) {
    return findJavaIdentifierPrefix(parameters.getPosition(), parameters.getOffset());
  }

  /**
   * @return a prefix for completion matching, calculated from the given parameters.
   * The prefix is the longest substring from inside {@code position}'s text,
   * ending at {@code offsetInFile}, being a valid Java identifier.
   */
  @NotNull
  public static String findJavaIdentifierPrefix(@Nullable PsiElement position, int offsetInFile) {
    return findIdentifierPrefix(position, offsetInFile, CharPattern.javaIdentifierPartCharacter(), CharPattern.javaIdentifierStartCharacter());
  }

  /**
   * @return the result of {@link #findReferencePrefix}, or {@link #findAlphanumericPrefix} if there's no reference.
   */
  @NotNull
  public static String findReferenceOrAlphanumericPrefix(@NotNull CompletionParameters parameters) {
    String prefix = findReferencePrefix(parameters);
    return prefix == null ? findAlphanumericPrefix(parameters) : prefix;
  }

  /**
   * @return an alphanumertic prefix for completion matching, calculated from the given parameters.
   * The prefix is the longest substring from inside {@code parameters.getPosition()}'s text,
   * ending at {@code parameters.getOffset()}, consisting of letters and digits.
   */
  @NotNull
  public static String findAlphanumericPrefix(@NotNull CompletionParameters parameters) {
    return findIdentifierPrefix(parameters.getPosition().getContainingFile(), parameters.getOffset(), CharPattern.letterOrDigitCharacter(), CharPattern.letterOrDigitCharacter());
  }

  /**
   * @return a prefix for completion matching, calculated from the given element's text and the offsets.
   * The prefix is the longest substring from inside {@code position}'s text,
   * ending at {@code offsetInFile}, beginning with a character
   * satisfying {@code idStart}, and with all other characters satisfying {@code idPart}.
   */
  @NotNull
  public static String findIdentifierPrefix(@Nullable PsiElement position, int offsetInFile,
                                            @NotNull ElementPattern<Character> idPart,
                                            @NotNull ElementPattern<Character> idStart) {
    if (position == null) return "";
    int startOffset = position.getTextRange().getStartOffset();
    return findInText(offsetInFile, startOffset, idPart, idStart, position.getNode().getChars());
  }

  public static String findIdentifierPrefix(@NotNull Document document, int offset, ElementPattern<Character> idPart,
                                            ElementPattern<Character> idStart) {
    final CharSequence text = document.getImmutableCharSequence();
    return findInText(offset, 0, idPart, idStart, text);
  }

  @NotNull
  private static String findInText(int offset, int startOffset, ElementPattern<Character> idPart, ElementPattern<Character> idStart, CharSequence text) {
    final int offsetInElement = offset - startOffset;
    int start = offsetInElement - 1;
    while (start >=0) {
      if (!idPart.accepts(text.charAt(start))) break;
      --start;
    }
    while (start + 1 < offsetInElement && !idStart.accepts(text.charAt(start + 1))) {
      start++;
    }

    return text.subSequence(start + 1, offsetInElement).toString().trim();
  }

  /**
   * @return a prefix from completion matching calculated by a reference found at parameters' offset
   * (the reference text from the beginning until that offset),
   * or {@code null} if there's no reference there.
   */
  @Nullable
  public static String findReferencePrefix(@NotNull CompletionParameters parameters) {
    return findReferencePrefix(parameters.getPosition(), parameters.getOffset());
  }

  /**
   * @return a prefix from completion matching calculated by a reference found at the given offset
   * (the reference text from the beginning until that offset),
   * or {@code null} if there's no reference there.
   */
  @Nullable
  public static String findReferencePrefix(@NotNull PsiElement position, int offsetInFile) {
    try {
      PsiUtilCore.ensureValid(position);
      PsiReference ref = position.getContainingFile().findReferenceAt(offsetInFile);
      if (ref != null) {
        PsiElement element = ref.getElement();
        int offsetInElement = offsetInFile - element.getTextRange().getStartOffset();
        for (TextRange refRange : ReferenceRange.getRanges(ref)) {
          if (refRange.contains(offsetInElement)) {
            int beginIndex = refRange.getStartOffset();
            String text = element.getText();
            if (beginIndex < 0 || beginIndex > offsetInElement || offsetInElement > text.length()) {
              throw new AssertionError("Inconsistent reference range:" +
                                       " ref=" + ref.getClass() +
                                       " element=" + element.getClass() +
                                       " ref.start=" + refRange.getStartOffset() +
                                       " offset=" + offsetInElement +
                                       " psi.length=" + text.length());
            }
            return text.substring(beginIndex, offsetInElement);
          }
        }
      }
    }
    catch (IndexNotReadyException ignored) {
    }
    return null;
  }

  public static InsertionContext emulateInsertion(InsertionContext oldContext, int newStart, final LookupElement item) {
    final InsertionContext newContext = newContext(oldContext, item);
    emulateInsertion(item, newStart, newContext);
    return newContext;
  }

  private static InsertionContext newContext(InsertionContext oldContext, LookupElement forElement) {
    final Editor editor = oldContext.getEditor();
    return new InsertionContext(new OffsetMap(editor.getDocument()), Lookup.AUTO_INSERT_SELECT_CHAR, new LookupElement[]{forElement}, oldContext.getFile(), editor,
                                oldContext.shouldAddCompletionChar());
  }

  public static InsertionContext newContext(InsertionContext oldContext, LookupElement forElement, int startOffset, int tailOffset) {
    final InsertionContext context = newContext(oldContext, forElement);
    setOffsets(context, startOffset, tailOffset);
    return context;
  }

  public static void emulateInsertion(LookupElement item, int offset, InsertionContext context) {
    setOffsets(context, offset, offset);

    final Editor editor = context.getEditor();
    final Document document = editor.getDocument();
    final String lookupString = item.getLookupString();

    document.insertString(offset, lookupString);
    editor.getCaretModel().moveToOffset(context.getTailOffset());
    PsiDocumentManager.getInstance(context.getProject()).commitDocument(document);
    item.handleInsert(context);
    PsiDocumentManager.getInstance(context.getProject()).doPostponedOperationsAndUnblockDocument(document);
  }

  private static void setOffsets(InsertionContext context, int offset, final int tailOffset) {
    final OffsetMap offsetMap = context.getOffsetMap();
    offsetMap.addOffset(CompletionInitializationContext.START_OFFSET, offset);
    offsetMap.addOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET, tailOffset);
    offsetMap.addOffset(CompletionInitializationContext.SELECTION_END_OFFSET, tailOffset);
    context.setTailOffset(tailOffset);
  }

  @Nullable
  public static PsiElement getTargetElement(LookupElement lookupElement) {
    PsiElement psiElement = lookupElement.getPsiElement();
    if (psiElement != null && psiElement.isValid()) {
      return getOriginalElement(psiElement);
    }

    Object object = lookupElement.getObject();
    if (object instanceof LookupValueWithPsiElement) {
      final PsiElement element = ((LookupValueWithPsiElement)object).getElement();
      if (element != null && element.isValid()) return getOriginalElement(element);
    }

    return null;
  }

  @Nullable
  public static <T extends PsiElement> T getOriginalElement(@NotNull T psi) {
    return CompletionUtilCoreImpl.getOriginalElement(psi);
  }

  @NotNull
  public static <T extends PsiElement> T getOriginalOrSelf(@NotNull T psi) {
    final T element = getOriginalElement(psi);
    return element == null ? psi : element;
  }

  public static Iterable<String> iterateLookupStrings(@NotNull final LookupElement element) {
    return new Iterable<>() {
      @NotNull
      @Override
      public Iterator<String> iterator() {
        final Iterator<String> original = element.getAllLookupStrings().iterator();
        return new UnmodifiableIterator<>(original) {
          @Override
          public boolean hasNext() {
            try {
              return super.hasNext();
            }
            catch (ConcurrentModificationException e) {
              throw handleCME(e);
            }
          }

          @Override
          public String next() {
            try {
              return super.next();
            }
            catch (ConcurrentModificationException e) {
              throw handleCME(e);
            }
          }

          private RuntimeException handleCME(ConcurrentModificationException cme) {
            RuntimeExceptionWithAttachments ewa = new RuntimeExceptionWithAttachments(
              "Error while traversing lookup strings of " + element + " of " + element.getClass(),
              (String)null,
              new Attachment("threadDump.txt", ThreadDumper.dumpThreadsToString()));
            ewa.initCause(cme);
            return ewa;
          }
        };
      }
    };
  }

  @NotNull
  @ApiStatus.Internal
  public static CompletionAssertions.WatchingInsertionContext createInsertionContext(@Nullable List<LookupElement> lookupItems,
                                                                                     LookupElement item,
                                                                                     char completionChar,
                                                                                     Editor editor,
                                                                                     PsiFile psiFile,
                                                                                     int caretOffset,
                                                                                     int idEndOffset,
                                                                                     OffsetMap offsetMap) {
    int initialStartOffset = Math.max(0, caretOffset - item.getLookupString().length());

    return createInsertionContext(lookupItems, completionChar, editor, psiFile, initialStartOffset, caretOffset, idEndOffset, offsetMap);
  }

  @NotNull
  @ApiStatus.Internal
  public static CompletionAssertions.WatchingInsertionContext createInsertionContext(@Nullable List<LookupElement> lookupItems,
                                                                                     char completionChar,
                                                                                     Editor editor,
                                                                                     PsiFile psiFile,
                                                                                     int startOffset,
                                                                                     int caretOffset,
                                                                                     int idEndOffset,
                                                                                     OffsetMap offsetMap) {

    offsetMap.addOffset(CompletionInitializationContext.START_OFFSET, startOffset);
    offsetMap.addOffset(CompletionInitializationContext.SELECTION_END_OFFSET, caretOffset);
    offsetMap.addOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET, idEndOffset);

    List<LookupElement> items = lookupItems == null ? Collections.emptyList() : lookupItems;

    return new CompletionAssertions.WatchingInsertionContext(offsetMap, psiFile, completionChar, items, editor);
  }

  @ApiStatus.Internal
  public static int calcIdEndOffset(OffsetMap offsetMap, Editor editor, Integer initOffset) {
    return offsetMap.containsOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET) ?
           offsetMap.getOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET) :
           CompletionInitializationContext.calcDefaultIdentifierEnd(editor, initOffset);
  }

  @ApiStatus.Internal
  public static int calcIdEndOffset(CompletionProcessEx indicator) {
    return calcIdEndOffset(indicator.getOffsetMap(), indicator.getEditor(), indicator.getCaret().getOffset());
  }

}