introduce RequestResultProcessor.BulkResultProcessor for poor languages (aka Kotlin) that can't resolve references efficiently one by one

This commit is contained in:
peter
2016-09-16 13:38:25 +02:00
parent ef8ce935ba
commit e8c0586d82
4 changed files with 150 additions and 43 deletions
@@ -17,17 +17,25 @@ package com.intellij.psi.search;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.util.Processor;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
/**
* An occurrence processor for Find Usages functionality. A typical scenario involves invoking
* {@link ReferencesSearch.SearchParameters#getOptimizer()} and passing this processor together with search string and some other parameters to
* {@link SearchRequestCollector#searchWord(String, SearchScope, short, boolean, String, RequestResultProcessor)}.
*
* @author peter
*/
public abstract class RequestResultProcessor {
private final Object myEquality;
/**
* @param equality this processor's equals/hashCode will delegate to this object
*/
protected RequestResultProcessor(@NotNull Object... equality) {
myEquality = Arrays.asList(equality);
}
@@ -47,5 +55,32 @@ public abstract class RequestResultProcessor {
return myEquality.hashCode();
}
/**
* For every occurrence of the search string in the search scope, the infrastructure invokes this method for every PSI element having that
* occurrence inside, from leaf elements up the tree until file element. The implementation is supposed to search for references
* inside the given element at the given offset, and feed them to <code>consumer</code>.<p/>
*
* If you wish to process all offsets in the scope (e.g. file) at once, extend {@link BulkResultProcessor}.
*
* @return whether the consumer has returned false for any of the references (and thus stopped searching), false otherwise.
*/
public abstract boolean processTextOccurrence(@NotNull PsiElement element, int offsetInElement, @NotNull Processor<PsiReference> consumer);
/**
* A variant of {@link RequestResultProcessor} that processes all text occurrences at once, e.g. for performance purposes.
*/
public static abstract class BulkResultProcessor extends RequestResultProcessor {
@Override
public boolean processTextOccurrence(@NotNull PsiElement element, int offsetInElement, @NotNull Processor<PsiReference> consumer) {
return processTextOccurrences(element, new int[]{offsetInElement}, consumer);
}
/**
* Invoked for every element of the search scope (e.g. file) with the array of all offsets of search string occurrences in this scope.
* Offsets are relative to <code>scope</code> start offset. The implementation is supposed to search for references
* inside the given element at the given offsets, and feed them to <code>consumer</code>.<p/>
* @return whether the consumer has returned false for any of the references (and thus stopped searching), false otherwise.
*/
public abstract boolean processTextOccurrences(@NotNull PsiElement scope, int[] offsetsInScope, @NotNull Processor<PsiReference> consumer);
}
}
@@ -0,0 +1,25 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.psi.impl.search;
import com.intellij.psi.PsiElement;
import com.intellij.util.text.StringSearcher;
import org.jetbrains.annotations.NotNull;
@FunctionalInterface
interface BulkOccurrenceProcessor {
boolean execute(@NotNull PsiElement scope, int[] offsetsInScope, @NotNull StringSearcher searcher);
}
@@ -32,6 +32,7 @@ import com.intellij.psi.impl.source.tree.LeafElement;
import com.intellij.psi.impl.source.tree.TreeElement;
import com.intellij.psi.search.TextOccurenceProcessor;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ConcurrencyUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.StringSearcher;
@@ -52,7 +53,7 @@ public class LowLevelSearchUtil {
private static Boolean processInjectedFile(PsiElement element,
final TextOccurenceProcessor processor,
final StringSearcher searcher,
ProgressIndicator progress,
@NotNull ProgressIndicator progress,
InjectedLanguageManager injectedLanguageManager) {
if (!(element instanceof PsiLanguageInjectionHost)) return null;
if (injectedLanguageManager == null) return null;
@@ -75,7 +76,7 @@ public class LowLevelSearchUtil {
@NotNull StringSearcher searcher,
final int offset,
final boolean processInjectedPsi,
ProgressIndicator progress,
@NotNull ProgressIndicator progress,
TreeElement lastElement) {
if (scope instanceof PsiCompiledElement) {
throw new IllegalArgumentException("Scope is compiled, can't scan: "+scope);
@@ -115,7 +116,7 @@ public class LowLevelSearchUtil {
TreeElement prevNode = null;
PsiElement run = null;
while (run != scope) {
if (progress != null) progress.checkCanceled();
progress.checkCanceled();
if (useTree) {
start += prevNode == null ? 0 : prevNode.getStartOffsetInParent();
prevNode = leafNode;
@@ -173,12 +174,16 @@ public class LowLevelSearchUtil {
return (LeafElement)scopeNode.findLeafElementAt(offsetR);
}
//@RequiresReadAction
public static boolean processElementsContainingWordInElement(@NotNull final TextOccurenceProcessor processor,
@NotNull final PsiElement scope,
@NotNull final StringSearcher searcher,
final boolean processInjectedPsi,
final ProgressIndicator progress) {
boolean processInjectedPsi,
@NotNull ProgressIndicator progress) {
int[] occurrences = getTextOccurrencesInScope(scope, searcher, progress);
return processElementsAtOffsets(scope, searcher, processInjectedPsi, progress, occurrences, processor);
}
static int[] getTextOccurrencesInScope(@NotNull PsiElement scope, @NotNull StringSearcher searcher, ProgressIndicator progress) {
if (progress != null) progress.checkCanceled();
PsiFile file = scope.getContainingFile();
@@ -188,25 +193,38 @@ public class LowLevelSearchUtil {
TextRange range = scope.getTextRange();
if (range == null) {
LOG.error("Element " + scope + " of class " + scope.getClass() + " has null range");
return true;
return ArrayUtil.EMPTY_INT_ARRAY;
}
final int scopeStart = range.getStartOffset();
final int startOffset = scopeStart;
int startOffset = range.getStartOffset();
int endOffset = range.getEndOffset();
if (endOffset > buffer.length()) {
diagnoseInvalidRange(scope, file, viewProvider, buffer, range);
return true;
return ArrayUtil.EMPTY_INT_ARRAY;
}
final Project project = file.getProject();
final TreeElement[] lastElement = {null};
return processTextOccurrences(buffer, startOffset, endOffset, searcher, progress, offset -> {
if (progress != null) progress.checkCanceled();
lastElement[0] = processTreeUp(project, processor, scope, searcher, offset - scopeStart, processInjectedPsi, progress,
lastElement[0]);
return lastElement[0] != null;
});
int[] offsets = getTextOccurrences(buffer, startOffset, endOffset, searcher, progress);
for (int i = 0; i < offsets.length; i++) {
offsets[i] -= startOffset;
}
return offsets;
}
static boolean processElementsAtOffsets(@NotNull PsiElement scope,
@NotNull StringSearcher searcher,
boolean processInjectedPsi,
@NotNull ProgressIndicator progress,
int[] offsetsInScope, @NotNull TextOccurenceProcessor processor) {
if (offsetsInScope.length == 0) return true;
Project project = scope.getProject();
TreeElement[] lastElement = {null};
for (int offset : offsetsInScope) {
progress.checkCanceled();
lastElement[0] = processTreeUp(project, processor, scope, searcher, offset, processInjectedPsi, progress, lastElement[0]);
if (lastElement[0] == null) return false;
}
return true;
}
private static void diagnoseInvalidRange(@NotNull PsiElement scope,
@@ -239,6 +257,19 @@ public class LowLevelSearchUtil {
@NotNull StringSearcher searcher,
@Nullable ProgressIndicator progress,
@NotNull TIntProcedure processor) {
for (int offset : getTextOccurrences(text, startOffset, endOffset, searcher, progress)) {
if (!processor.execute(offset)) {
return false;
}
}
return true;
}
private static int[] getTextOccurrences(@NotNull CharSequence text,
int startOffset,
int endOffset,
@NotNull StringSearcher searcher,
@Nullable ProgressIndicator progress) {
if (endOffset > text.length()) {
throw new IllegalArgumentException("end: " + endOffset + " > length: "+text.length());
}
@@ -266,14 +297,15 @@ public class LowLevelSearchUtil {
}
cachedMap.put(searcher, cachedOccurrences);
}
TIntArrayList offsets = new TIntArrayList(cachedOccurrences.length - 2);
for (int i = 2; i < cachedOccurrences.length; i++) {
int occurrence = cachedOccurrences[i];
if (occurrence > endOffset - searcher.getPatternLength()) break;
if (occurrence >= startOffset && !processor.execute(occurrence)) {
return false;
if (occurrence >= startOffset) {
offsets.add(occurrence);
}
}
return true;
return offsets.toNativeArray();
}
private static boolean checkJavaIdentifier(@NotNull CharSequence text,
@@ -157,11 +157,20 @@ public class PsiSearchHelperImpl implements PsiSearchHelper {
final short searchContext,
@NotNull EnumSet<Options> options,
@Nullable String containerName) {
return bulkProcessElementsWithWord(searchScope, text, searchContext, options, containerName, (scope, offsetsInScope, searcher) ->
LowLevelSearchUtil.processElementsAtOffsets(scope, searcher, options.contains(Options.PROCESS_INJECTED_PSI), getOrCreateIndicator(),
offsetsInScope, processor));
}
private boolean bulkProcessElementsWithWord(@NotNull SearchScope searchScope,
@NotNull final String text,
final short searchContext,
@NotNull EnumSet<Options> options,
@Nullable String containerName, @NotNull final BulkOccurrenceProcessor processor) {
if (text.isEmpty()) {
throw new IllegalArgumentException("Cannot search for elements with empty text");
}
final ProgressIndicator progress = getOrCreateIndicator();
final boolean processInjectedPsi = options.contains(Options.PROCESS_INJECTED_PSI);
if (searchScope instanceof GlobalSearchScope) {
StringSearcher searcher = new StringSearcher(text, options.contains(Options.CASE_SENSITIVE_SEARCH), true,
searchContext == UsageSearchContext.IN_STRINGS,
@@ -170,8 +179,8 @@ public class PsiSearchHelperImpl implements PsiSearchHelper {
return processElementsWithTextInGlobalScope(processor,
(GlobalSearchScope)searchScope,
searcher,
searchContext, options.contains(Options.CASE_SENSITIVE_SEARCH), containerName, progress,
processInjectedPsi);
searchContext, options.contains(Options.CASE_SENSITIVE_SEARCH), containerName, progress
);
}
LocalSearchScope scope = (LocalSearchScope)searchScope;
PsiElement[] scopeElements = scope.getScope();
@@ -190,7 +199,7 @@ public class PsiSearchHelperImpl implements PsiSearchHelper {
LOG.debug("Element " + scopeElement + " of class " + scopeElement.getClass() + " has null range");
return true;
}
return LowLevelSearchUtil.processElementsContainingWordInElement(processor, scopeElement, searcher, processInjectedPsi, progress);
return processor.execute(scopeElement, LowLevelSearchUtil.getTextOccurrencesInScope(scopeElement, searcher, progress), searcher);
}
@Override
@@ -213,15 +222,14 @@ public class PsiSearchHelperImpl implements PsiSearchHelper {
}
@NotNull
private static Processor<PsiElement> localProcessor(@NotNull final TextOccurenceProcessor processor,
private static Processor<PsiElement> localProcessor(@NotNull final BulkOccurrenceProcessor processor,
@NotNull final ProgressIndicator progress,
final boolean processInjectedPsi,
@NotNull final StringSearcher searcher) {
return new ReadActionProcessor<PsiElement>() {
@Override
public boolean processInReadAction(PsiElement scopeElement) {
return scopeElement.isValid() &&
LowLevelSearchUtil.processElementsContainingWordInElement(processor, scopeElement, searcher, processInjectedPsi, progress);
processor.execute(scopeElement, LowLevelSearchUtil.getTextOccurrencesInScope(scopeElement, searcher, progress), searcher);
}
@Override
@@ -231,14 +239,13 @@ public class PsiSearchHelperImpl implements PsiSearchHelper {
};
}
private boolean processElementsWithTextInGlobalScope(@NotNull final TextOccurenceProcessor processor,
private boolean processElementsWithTextInGlobalScope(@NotNull final BulkOccurrenceProcessor processor,
@NotNull final GlobalSearchScope scope,
@NotNull final StringSearcher searcher,
final short searchContext,
final boolean caseSensitively,
@Nullable String containerName,
@NotNull ProgressIndicator progress,
final boolean processInjectedPsi) {
@NotNull ProgressIndicator progress) {
if (Thread.holdsLock(PsiLock.LOCK)) {
throw new AssertionError("You must not run search from within updating PSI activity. Please consider invokeLatering it instead.");
}
@@ -253,7 +260,7 @@ public class PsiSearchHelperImpl implements PsiSearchHelper {
progress.setText(PsiBundle.message("psi.search.for.word.progress", text));
final Processor<PsiElement> localProcessor = localProcessor(processor, progress, processInjectedPsi, searcher);
final Processor<PsiElement> localProcessor = localProcessor(processor, progress, searcher);
if (containerName != null) {
List<VirtualFile> intersectionWithContainerFiles = new ArrayList<>();
// intersectionWithContainerFiles holds files containing words from both `text` and `containerName`
@@ -707,18 +714,25 @@ public class PsiSearchHelperImpl implements PsiSearchHelper {
}
@NotNull
private static TextOccurenceProcessor adaptProcessor(@NotNull PsiSearchRequest singleRequest,
@NotNull final Processor<PsiReference> consumer) {
private static BulkOccurrenceProcessor adaptProcessor(@NotNull PsiSearchRequest singleRequest,
@NotNull Processor<PsiReference> consumer) {
final SearchScope searchScope = singleRequest.searchScope;
final boolean ignoreInjectedPsi = searchScope instanceof LocalSearchScope && ((LocalSearchScope)searchScope).isIgnoreInjectedPsi();
final RequestResultProcessor wrapped = singleRequest.processor;
return new TextOccurenceProcessor() {
return new BulkOccurrenceProcessor() {
@Override
public boolean execute(@NotNull PsiElement element, int offsetInElement) {
if (ignoreInjectedPsi && element instanceof PsiLanguageInjectionHost) return true;
public boolean execute(@NotNull PsiElement scope, int[] offsetsInScope, @NotNull StringSearcher searcher) {
try {
return wrapped.processTextOccurrence(element, offsetInElement, consumer);
if (wrapped instanceof RequestResultProcessor.BulkResultProcessor) {
return ((RequestResultProcessor.BulkResultProcessor)wrapped).processTextOccurrences(scope, offsetsInScope, consumer);
}
return LowLevelSearchUtil.processElementsAtOffsets(scope, searcher, !ignoreInjectedPsi,
getOrCreateIndicator(), offsetsInScope,
(element, offsetInElement) -> {
if (ignoreInjectedPsi && element instanceof PsiLanguageInjectionHost) return true;
return wrapped.processTextOccurrence(element, offsetInElement, consumer);
});
}
catch (ProcessCanceledException e) {
throw e;
@@ -852,9 +866,9 @@ public class PsiSearchHelperImpl implements PsiSearchHelper {
for (RequestWithProcessor singleRequest : entry.getValue()) {
PsiSearchRequest primitive = singleRequest.request;
StringSearcher searcher = new StringSearcher(primitive.word, primitive.caseSensitive, true, false);
final TextOccurenceProcessor adapted = adaptProcessor(primitive, singleRequest.refProcessor);
BulkOccurrenceProcessor adapted = adaptProcessor(primitive, singleRequest.refProcessor);
Processor<PsiElement> localProcessor = localProcessor(adapted, progress, true, searcher);
Processor<PsiElement> localProcessor = localProcessor(adapted, progress, searcher);
assert !localProcessors.containsKey(singleRequest) || localProcessors.get(singleRequest) == localProcessor;
localProcessors.put(singleRequest, localProcessor);
@@ -880,8 +894,9 @@ public class PsiSearchHelperImpl implements PsiSearchHelper {
if (single.caseSensitive) options.add(Options.CASE_SENSITIVE_SEARCH);
if (shouldProcessInjectedPsi(single.searchScope)) options.add(Options.PROCESS_INJECTED_PSI);
return processElementsWithWord(adaptProcessor(single, consumer), single.searchScope, single.word, single.searchContext, options,
single.containerName);
return bulkProcessElementsWithWord(single.searchScope, single.word, single.searchContext, options, single.containerName,
adaptProcessor(single, consumer)
);
}
@NotNull