From e8c0586d822345f43ca1a6a839d8e370cfc004cc Mon Sep 17 00:00:00 2001 From: peter Date: Fri, 16 Sep 2016 13:37:30 +0200 Subject: [PATCH] introduce RequestResultProcessor.BulkResultProcessor for poor languages (aka Kotlin) that can't resolve references efficiently one by one --- .../psi/search/RequestResultProcessor.java | 35 +++++++++ .../impl/search/BulkOccurrenceProcessor.java | 25 +++++++ .../psi/impl/search/LowLevelSearchUtil.java | 74 +++++++++++++------ .../psi/impl/search/PsiSearchHelperImpl.java | 59 +++++++++------ 4 files changed, 150 insertions(+), 43 deletions(-) create mode 100644 platform/indexing-impl/src/com/intellij/psi/impl/search/BulkOccurrenceProcessor.java 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