/*
* 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.intellij.BundleBase;
import com.intellij.find.*;
import com.intellij.find.findInProject.FindInProjectManager;
import com.intellij.icons.AllIcons;
import com.intellij.ide.DataManager;
import com.intellij.navigation.ItemPresentation;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeRegistry;
import com.intellij.openapi.fileTypes.UnknownFileType;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.util.ProgressWrapper;
import com.intellij.openapi.progress.util.TooManyUsagesStatus;
import com.intellij.openapi.project.DumbServiceImpl;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.LibraryOrderEntry;
import com.intellij.openapi.roots.OrderEntry;
import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Conditions;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.ex.VirtualFileManagerEx;
import com.intellij.psi.*;
import com.intellij.psi.search.*;
import com.intellij.ui.content.Content;
import com.intellij.usageView.UsageInfo;
import com.intellij.usageView.UsageViewManager;
import com.intellij.usages.ConfigurableUsageTarget;
import com.intellij.usages.FindUsagesProcessPresentation;
import com.intellij.usages.UsageView;
import com.intellij.usages.UsageViewPresentation;
import com.intellij.util.Function;
import com.intellij.util.PatternUtil;
import com.intellij.util.Processor;
import consulo.annotations.RequiredReadAction;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.io.File;
import java.util.*;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
public class FindInProjectUtil {
private static final int USAGES_PER_READ_ACTION = 100;
private FindInProjectUtil() {}
public static void setDirectoryName(@NotNull FindModel model, @NotNull DataContext dataContext) {
PsiElement psiElement = null;
Project project = CommonDataKeys.PROJECT.getData(dataContext);
if (project != null && !DumbServiceImpl.getInstance(project).isDumb()) {
try {
psiElement = CommonDataKeys.PSI_ELEMENT.getData(dataContext);
}
catch (IndexNotReadyException ignore) {}
}
String directoryName = null;
if (psiElement instanceof PsiDirectory) {
directoryName = ((PsiDirectory)psiElement).getVirtualFile().getPresentableUrl();
}
if (directoryName == null && psiElement instanceof PsiDirectoryContainer) {
final PsiDirectory[] directories = ((PsiDirectoryContainer)psiElement).getDirectories();
directoryName = directories.length == 1 ? directories[0].getVirtualFile().getPresentableUrl():null;
}
if (directoryName == null) {
VirtualFile virtualFile = CommonDataKeys.VIRTUAL_FILE.getData(dataContext);
if (virtualFile != null && virtualFile.isDirectory()) directoryName = virtualFile.getPresentableUrl();
}
Module module = LangDataKeys.MODULE_CONTEXT.getData(dataContext);
if (module != null) {
model.setModuleName(module.getName());
}
// model contains previous find in path settings
// apply explicit settings from context
if (module != null) {
model.setModuleName(module.getName());
}
Editor editor = CommonDataKeys.EDITOR.getData(dataContext);
if (model.getModuleName() == null || editor == null) {
if (directoryName != null) {
model.setDirectoryName(directoryName);
model.setCustomScope(false); // to select "Directory: " radio button
}
}
// set project scope if we have no other settings
model.setProjectScope(model.getDirectoryName() == null && model.getModuleName() == null && !model.isCustomScope());
}
/**
* @deprecated to remove in IDEA 16
*/
@Nullable
public static PsiDirectory getPsiDirectory(@NotNull final FindModel findModel, @NotNull Project project) {
VirtualFile directory = getDirectory(findModel);
return directory == null ? null : PsiManager.getInstance(project).findDirectory(directory);
}
@Nullable
public static VirtualFile getDirectory(@NotNull final FindModel findModel) {
String directoryName = findModel.getDirectoryName();
if (findModel.isProjectScope() || StringUtil.isEmpty(directoryName)) {
return null;
}
String path = directoryName.replace(File.separatorChar, '/');
VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByPath(path);
if (virtualFile == null || !virtualFile.isDirectory()) {
virtualFile = null;
for (LocalFileProvider provider : ((VirtualFileManagerEx)VirtualFileManager.getInstance()).getLocalFileProviders()) {
VirtualFile file = provider.findLocalVirtualFileByPath(path);
if (file != null && file.isDirectory()) {
if (file.getChildren().length > 0) {
virtualFile = file;
break;
}
if(virtualFile == null){
virtualFile = file;
}
}
}
}
return virtualFile;
}
/* filter can have form "*.js, !*_min.js", latter means except matched by *_min.js */
@NotNull
public static Condition<CharSequence> createFileMaskCondition(@Nullable String filter) throws PatternSyntaxException {
if (filter == null) {
return Conditions.alwaysTrue();
}
String pattern = "";
String negativePattern = "";
final List<String> masks = StringUtil.split(filter, ",");
for(String mask:masks) {
mask = mask.trim();
if (StringUtil.startsWith(mask, "!")) {
negativePattern += (negativePattern.isEmpty() ? "" : "|") + "(" + PatternUtil.convertToRegex(mask.substring(1)) + ")";
}
else {
pattern += (pattern.isEmpty() ? "" : "|") + "(" + PatternUtil.convertToRegex(mask) + ")";
}
}
if (pattern.isEmpty()) pattern = PatternUtil.convertToRegex("*");
final String finalPattern = pattern;
final String finalNegativePattern = negativePattern;
return new Condition<CharSequence>() {
final Pattern regExp = Pattern.compile(finalPattern, Pattern.CASE_INSENSITIVE);
final Pattern negativeRegExp = StringUtil.isEmpty(finalNegativePattern) ? null : Pattern.compile(finalNegativePattern, Pattern.CASE_INSENSITIVE);
@Override
public boolean value(CharSequence input) {
return regExp.matcher(input).matches() && (negativeRegExp == null || !negativeRegExp.matcher(input).matches());
}
};
}
/**
* @deprecated to be removed in IDEA 16
*/
@Nullable
public static Pattern createFileMaskRegExp(@Nullable String filter) {
if (filter == null) {
return null;
}
String pattern;
final List<String> strings = StringUtil.split(filter, ",");
if (strings.size() == 1) {
pattern = PatternUtil.convertToRegex(filter.trim());
}
else {
pattern = StringUtil.join(strings, s -> "(" + PatternUtil.convertToRegex(s.trim()) + ")", "|");
}
return Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
}
/**
* @deprecated to remove in IDEA 16
*/
public static void findUsages(@NotNull FindModel findModel,
@Nullable final PsiDirectory psiDirectory,
@NotNull final Project project,
@NotNull final Processor<UsageInfo> consumer,
@NotNull FindUsagesProcessPresentation processPresentation) {
findUsages(findModel, project, consumer, processPresentation);
}
public static void findUsages(@NotNull FindModel findModel,
@NotNull final Project project,
@NotNull final Processor<UsageInfo> consumer,
@NotNull FindUsagesProcessPresentation processPresentation) {
findUsages(findModel, project, consumer, processPresentation, Collections.emptySet());
}
public static void findUsages(@NotNull FindModel findModel,
@NotNull final Project project,
@NotNull final Processor<UsageInfo> consumer,
@NotNull FindUsagesProcessPresentation processPresentation,
@NotNull Set<VirtualFile> filesToStart) {
new FindInProjectTask(findModel, project, filesToStart).findUsages(consumer, processPresentation);
}
// returns number of hits
static int processUsagesInFile(@NotNull final PsiFile psiFile,
@NotNull final VirtualFile virtualFile,
@NotNull final FindModel findModel,
@NotNull final Processor<UsageInfo> consumer) {
if (findModel.getStringToFind().isEmpty()) {
if (!ReadAction.compute(() -> consumer.process(new UsageInfo(psiFile)))) {
throw new ProcessCanceledException();
}
return 1;
}
if (virtualFile.getFileType().isBinary()) return 0; // do not decompile .class files
final Document document = ReadAction.compute(() -> virtualFile.isValid() ? FileDocumentManager.getInstance().getDocument(virtualFile) : null);
if (document == null) return 0;
final int[] offset = {0};
int count = 0;
int found;
ProgressIndicator indicator = ProgressWrapper.unwrap(ProgressManager.getInstance().getProgressIndicator());
TooManyUsagesStatus tooManyUsagesStatus = TooManyUsagesStatus.getFrom(indicator);
do {
tooManyUsagesStatus.pauseProcessingIfTooManyUsages(); // wait for user out of read action
found = ReadAction.compute(() -> {
if (!psiFile.isValid()) return 0;
return addToUsages(document, consumer, findModel, psiFile, offset, USAGES_PER_READ_ACTION);
});
count += found;
}
while (found != 0);
return count;
}
private static int addToUsages(@NotNull Document document, @NotNull Processor<UsageInfo> consumer, @NotNull FindModel findModel,
@NotNull final PsiFile psiFile, @NotNull int[] offsetRef, int maxUsages) {
int count = 0;
CharSequence text = document.getCharsSequence();
int textLength = document.getTextLength();
int offset = offsetRef[0];
Project project = psiFile.getProject();
FindManager findManager = FindManager.getInstance(project);
while (offset < textLength) {
FindResult result = findManager.findString(text, offset, findModel, psiFile.getVirtualFile());
if (!result.isStringFound()) break;
final int prevOffset = offset;
offset = result.getEndOffset();
if (prevOffset == offset) {
// for regular expr the size of the match could be zero -> could be infinite loop in finding usages!
++offset;
}
final SearchScope customScope = findModel.getCustomScope();
if (customScope instanceof LocalSearchScope) {
final TextRange range = new TextRange(result.getStartOffset(), result.getEndOffset());
if (!((LocalSearchScope)customScope).containsRange(psiFile, range)) continue;
}
UsageInfo info = new FindResultUsageInfo(findManager, psiFile, prevOffset, findModel, result);
if (!consumer.process(info)){
throw new ProcessCanceledException();
}
count++;
if (maxUsages > 0 && count >= maxUsages) {
break;
}
}
offsetRef[0] = offset;
return count;
}
@NotNull
private static String getTitleForScope(@NotNull final FindModel findModel) {
String scopeName;
if (findModel.isProjectScope()) {
scopeName = FindBundle.message("find.scope.project.title");
}
else if (findModel.getModuleName() != null) {
scopeName = FindBundle.message("find.scope.module.title", findModel.getModuleName());
}
else if(findModel.getCustomScopeName() != null) {
scopeName = findModel.getCustomScopeName();
}
else {
scopeName = FindBundle.message("find.scope.directory.title", findModel.getDirectoryName());
}
String result = scopeName;
if (findModel.getFileFilter() != null) {
result += " "+FindBundle.message("find.scope.files.with.mask", findModel.getFileFilter());
}
return result;
}
@NotNull
public static UsageViewPresentation setupViewPresentation(final boolean toOpenInNewTab, @NotNull FindModel findModel) {
final UsageViewPresentation presentation = new UsageViewPresentation();
final String scope = getTitleForScope(findModel);
final String stringToFind = findModel.getStringToFind();
presentation.setScopeText(scope);
if (stringToFind.isEmpty()) {
presentation.setTabText("Files");
presentation.setToolwindowTitle(BundleBase.format("Files in {0}", scope));
presentation.setUsagesString("files");
}
else {
FindModel.SearchContext searchContext = findModel.getSearchContext();
String contextText = "";
if (searchContext != FindModel.SearchContext.ANY) {
contextText = FindBundle.message("find.context.presentation.scope.label", FindDialog.getPresentableName(searchContext));
}
presentation.setTabText(FindBundle.message("find.usage.view.tab.text", stringToFind, contextText));
presentation.setToolwindowTitle(FindBundle.message("find.usage.view.toolwindow.title", stringToFind, scope, contextText));
presentation.setUsagesString(FindBundle.message("find.usage.view.usages.text", stringToFind));
presentation.setUsagesWord(FindBundle.message("occurrence"));
presentation.setCodeUsagesString(FindBundle.message("found.occurrences"));
presentation.setContextText(contextText);
}
presentation.setOpenInNewTab(toOpenInNewTab);
presentation.setCodeUsages(false);
presentation.setUsageTypeFilteringAvailable(true);
return presentation;
}
@NotNull
public static FindUsagesProcessPresentation setupProcessPresentation(@NotNull final Project project,
final boolean showPanelIfOnlyOneUsage,
@NotNull final UsageViewPresentation presentation) {
FindUsagesProcessPresentation processPresentation = new FindUsagesProcessPresentation(presentation);
processPresentation.setShowNotFoundMessage(true);
processPresentation.setShowPanelIfOnlyOneUsage(showPanelIfOnlyOneUsage);
processPresentation.setProgressIndicatorFactory(
() -> new FindProgressIndicator(project, presentation.getScopeText())
);
return processPresentation;
}
/**
* FIXME [VISTALL] find way found regexp plugin
*/
@RequiredReadAction
private static List<PsiElement> getTopLevelRegExpChars(String regExpText, Project project) {
FileType regexpFileType = FileTypeRegistry.getInstance().getFileTypeByExtension("regexp");
if (regexpFileType == UnknownFileType.INSTANCE) {
return Collections.emptyList();
}
PsiFile file = PsiFileFactory.getInstance(project).createFileFromText("A.regexp", regexpFileType, regExpText);
List<PsiElement> result = null;
final PsiElement[] children = file.getChildren();
for (PsiElement child:children) {
PsiElement[] grandChildren = child.getChildren();
if (grandChildren.length != 1) return Collections.emptyList(); // a | b, more than one branch, can not predict in current way
for(PsiElement grandGrandChild:grandChildren[0].getChildren()) {
if (result == null) result = new ArrayList<>();
result.add(grandGrandChild);
}
}
return result != null ? result : Collections.emptyList();
}
@NotNull
static String buildStringToFindForIndicesFromRegExp(@NotNull String stringToFind, @NotNull Project project) {
if (!Registry.is("idea.regexp.search.uses.indices")) return "";
return ReadAction.compute(() -> {
final List<PsiElement> topLevelRegExpChars = getTopLevelRegExpChars("a", project);
if (topLevelRegExpChars.size() != 1) return "";
// leave only top level regExpChars
return StringUtil.join(getTopLevelRegExpChars(stringToFind, project), new Function<PsiElement, String>() {
final Class regExpCharPsiClass = topLevelRegExpChars.get(0).getClass();
@Override
public String fun(PsiElement element) {
if (regExpCharPsiClass.isInstance(element)) {
String text = element.getText();
if (!text.startsWith("\\")) return text;
}
return " ";
}
}, "");
});
}
public static void initStringToFindFromDataContext(FindModel findModel, @NotNull DataContext dataContext) {
Editor editor = CommonDataKeys.EDITOR.getData(dataContext);
FindUtil.initStringToFindWithSelection(findModel, editor);
if (editor == null || !editor.getSelectionModel().hasSelection()) {
FindUtil.useFindStringFromFindInFileModel(findModel, CommonDataKeys.EDITOR_EVEN_IF_INACTIVE.getData(dataContext));
}
}
public static class StringUsageTarget implements ConfigurableUsageTarget, ItemPresentation, TypeSafeDataProvider {
@NotNull protected final Project myProject;
@NotNull protected final FindModel myFindModel;
public StringUsageTarget(@NotNull Project project, @NotNull FindModel findModel) {
myProject = project;
myFindModel = findModel;
}
@Override
@NotNull
public String getPresentableText() {
UsageViewPresentation presentation = setupViewPresentation(false, myFindModel);
return presentation.getToolwindowTitle();
}
@NotNull
@Override
public String getLongDescriptiveName() {
return getPresentableText();
}
@Override
public String getLocationString() {
return myFindModel + "!!";
}
@Override
public Icon getIcon(boolean open) {
return AllIcons.Actions.Menu_find;
}
@Override
public void findUsages() {
FindInProjectManager.getInstance(myProject).startFindInProject(myFindModel);
}
@Override
public void findUsagesInEditor(@NotNull FileEditor editor) {}
@Override
public void highlightUsages(@NotNull PsiFile file, @NotNull Editor editor, boolean clearHighlights) {}
@Override
public boolean isValid() {
return true;
}
@Override
public boolean isReadOnly() {
return true;
}
@Override
@Nullable
public VirtualFile[] getFiles() {
return null;
}
@Override
public void update() {
}
@Override
public String getName() {
return myFindModel.getStringToFind().isEmpty() ? myFindModel.getFileFilter() : myFindModel.getStringToFind();
}
@Override
public ItemPresentation getPresentation() {
return this;
}
@Override
public void navigate(boolean requestFocus) {
throw new UnsupportedOperationException();
}
@Override
public boolean canNavigate() {
return false;
}
@Override
public boolean canNavigateToSource() {
return false;
}
@Override
public void showSettings() {
Content selectedContent = UsageViewManager.getInstance(myProject).getSelectedContent(true);
JComponent component = selectedContent == null ? null : selectedContent.getComponent();
FindInProjectManager findInProjectManager = FindInProjectManager.getInstance(myProject);
findInProjectManager.findInProject(DataManager.getInstance().getDataContext(component));
}
@Override
public KeyboardShortcut getShortcut() {
return ActionManager.getInstance().getKeyboardShortcut("FindInPath");
}
@Override
public void calcData(DataKey key, DataSink sink) {
if (UsageView.USAGE_SCOPE.equals(key)) {
SearchScope scope = getScopeFromModel(myProject, myFindModel);
sink.put(UsageView.USAGE_SCOPE, scope);
}
}
}
private static void addSourceDirectoriesFromLibraries(@NotNull Project project,
@NotNull VirtualFile directory,
@NotNull Collection<VirtualFile> outSourceRoots) {
ProjectFileIndex index = ProjectFileIndex.SERVICE.getInstance(project);
// if we already are in the sources, search just in this directory only
if (!index.isInLibraryClasses(directory)) return;
VirtualFile classRoot = index.getClassRootForFile(directory);
if (classRoot == null) return;
String relativePath = VfsUtilCore.getRelativePath(directory, classRoot);
if (relativePath == null) return;
Collection<VirtualFile> otherSourceRoots = new THashSet<>();
// if we are in the library sources, return (to search in this directory only)
// otherwise, if we outside sources or in a jar directory, add directories from other source roots
searchForOtherSourceDirs:
for (OrderEntry entry : index.getOrderEntriesForFile(directory)) {
if (entry instanceof LibraryOrderEntry) {
Library library = ((LibraryOrderEntry)entry).getLibrary();
if (library == null) continue;
// note: getUrls() returns jar directories too
String[] sourceUrls = library.getUrls(OrderRootType.SOURCES);
for (String sourceUrl : sourceUrls) {
if (VfsUtilCore.isEqualOrAncestor(sourceUrl, directory.getUrl())) {
// already in this library sources, no need to look for another source root
otherSourceRoots.clear();
break searchForOtherSourceDirs;
}
// otherwise we may be inside the jar file in a library which is configured as a jar directory
// in which case we have no way to know whether this is a source jar or classes jar - so try to locate the source jar
}
}
for (VirtualFile sourceRoot : entry.getFiles(OrderRootType.SOURCES)) {
VirtualFile sourceFile = sourceRoot.findFileByRelativePath(relativePath);
if (sourceFile != null) {
otherSourceRoots.add(sourceFile);
}
}
}
outSourceRoots.addAll(otherSourceRoots);
}
@NotNull
static SearchScope getScopeFromModel(@NotNull Project project, @NotNull FindModel findModel) {
SearchScope customScope = findModel.getCustomScope();
VirtualFile directory = getDirectory(findModel);
Module module = findModel.getModuleName() == null ? null : ModuleManager.getInstance(project).findModuleByName(findModel.getModuleName());
return findModel.isCustomScope() && customScope != null ? customScope.intersectWith(GlobalSearchScope.allScope(project)) :
// we don't have to check for myProjectFileIndex.isExcluded(file) here like FindInProjectTask.collectFilesInScope() does
// because all found usages are guaranteed to be not in excluded dir
directory != null ? forDirectory(project, findModel.isWithSubdirectories(), directory) :
module != null ? module.getModuleContentScope() :
findModel.isProjectScope() ? ProjectScope.getContentScope(project) :
GlobalSearchScope.allScope(project);
}
@NotNull
private static GlobalSearchScope forDirectory(@NotNull Project project,
boolean withSubdirectories,
@NotNull VirtualFile directory) {
Set<VirtualFile> result = new LinkedHashSet<>();
result.add(directory);
addSourceDirectoriesFromLibraries(project, directory, result);
VirtualFile[] array = result.toArray(new VirtualFile[result.size()]);
return GlobalSearchScopesCore.directoriesScope(project, withSubdirectories, array);
}
}