avoid expensive PSI copying in completion when the dummy identifier is empty, use this to speed up word completion

This commit is contained in:
peter
2019-04-16 18:19:33 +02:00
parent 35bf859a60
commit 32b759069f
4 changed files with 56 additions and 34 deletions
@@ -42,14 +42,6 @@ import java.util.List;
* A more generic way is to override default {@link #fillCompletionVariants(CompletionParameters, CompletionResultSet)} implementation
* and provide your own. It's easier to debug, but harder to write.<p>
*
* Q: What does the {@link CompletionParameters#getPosition()} return?<br>
* A: When completion is invoked, the file being edited is first copied (the original file can be accessed from {@link com.intellij.psi.PsiFile#getOriginalFile()}
* and {@link CompletionParameters#getOriginalFile()}. Then a special 'dummy identifier' string is inserted to the copied file at caret offset (removing the selection).
* Most often this string is an identifier (see {@link CompletionInitializationContext#DUMMY_IDENTIFIER}).
* This is usually done to guarantee that there'll always be some non-empty element there, which will be easy to describe via {@link ElementPattern}s.
* Also a reference can suddenly appear in that position, which will certainly help invoking its {@link PsiReference#getVariants()}.
* Dummy identifier string can be easily changed in {@link #beforeCompletion(CompletionInitializationContext)} method.<p>
*
* Q: How do I get automatic lookup element filtering by prefix?<br>
* A: When you return variants from reference ({@link PsiReference#getVariants()}), the filtering will be done
* automatically, with prefix taken as the reference text from its start ({@link PsiReference#getRangeInElement()}) to
@@ -150,7 +142,7 @@ public abstract class CompletionContributor {
* If you want to implement this functionality directly by overriding this method, the following is for you.
* Always check that parameters match your situation, and that completion type ({@link CompletionParameters#getCompletionType()}
* is of your favourite kind. This method is run inside a read action. If you do any long activity non-related to PSI in it, please
* ensure you call {@link com.intellij.openapi.progress.ProgressManager#checkCanceled()} often enough so that the completion process
* ensure you call {@link ProgressManager#checkCanceled()} often enough so that the completion process
* can be cancelled smoothly when the user begins to type in the editor.
*/
public void fillCompletionVariants(@NotNull final CompletionParameters parameters, @NotNull CompletionResultSet result) {
@@ -201,7 +193,7 @@ public abstract class CompletionContributor {
}
/**
* Called when the completion is finished quickly, lookup hasn't been shown and gives possibility to autoinsert some item (typically - the only one).
* Called when the completion is finished quickly, lookup hasn't been shown and gives possibility to auto-insert some item (typically - the only one).
*/
@Nullable
public AutoCompletionDecision handleAutoCompletionPossibility(@NotNull AutoCompletionContext context) {
@@ -50,6 +50,23 @@ public final class CompletionParameters {
return new CompletionParameters(myPosition, myOriginalFile, myCompletionType, myOffset, newCount, myEditor, myProcess);
}
/**
* Return the leaf PSI element in the "completion file" at offset {@link #getOffset()}.<p></p>
*
* "Completion file" is a PSI file used for completion purposes. Most often it's a non-physical copy of the file being edited
* (the original file can be accessed from {@link PsiFile#getOriginalFile()} or {@link #getOriginalFile()}).<p></p>
*
* A special 'dummy identifier' string is inserted to the copied file at caret offset (removing the selection).
* Most often this string is an identifier (see {@link CompletionInitializationContext#DUMMY_IDENTIFIER}).
* It can be changed via {@link CompletionContributor#beforeCompletion(CompletionInitializationContext)} method.<p></p>
*
* Why? This way there'll always be some non-empty element there, which usually reduces the number of
* possible cases to be considered inside a {@link CompletionContributor}.
* Also, even if completion was invoked in the middle of a white space, a reference might appear there after dummy identifier is inserted,
* and its {@link com.intellij.psi.PsiReference#getVariants()} can then be suggested.<p></p>
*
* If the dummy identifier is empty, then the file isn't copied and this method returns whatever is at caret in the original file.
*/
@NotNull
public PsiElement getPosition() {
return myPosition;
@@ -60,6 +77,9 @@ public final class CompletionParameters {
return myOriginalFile.findElementAt(myPosition.getTextRange().getStartOffset());
}
/**
* @return the file being edited, possibly injected, where code completion was invoked.
*/
@NotNull
public PsiFile getOriginalFile() {
return myOriginalFile;
@@ -70,14 +90,17 @@ public final class CompletionParameters {
return myCompletionType;
}
/**
* @return the offset (relative to the file) where code completion was invoked.
*/
public int getOffset() {
return myOffset;
}
/**
* @return
* 0 for autopopup
* 1 for explicitly invoked completion
* 0 for autopopup<br>
* 1 for explicitly invoked completion<br>
* >1 for next completion invocations when one lookup is already active
*/
public int getInvocationCount() {
@@ -116,6 +116,9 @@ public class CompletionInitializationUtil {
CompletionProcessEx indicator,
OffsetsInFile topLevelOffsets) {
CompletionAssertions.checkEditorValid(initContext.getEditor());
if (initContext.getDummyIdentifier().isEmpty()) {
return topLevelOffsets;
}
Editor hostEditor = InjectedLanguageUtil.getTopLevelEditor(initContext.getEditor());
OffsetMap hostMap = topLevelOffsets.getOffsets();
@@ -141,7 +144,9 @@ public class CompletionInitializationUtil {
OffsetsInFile translatedOffsets = hostCopyOffsets.toInjectedIfAny(hostStartOffset);
if (translatedOffsets != hostCopyOffsets) {
PsiFile injected = translatedOffsets.getFile();
if (injected instanceof PsiFileImpl && InjectedLanguageManager.getInstance(originalFile.getProject()).isInjectedFragment(originalFile)) {
if (originalFile != injected &&
injected instanceof PsiFileImpl &&
InjectedLanguageManager.getInstance(originalFile.getProject()).isInjectedFragment(originalFile)) {
((PsiFileImpl)injected).setOriginalFile(originalFile);
}
DocumentWindow documentWindow = InjectedLanguageUtil.getDocumentWindow(injected);
@@ -158,7 +163,7 @@ public class CompletionInitializationUtil {
@NotNull
private static PsiElement findCompletionPositionLeaf(OffsetsInFile offsets, int offset, PsiFile originalFile) {
PsiElement insertedElement = offsets.getFile().findElementAt(offset);
if (insertedElement == null && offsets.getFile().getTextLength() == 0) {
if (insertedElement == null && offsets.getFile().getTextLength() == offset) {
insertedElement = PsiTreeUtil.getDeepestLast(offsets.getFile());
}
CompletionAssertions.assertCompletionPositionPsiConsistent(offsets, offset, originalFile, insertedElement);
@@ -27,10 +27,8 @@ import com.intellij.openapi.util.text.StringUtil;
import com.intellij.patterns.ElementPattern;
import com.intellij.psi.*;
import com.intellij.psi.impl.cache.impl.id.IdTableBuilding;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.HashSet;
@@ -44,6 +42,17 @@ import static com.intellij.patterns.PlatformPatterns.psiElement;
*/
public class WordCompletionContributor extends CompletionContributor implements DumbAware {
@Override
public void beforeCompletion(@NotNull CompletionInitializationContext context) {
if (context.getCompletionType() == CompletionType.BASIC && isWordCompletionDefinitelyEnabled(context.getFile())) {
context.setDummyIdentifier("");
}
}
private static boolean isWordCompletionDefinitelyEnabled(@NotNull PsiFile file) {
return DumbService.isDumb(file.getProject()) || file instanceof PsiPlainTextFile;
}
@Override
public void fillCompletionVariants(@NotNull final CompletionParameters parameters, @NotNull final CompletionResultSet result) {
if (parameters.getCompletionType() == CompletionType.BASIC && shouldPerformWordCompletion(parameters)) {
@@ -54,7 +63,7 @@ public class WordCompletionContributor extends CompletionContributor implements
public static void addWordCompletionVariants(CompletionResultSet result, final CompletionParameters parameters, Set<String> excludes) {
final Set<String> realExcludes = new HashSet<>(excludes);
for (String exclude : excludes) {
String[] words = exclude.split("[ \\.-]");
String[] words = exclude.split("[ .-]");
if (words.length > 0 && StringUtil.isNotEmpty(words[0])) {
realExcludes.add(words[0]);
}
@@ -120,19 +129,16 @@ public class WordCompletionContributor extends CompletionContributor implements
}
private static boolean shouldPerformWordCompletion(CompletionParameters parameters) {
final PsiElement insertedElement = parameters.getPosition();
final boolean dumb = DumbService.getInstance(insertedElement.getProject()).isDumb();
if (dumb) {
return true;
}
if (parameters.getInvocationCount() == 0) {
return false;
}
PsiElement insertedElement = parameters.getPosition();
PsiFile file = insertedElement.getContainingFile();
if (isWordCompletionDefinitelyEnabled(file)) {
return true;
}
final PsiFile file = insertedElement.getContainingFile();
final CompletionData data = CompletionUtil.getCompletionDataByElement(insertedElement, file);
if (data != null) {
Set<CompletionVariant> toAdd = new HashSet<>();
@@ -155,8 +161,7 @@ public class WordCompletionContributor extends CompletionContributor implements
ASTNode textContainer = element != null ? element.getNode() : null;
while (textContainer != null) {
final IElementType elementType = textContainer.getElementType();
if (LanguageWordCompletion.INSTANCE.isEnabledIn(elementType) || elementType == PlainTextTokenTypes.PLAIN_TEXT) {
if (LanguageWordCompletion.INSTANCE.isEnabledIn(textContainer.getElementType())) {
return true;
}
textContainer = textContainer.getTreeParent();
@@ -164,19 +169,16 @@ public class WordCompletionContributor extends CompletionContributor implements
return false;
}
public static Set<String> getAllWords(final PsiElement context, final int offset) {
private static Set<String> getAllWords(PsiElement context, int offset) {
final Set<String> words = new LinkedHashSet<>();
if (StringUtil.isEmpty(CompletionUtil.findJavaIdentifierPrefix(context, offset))) {
return words;
}
final CharSequence chars = context.getContainingFile().getViewProvider().getContents(); // ??
IdTableBuilding.scanWords(new IdTableBuilding.ScanWordProcessor() {
@Override
public void run(final CharSequence chars, @Nullable char[] charsArray, final int start, final int end) {
if (start > offset || offset > end) {
words.add(chars.subSequence(start, end).toString());
}
IdTableBuilding.scanWords((chars1, charsArray, start, end) -> {
if (start > offset || offset > end) {
words.add(chars1.subSequence(start, end).toString());
}
}, chars, 0, chars.length());
return words;