/*
* 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.find.impl;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;
import com.intellij.find.FindBundle;
import com.intellij.find.FindModel;
import com.intellij.find.findInProject.FindInProjectManager;
import com.intellij.find.ngrams.TrigramIndex;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.progress.EmptyProgressIndicator;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectCoreUtil;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.util.text.TrigramBuilder;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileFilter;
import com.intellij.openapi.vfs.VirtualFileVisitor;
import com.intellij.psi.PsiBinaryFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.impl.cache.CacheManager;
import com.intellij.psi.impl.cache.impl.id.IdIndex;
import com.intellij.psi.impl.search.PsiSearchHelperImpl;
import com.intellij.psi.search.*;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.usageView.UsageInfo;
import com.intellij.usages.FindUsagesProcessPresentation;
import com.intellij.usages.UsageLimitUtil;
import com.intellij.usages.impl.UsageViewManagerImpl;
import com.intellij.util.Processor;
import com.intellij.util.Processors;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.indexing.FileBasedIndex;
import com.intellij.util.indexing.FileBasedIndexImpl;
import consulo.annotations.RequiredReadAction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
/**
* @author peter
*/
class FindInProjectTask {
private static final Logger LOG = Logger.getInstance("#com.intellij.find.impl.FindInProjectTask");
private static final int FILES_SIZE_LIMIT = 70 * 1024 * 1024; // megabytes.
private static final int SINGLE_FILE_SIZE_LIMIT = 5 * 1024 * 1024; // megabytes.
private final FindModel myFindModel;
private final Project myProject;
private final PsiManager myPsiManager;
@Nullable
private final VirtualFile myDirectory;
private final ProjectFileIndex myProjectFileIndex;
private final FileIndex myFileIndex;
private final Condition<VirtualFile> myFileMask;
private final ProgressIndicator myProgress;
@Nullable
private final Module myModule;
private final Set<VirtualFile> myLargeFiles = Collections.synchronizedSet(ContainerUtil.newTroveSet());
private final Set<VirtualFile> myFilesToScanInitially;
private final AtomicBoolean myWarningShown = new AtomicBoolean();
private final AtomicLong myTotalFilesSize = new AtomicLong();
private final String myStringToFindInIndices;
FindInProjectTask(@NotNull final FindModel findModel, @NotNull final Project project, @NotNull Set<VirtualFile> filesToScanInitially) {
myFindModel = findModel;
myProject = project;
myFilesToScanInitially = filesToScanInitially;
myDirectory = FindInProjectUtil.getDirectory(findModel);
myPsiManager = PsiManager.getInstance(project);
final String moduleName = findModel.getModuleName();
myModule = moduleName == null ? null : ReadAction.compute(() -> ModuleManager.getInstance(project).findModuleByName(moduleName));
myProjectFileIndex = ProjectRootManager.getInstance(project).getFileIndex();
myFileIndex = myModule == null ? myProjectFileIndex : ModuleRootManager.getInstance(myModule).getFileIndex();
Condition<CharSequence> patternCondition = FindInProjectUtil.createFileMaskCondition(findModel.getFileFilter());
myFileMask = file -> file != null && patternCondition.value(file.getNameSequence());
final ProgressIndicator progress = ProgressManager.getInstance().getProgressIndicator();
myProgress = progress != null ? progress : new EmptyProgressIndicator();
String stringToFind = myFindModel.getStringToFind();
if (myFindModel.isRegularExpressions()) {
stringToFind = FindInProjectUtil.buildStringToFindForIndicesFromRegExp(stringToFind, myProject);
}
myStringToFindInIndices = stringToFind;
}
public void findUsages(@NotNull Processor<UsageInfo> consumer, @NotNull FindUsagesProcessPresentation processPresentation) {
try {
myProgress.setIndeterminate(true);
myProgress.setText("Scanning indexed files...");
Set<VirtualFile> filesForFastWordSearch = ReadAction.compute(this::getFilesForFastWordSearch);
myProgress.setIndeterminate(false);
if (LOG.isDebugEnabled()) {
LOG.debug("Searching for " + myFindModel.getStringToFind() + " in " + filesForFastWordSearch.size() + " indexed files");
}
searchInFiles(filesForFastWordSearch, processPresentation, consumer);
myProgress.setIndeterminate(true);
myProgress.setText("Scanning non-indexed files...");
boolean canRelyOnIndices = canRelyOnIndices();
final Collection<VirtualFile> otherFiles = collectFilesInScope(filesForFastWordSearch, canRelyOnIndices);
myProgress.setIndeterminate(false);
if (LOG.isDebugEnabled()) {
LOG.debug("Searching for " + myFindModel.getStringToFind() + " in " + otherFiles.size() + " non-indexed files");
}
myProgress.checkCanceled();
long start = System.currentTimeMillis();
searchInFiles(otherFiles, processPresentation, consumer);
if (canRelyOnIndices && otherFiles.size() > 1000) {
long time = System.currentTimeMillis() - start;
logStats(otherFiles, time);
}
}
catch (ProcessCanceledException e) {
processPresentation.setCanceled(true);
if (LOG.isDebugEnabled()) {
LOG.debug("Usage search canceled", e);
}
}
if (!myLargeFiles.isEmpty()) {
processPresentation.setLargeFilesWereNotScanned(myLargeFiles);
}
if (!myProgress.isCanceled()) {
myProgress.setText(FindBundle.message("find.progress.search.completed"));
}
}
private static void logStats(@NotNull Collection<VirtualFile> otherFiles, long time) {
final Multiset<String> stats = HashMultiset.create();
for (VirtualFile file : otherFiles) {
//noinspection StringToUpperCaseOrToLowerCaseWithoutLocale
stats.add(StringUtil.notNullize(file.getExtension()).toLowerCase());
}
List<String> extensions = ContainerUtil.newArrayList(stats.elementSet());
Collections.sort(extensions, (o1, o2) -> stats.count(o2) - stats.count(o1));
String message = "Search in " +
otherFiles.size() +
" files with unknown types took " +
time +
"ms.\n" +
"Mapping their extensions to an existing file type (e.g. Plain Text) might speed up the search.\n" +
"Most frequent non-indexed file extensions: ";
for (int i = 0; i < Math.min(10, extensions.size()); i++) {
String extension = extensions.get(i);
message += extension + "(" + stats.count(extension) + ") ";
}
LOG.info(message);
}
private void searchInFiles(@NotNull Collection<VirtualFile> virtualFiles,
@NotNull FindUsagesProcessPresentation processPresentation,
@NotNull final Processor<UsageInfo> consumer) {
AtomicInteger occurrenceCount = new AtomicInteger();
AtomicInteger processedFileCount = new AtomicInteger();
Processor<VirtualFile> processor = virtualFile -> {
if (!virtualFile.isValid()) return true;
long fileLength = UsageViewManagerImpl.getFileLength(virtualFile);
if (fileLength == -1) return true; // Binary or invalid
final boolean skipProjectFile = ProjectCoreUtil.isProjectOrWorkspaceFile(virtualFile) && !myFindModel.isSearchInProjectFiles();
if (skipProjectFile && !Registry.is("find.search.in.project.files")) return true;
if (fileLength > SINGLE_FILE_SIZE_LIMIT) {
myLargeFiles.add(virtualFile);
return true;
}
myProgress.checkCanceled();
if (myProgress.isRunning()) {
double fraction = (double)processedFileCount.incrementAndGet() / virtualFiles.size();
myProgress.setFraction(fraction);
}
String text = FindBundle.message("find.searching.for.string.in.file.progress", myFindModel.getStringToFind(), virtualFile.getPresentableUrl());
myProgress.setText(text);
myProgress.setText2(FindBundle.message("find.searching.for.string.in.file.occurrences.progress", occurrenceCount));
Pair.NonNull<PsiFile, VirtualFile> pair = ReadAction.compute(() -> findFile(virtualFile));
if (pair == null) return true;
PsiFile psiFile = pair.first;
VirtualFile sourceVirtualFile = pair.second;
int countInFile = FindInProjectUtil.processUsagesInFile(psiFile, sourceVirtualFile, myFindModel, info -> skipProjectFile || consumer.process(info));
if (countInFile > 0 && skipProjectFile) {
processPresentation.projectFileUsagesFound(() -> {
FindModel model = myFindModel.clone();
model.setSearchInProjectFiles(true);
FindInProjectManager.getInstance(myProject).startFindInProject(model);
});
return true;
}
occurrenceCount.addAndGet(countInFile);
if (countInFile > 0) {
if (myTotalFilesSize.addAndGet(fileLength) > FILES_SIZE_LIMIT && myWarningShown.compareAndSet(false, true)) {
String message = FindBundle.message("find.excessive.total.size.prompt", UsageViewManagerImpl.presentableSize(myTotalFilesSize.longValue()),
ApplicationNamesInfo.getInstance().getProductName());
UsageLimitUtil.showAndCancelIfAborted(myProject, message, processPresentation.getUsageViewPresentation());
}
}
return true;
};
PsiSearchHelperImpl.processFilesConcurrentlyDespiteWriteActions(myProject, new ArrayList<>(virtualFiles), myProgress, processor);
}
// must return non-binary files
@NotNull
private Collection<VirtualFile> collectFilesInScope(@NotNull final Set<VirtualFile> alreadySearched, final boolean skipIndexed) {
SearchScope customScope = myFindModel.isCustomScope() ? myFindModel.getCustomScope() : null;
final GlobalSearchScope globalCustomScope = customScope == null ? null : GlobalSearchScopeUtil.toGlobalSearchScope(customScope, myProject);
final ProjectFileIndex fileIndex = ProjectFileIndex.SERVICE.getInstance(myProject);
final boolean hasTrigrams = hasTrigrams(myStringToFindInIndices);
class EnumContentIterator implements ContentIterator {
private final Set<VirtualFile> myFiles = new LinkedHashSet<>();
@Override
public boolean processFile(@NotNull final VirtualFile virtualFile) {
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
ProgressManager.checkCanceled();
if (virtualFile.isDirectory() ||
!virtualFile.isValid() ||
!myFileMask.value(virtualFile) ||
globalCustomScope != null && !globalCustomScope.contains(virtualFile)) {
return;
}
if (skipIndexed &&
isCoveredByIndex(virtualFile) &&
(fileIndex.isInContent(virtualFile) || fileIndex.isInLibraryClasses(virtualFile) || fileIndex.isInLibrarySource(virtualFile))) {
return;
}
if(projectFileIndex.isExcluded(virtualFile)) {
return;
}
Pair.NonNull<PsiFile, VirtualFile> pair = findFile(virtualFile);
if (pair == null) return;
VirtualFile sourceVirtualFile = pair.second;
if (sourceVirtualFile != null && !alreadySearched.contains(sourceVirtualFile)) {
myFiles.add(sourceVirtualFile);
}
}
private final ProjectFileIndex projectFileIndex = ProjectFileIndex.SERVICE.getInstance(myProject);
private final FileBasedIndexImpl fileBasedIndex = (FileBasedIndexImpl)FileBasedIndex.getInstance();
private boolean isCoveredByIndex(VirtualFile file) {
FileType fileType = file.getFileType();
if (hasTrigrams) {
return TrigramIndex.isIndexable(fileType) && fileBasedIndex.isIndexingCandidate(file, TrigramIndex.INDEX_ID);
}
return IdIndex.isIndexable(fileType) && fileBasedIndex.isIndexingCandidate(file, IdIndex.NAME);
}
});
return true;
}
@NotNull
private Collection<VirtualFile> getFiles() {
return myFiles;
}
}
final EnumContentIterator iterator = new EnumContentIterator();
if (customScope instanceof LocalSearchScope) {
for (VirtualFile file : GlobalSearchScopeUtil.getLocalScopeFiles((LocalSearchScope)customScope)) {
iterator.processFile(file);
}
}
else if (customScope instanceof Iterable) { // GlobalSearchScope can span files out of project roots e.g. FileScope / FilesScope
//noinspection unchecked
for (VirtualFile file : (Iterable<VirtualFile>)customScope) {
iterator.processFile(file);
}
}
else if (myDirectory != null) {
VirtualFileVisitor.Option limit = VirtualFileVisitor.limit(myFindModel.isWithSubdirectories() ? -1 : 1);
VfsUtilCore.visitChildrenRecursively(myDirectory, new VirtualFileVisitor(limit) {
@Override
public boolean visitFile(@NotNull VirtualFile file) {
iterator.processFile(file);
return true;
}
});
}
else {
boolean success = myFileIndex.iterateContent(iterator);
if (success && globalCustomScope != null && globalCustomScope.isSearchInLibraries()) {
final VirtualFile[] librarySources = ReadAction.compute(() -> {
OrderEnumerator enumerator = myModule == null ? OrderEnumerator.orderEntries(myProject) : OrderEnumerator.orderEntries(myModule);
return enumerator.withoutModuleSourceEntries().withoutDepModules().getSourceRoots();
});
iterateAll(librarySources, globalCustomScope, iterator);
}
}
return iterator.getFiles();
}
private static boolean iterateAll(@NotNull VirtualFile[] files, @NotNull final GlobalSearchScope searchScope, @NotNull final ContentIterator iterator) {
final FileTypeManager fileTypeManager = FileTypeManager.getInstance();
final VirtualFileFilter contentFilter =
file -> file.isDirectory() || !fileTypeManager.isFileIgnored(file) && !file.getFileType().isBinary() && searchScope.contains(file);
for (VirtualFile file : files) {
if (!VfsUtilCore.iterateChildrenRecursively(file, contentFilter, iterator)) return false;
}
return true;
}
private boolean canRelyOnIndices() {
if (DumbService.isDumb(myProject)) return false;
// a local scope may be over a non-indexed file
if (myFindModel.getCustomScope() instanceof LocalSearchScope) return false;
String text = myStringToFindInIndices;
if (StringUtil.isEmptyOrSpaces(text)) return false;
if (hasTrigrams(text)) return true;
// $ is used to separate words when indexing plain-text files but not when indexing
// Java identifiers, so we can't consistently break a string containing $ characters into words
return myFindModel.isWholeWordsOnly() && text.indexOf('$') < 0 && !StringUtil.getWordsInStringLongestFirst(text).isEmpty();
}
private static boolean hasTrigrams(@NotNull String text) {
return TrigramIndex.ENABLED && !TrigramBuilder.processTrigrams(text, new TrigramBuilder.TrigramProcessor() {
@Override
public boolean execute(int value) {
return false;
}
});
}
@NotNull
private Set<VirtualFile> getFilesForFastWordSearch() {
String stringToFind = myStringToFindInIndices;
if (stringToFind.isEmpty() || DumbService.getInstance(myProject).isDumb()) {
return Collections.emptySet();
}
final Set<VirtualFile> resultFiles = new LinkedHashSet<>();
for (VirtualFile file : myFilesToScanInitially) {
if (myFileMask.value(file)) {
resultFiles.add(file);
}
}
final GlobalSearchScope scope = GlobalSearchScopeUtil.toGlobalSearchScope(FindInProjectUtil.getScopeFromModel(myProject, myFindModel), myProject);
ProjectFileIndex index = ProjectFileIndex.SERVICE.getInstance(myProject);
if (TrigramIndex.ENABLED) {
final Set<Integer> keys = ContainerUtil.newTroveSet();
TrigramBuilder.processTrigrams(stringToFind, new TrigramBuilder.TrigramProcessor() {
@Override
public boolean execute(int value) {
keys.add(value);
return true;
}
});
if (!keys.isEmpty()) {
final List<VirtualFile> hits = new ArrayList<>();
ApplicationManager.getApplication().runReadAction(() -> {
FileBasedIndex.getInstance().getFilesWithKey(TrigramIndex.INDEX_ID, keys, Processors.cancelableCollectProcessor(hits), scope);
});
for (VirtualFile hit : hits) {
if (myFileMask.value(hit)) {
resultFiles.add(hit);
}
}
return resultFiles.stream().filter(it -> !index.isExcluded(it)).collect(Collectors.toSet());
}
}
PsiSearchHelperImpl helper = (PsiSearchHelperImpl)PsiSearchHelper.SERVICE.getInstance(myProject);
helper.processFilesWithText(scope, UsageSearchContext.ANY, myFindModel.isCaseSensitive(), stringToFind, file -> {
if (myFileMask.value(file)) {
ContainerUtil.addIfNotNull(resultFiles, file);
}
return true;
});
// in case our word splitting is incorrect
CacheManager cacheManager = CacheManager.getInstance(myProject);
VirtualFile[] filesWithWord = cacheManager.getVirtualFilesWithWord(stringToFind, UsageSearchContext.ANY, scope, myFindModel.isCaseSensitive());
for (VirtualFile file : filesWithWord) {
if (myFileMask.value(file)) {
resultFiles.add(file);
}
}
return resultFiles.stream().filter(it -> !index.isExcluded(it)).collect(Collectors.toSet());
}
@RequiredReadAction
private Pair.NonNull<PsiFile, VirtualFile> findFile(@NotNull final VirtualFile virtualFile) {
PsiFile psiFile = myPsiManager.findFile(virtualFile);
if (psiFile != null && !(psiFile instanceof PsiBinaryFile)) {
PsiFile sourceFile = (PsiFile)psiFile.getNavigationElement();
if (sourceFile != null) psiFile = sourceFile;
if (psiFile.getFileType().isBinary()) {
psiFile = null;
}
}
VirtualFile sourceVirtualFile = PsiUtilCore.getVirtualFile(psiFile);
if (psiFile == null || psiFile.getFileType().isBinary() || sourceVirtualFile == null) {
return null;
}
return Pair.createNonNull(psiFile, sourceVirtualFile);
}
}