diff --git a/platform/indexing-api/src/com/intellij/psi/search/RequestResultProcessor.java b/platform/indexing-api/src/com/intellij/psi/search/RequestResultProcessor.java
index 8f9bb141d538..131dec7ee418 100644
--- a/platform/indexing-api/src/com/intellij/psi/search/RequestResultProcessor.java
+++ b/platform/indexing-api/src/com/intellij/psi/search/RequestResultProcessor.java
@@ -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 consumer.
+ *
+ * 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 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 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 scope start offset. The implementation is supposed to search for references
+ * inside the given element at the given offsets, and feed them to consumer.
+ * @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 consumer);
+ }
}
diff --git a/platform/indexing-impl/src/com/intellij/psi/impl/search/BulkOccurrenceProcessor.java b/platform/indexing-impl/src/com/intellij/psi/impl/search/BulkOccurrenceProcessor.java
new file mode 100644
index 000000000000..2f7b36b9c2f7
--- /dev/null
+++ b/platform/indexing-impl/src/com/intellij/psi/impl/search/BulkOccurrenceProcessor.java
@@ -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);
+}
diff --git a/platform/indexing-impl/src/com/intellij/psi/impl/search/LowLevelSearchUtil.java b/platform/indexing-impl/src/com/intellij/psi/impl/search/LowLevelSearchUtil.java
index a59ab6259228..798519f1ddc5 100644
--- a/platform/indexing-impl/src/com/intellij/psi/impl/search/LowLevelSearchUtil.java
+++ b/platform/indexing-impl/src/com/intellij/psi/impl/search/LowLevelSearchUtil.java
@@ -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,
diff --git a/platform/indexing-impl/src/com/intellij/psi/impl/search/PsiSearchHelperImpl.java b/platform/indexing-impl/src/com/intellij/psi/impl/search/PsiSearchHelperImpl.java
index f4d27b5f2a8a..3be5d14d704f 100644
--- a/platform/indexing-impl/src/com/intellij/psi/impl/search/PsiSearchHelperImpl.java
+++ b/platform/indexing-impl/src/com/intellij/psi/impl/search/PsiSearchHelperImpl.java
@@ -157,11 +157,20 @@ public class PsiSearchHelperImpl implements PsiSearchHelper {
final short searchContext,
@NotNull EnumSet 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,
+ @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 localProcessor(@NotNull final TextOccurenceProcessor processor,
+ private static Processor localProcessor(@NotNull final BulkOccurrenceProcessor processor,
@NotNull final ProgressIndicator progress,
- final boolean processInjectedPsi,
@NotNull final StringSearcher searcher) {
return new ReadActionProcessor() {
@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 localProcessor = localProcessor(processor, progress, processInjectedPsi, searcher);
+ final Processor localProcessor = localProcessor(processor, progress, searcher);
if (containerName != null) {
List 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 consumer) {
+ private static BulkOccurrenceProcessor adaptProcessor(@NotNull PsiSearchRequest singleRequest,
+ @NotNull Processor 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 localProcessor = localProcessor(adapted, progress, true, searcher);
+ Processor 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