blob: 6e5e93f3ed9657591994e7e51f2e10b2605d24c6 [file] [log] [blame]
/*
* Copyright 2000-2014 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.usages.impl;
import com.intellij.find.FindManager;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.KeyboardShortcut;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.colors.CodeInsightColors;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.keymap.KeymapUtil;
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.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Factory;
import com.intellij.openapi.util.Segment;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.ToolWindowId;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.SearchScope;
import com.intellij.ui.HyperlinkAdapter;
import com.intellij.usageView.UsageViewBundle;
import com.intellij.usages.*;
import com.intellij.util.Alarm;
import com.intellij.util.ArrayUtil;
import com.intellij.util.CommonProcessors;
import com.intellij.util.Processor;
import com.intellij.util.ui.RangeBlinker;
import com.intellij.xml.util.XmlStringUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import java.awt.event.ActionEvent;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
class SearchForUsagesRunnable implements Runnable {
@NonNls private static final String FIND_OPTIONS_HREF_TARGET = "FindOptions";
@NonNls private static final String SEARCH_IN_PROJECT_HREF_TARGET = "SearchInProject";
@NonNls private static final String LARGE_FILES_HREF_TARGET = "LargeFiles";
@NonNls private static final String SHOW_PROJECT_FILE_OCCURRENCES_HREF_TARGET = "SHOW_PROJECT_FILE_OCCURRENCES";
private final AtomicInteger myUsageCountWithoutDefinition = new AtomicInteger(0);
private final AtomicReference<Usage> myFirstUsage = new AtomicReference<Usage>();
@NotNull
private final Project myProject;
private final AtomicReference<UsageViewImpl> myUsageViewRef;
private final UsageViewPresentation myPresentation;
private final UsageTarget[] mySearchFor;
private final Factory<UsageSearcher> mySearcherFactory;
private final FindUsagesProcessPresentation myProcessPresentation;
@NotNull private final SearchScope mySearchScopeToWarnOfFallingOutOf;
private final UsageViewManager.UsageViewStateListener myListener;
private final UsageViewManagerImpl myUsageViewManager;
private final AtomicInteger myOutOfScopeUsages = new AtomicInteger();
SearchForUsagesRunnable(@NotNull UsageViewManagerImpl usageViewManager,
@NotNull Project project,
@NotNull AtomicReference<UsageViewImpl> usageViewRef,
@NotNull UsageViewPresentation presentation,
@NotNull UsageTarget[] searchFor,
@NotNull Factory<UsageSearcher> searcherFactory,
@NotNull FindUsagesProcessPresentation processPresentation,
@NotNull SearchScope searchScopeToWarnOfFallingOutOf,
@Nullable UsageViewManager.UsageViewStateListener listener) {
myProject = project;
myUsageViewRef = usageViewRef;
myPresentation = presentation;
mySearchFor = searchFor;
mySearcherFactory = searcherFactory;
myProcessPresentation = processPresentation;
mySearchScopeToWarnOfFallingOutOf = searchScopeToWarnOfFallingOutOf;
myListener = listener;
myUsageViewManager = usageViewManager;
}
@NotNull
private static String createOptionsHtml(@NonNls UsageTarget[] searchFor) {
KeyboardShortcut shortcut = UsageViewImpl.getShowUsagesWithSettingsShortcut(searchFor);
String shortcutText = "";
if (shortcut != null) {
shortcutText = "&nbsp;(" + KeymapUtil.getShortcutText(shortcut) + ")";
}
return "<a href='" + FIND_OPTIONS_HREF_TARGET + "'>Find Options...</a>" + shortcutText;
}
@NotNull
private static String createSearchInProjectHtml() {
return "<a href='" + SEARCH_IN_PROJECT_HREF_TARGET + "'>Search in Project</a>";
}
private static void notifyByFindBalloon(@Nullable final HyperlinkListener listener,
@NotNull final MessageType info,
@NotNull FindUsagesProcessPresentation processPresentation,
@NotNull final Project project,
@NotNull final List<String> lines) {
com.intellij.usageView.UsageViewManager.getInstance(project); // in case tool window not registered
final Collection<PsiFile> largeFiles = processPresentation.getLargeFiles();
List<String> resultLines = new ArrayList<String>(lines);
HyperlinkListener resultListener = listener;
if (!largeFiles.isEmpty()) {
String shortMessage = "(<a href='" + LARGE_FILES_HREF_TARGET + "'>"
+ UsageViewBundle.message("large.files.were.ignored", largeFiles.size()) + "</a>)";
resultLines.add(shortMessage);
resultListener = addHrefHandling(resultListener, LARGE_FILES_HREF_TARGET, new Runnable() {
public void run() {
String detailedMessage = detailedLargeFilesMessage(largeFiles);
List<String> strings = new ArrayList<String>(lines);
strings.add(detailedMessage);
//noinspection SSBasedInspection
ToolWindowManager.getInstance(project).notifyByBalloon(ToolWindowId.FIND, info, wrapInHtml(strings), AllIcons.Actions.Find, listener);
}
});
}
Runnable searchIncludingProjectFileUsages = processPresentation.searchIncludingProjectFileUsages();
if (searchIncludingProjectFileUsages != null) {
resultLines.add("Occurrences in " + ApplicationNamesInfo.getInstance().getProductName() + " project files are skipped. " +
"<a href='" + SHOW_PROJECT_FILE_OCCURRENCES_HREF_TARGET + "'>Include them</a>");
resultListener = addHrefHandling(resultListener, SHOW_PROJECT_FILE_OCCURRENCES_HREF_TARGET, searchIncludingProjectFileUsages);
}
//noinspection SSBasedInspection
ToolWindowManager.getInstance(project).notifyByBalloon(ToolWindowId.FIND, info, wrapInHtml(resultLines), AllIcons.Actions.Find, resultListener);
}
private static HyperlinkListener addHrefHandling(@Nullable final HyperlinkListener listener,
@NotNull final String hrefTarget, @NotNull final Runnable handler) {
return new HyperlinkAdapter() {
@Override
protected void hyperlinkActivated(HyperlinkEvent e) {
if (e.getDescription().equals(hrefTarget)) {
handler.run();
}
else if (listener != null) {
listener.hyperlinkUpdate(e);
}
}
};
}
@NotNull
private static String wrapInHtml(@NotNull List<String> strings) {
return XmlStringUtil.wrapInHtml(StringUtil.join(strings, "<br>"));
}
@NotNull
private static String detailedLargeFilesMessage(@NotNull Collection<PsiFile> largeFiles) {
String message = "";
if (largeFiles.size() == 1) {
final VirtualFile vFile = largeFiles.iterator().next().getVirtualFile();
message += "File " + presentableFileInfo(vFile) + " is ";
}
else {
message += "Files<br> ";
int counter = 0;
for (PsiFile file : largeFiles) {
final VirtualFile vFile = file.getVirtualFile();
message += presentableFileInfo(vFile) + "<br> ";
if (counter++ > 10) break;
}
message += "are ";
}
message += "too large and cannot be scanned";
return message;
}
@NotNull
private static String presentableFileInfo(@NotNull VirtualFile vFile) {
return getPresentablePath(vFile)
+ "&nbsp;("
+ UsageViewManagerImpl.presentableSize(UsageViewManagerImpl.getFileLength(vFile))
+ ")";
}
@NotNull
private static String getPresentablePath(@NotNull final VirtualFile virtualFile) {
return "'" + ApplicationManager.getApplication().runReadAction(new Computable<String>() {
@Override
public String compute() {
return virtualFile.getPresentableUrl();
}
}) + "'";
}
@NotNull
private HyperlinkListener createGotToOptionsListener(@NotNull final UsageTarget[] targets) {
return new HyperlinkAdapter() {
@Override
protected void hyperlinkActivated(HyperlinkEvent e) {
if (e.getDescription().equals(FIND_OPTIONS_HREF_TARGET)) {
FindManager.getInstance(myProject).showSettingsAndFindUsages(targets);
}
}
};
}
@NotNull
private HyperlinkListener createSearchInProjectListener() {
return new HyperlinkAdapter() {
@Override
protected void hyperlinkActivated(HyperlinkEvent e) {
if (e.getDescription().equals(SEARCH_IN_PROJECT_HREF_TARGET)) {
PsiElement psiElement = getPsiElement(mySearchFor);
if (psiElement != null) {
FindManager.getInstance(myProject).findUsagesInScope(psiElement, GlobalSearchScope.projectScope(myProject));
}
}
}
};
}
private static PsiElement getPsiElement(UsageTarget[] searchFor) {
if (!(searchFor[0] instanceof PsiElementUsageTarget)) return null;
return ((PsiElementUsageTarget)searchFor[0]).getElement();
}
private static void flashUsageScriptaculously(@NotNull final Usage usage) {
if (!(usage instanceof UsageInfo2UsageAdapter)) {
return;
}
UsageInfo2UsageAdapter usageInfo = (UsageInfo2UsageAdapter)usage;
Editor editor = usageInfo.openTextEditor(true);
if (editor == null) return;
TextAttributes attributes = EditorColorsManager.getInstance().getGlobalScheme().getAttributes(CodeInsightColors.BLINKING_HIGHLIGHTS_ATTRIBUTES);
RangeBlinker rangeBlinker = new RangeBlinker(editor, attributes, 6);
List<Segment> segments = new ArrayList<Segment>();
CommonProcessors.CollectProcessor<Segment> processor = new CommonProcessors.CollectProcessor<Segment>(segments);
usageInfo.processRangeMarkers(processor);
rangeBlinker.resetMarkers(segments);
rangeBlinker.startBlinking();
}
private UsageViewImpl getUsageView(ProgressIndicator indicator) {
UsageViewImpl usageView = myUsageViewRef.get();
if (usageView != null) return usageView;
int usageCount = myUsageCountWithoutDefinition.get();
if (usageCount >= 2 || usageCount == 1 && myProcessPresentation.isShowPanelIfOnlyOneUsage()) {
usageView = new UsageViewImpl(myProject, myPresentation, mySearchFor, mySearcherFactory);
usageView.associateProgress(indicator);
if (myUsageViewRef.compareAndSet(null, usageView)) {
openView(usageView);
final Usage firstUsage = myFirstUsage.get();
if (firstUsage != null) {
final UsageViewImpl finalUsageView = usageView;
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
finalUsageView.appendUsage(firstUsage);
}
});
}
}
else {
Disposer.dispose(usageView);
}
return myUsageViewRef.get();
}
return null;
}
private void openView(@NotNull final UsageViewImpl usageView) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (myProject.isDisposed()) return;
myUsageViewManager.addContent(usageView, myPresentation);
if (myListener != null) {
myListener.usageViewCreated(usageView);
}
myUsageViewManager.showToolWindow(false);
}
});
}
@Override
public void run() {
AtomicBoolean findUsagesStartedShown = new AtomicBoolean();
searchUsages(findUsagesStartedShown);
endSearchForUsages(findUsagesStartedShown);
}
private void searchUsages(@NotNull final AtomicBoolean findStartedBalloonShown) {
ProgressIndicator indicator = ProgressWrapper.unwrap(ProgressManager.getInstance().getProgressIndicator());
TooManyUsagesStatus.createFor(indicator);
Alarm findUsagesStartedBalloon = new Alarm();
findUsagesStartedBalloon.addRequest(new Runnable() {
@Override
public void run() {
notifyByFindBalloon(null, MessageType.WARNING, myProcessPresentation, myProject,
Collections.singletonList(StringUtil.escapeXml(UsageViewManagerImpl.getProgressTitle(myPresentation))));
findStartedBalloonShown.set(true);
}
}, 300, ModalityState.NON_MODAL);
UsageSearcher usageSearcher = mySearcherFactory.create();
usageSearcher.generate(new Processor<Usage>() {
@Override
public boolean process(final Usage usage) {
ProgressIndicator indicator = ProgressWrapper.unwrap(ProgressManager.getInstance().getProgressIndicator());
if (indicator != null && indicator.isCanceled()) return false;
if (!UsageViewManagerImpl.isInScope(usage, mySearchScopeToWarnOfFallingOutOf)) {
myOutOfScopeUsages.incrementAndGet();
return true;
}
boolean incrementCounter = !UsageViewManager.isSelfUsage(usage, mySearchFor);
if (incrementCounter) {
final int usageCount = myUsageCountWithoutDefinition.incrementAndGet();
if (usageCount == 1 && !myProcessPresentation.isShowPanelIfOnlyOneUsage()) {
myFirstUsage.compareAndSet(null, usage);
}
final UsageViewImpl usageView = getUsageView(indicator);
TooManyUsagesStatus tooManyUsagesStatus;
if (usageCount > UsageLimitUtil.USAGES_LIMIT && (tooManyUsagesStatus = TooManyUsagesStatus.getFrom(indicator)).switchTooManyUsagesStatus()) {
UsageViewManagerImpl.showTooManyUsagesWarning(myProject, tooManyUsagesStatus, indicator, myPresentation, usageCount, usageView);
}
if (usageView != null) {
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
usageView.appendUsage(usage);
}
});
}
}
return indicator == null || !indicator.isCanceled();
}
});
if (getUsageView(indicator) != null) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
myUsageViewManager.showToolWindow(true);
}
}, myProject.getDisposed());
}
Disposer.dispose(findUsagesStartedBalloon);
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
if (findStartedBalloonShown.get()) {
Balloon balloon = ToolWindowManager.getInstance(myProject).getToolWindowBalloon(ToolWindowId.FIND);
if (balloon != null) {
balloon.hide();
}
}
}
}, myProject.getDisposed());
}
private void endSearchForUsages(@NotNull final AtomicBoolean findStartedBalloonShown) {
assert !ApplicationManager.getApplication().isDispatchThread() : Thread.currentThread();
int usageCount = myUsageCountWithoutDefinition.get();
if (usageCount == 0 && myProcessPresentation.isShowNotFoundMessage()) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
if (myProcessPresentation.isCanceled()) {
notifyByFindBalloon(null, MessageType.WARNING, myProcessPresentation, myProject, Arrays.asList("Usage search was canceled"));
findStartedBalloonShown.set(false);
return;
}
final List<Action> notFoundActions = myProcessPresentation.getNotFoundActions();
final String message = UsageViewBundle.message("dialog.no.usages.found.in",
StringUtil.decapitalize(myPresentation.getUsagesString()),
myPresentation.getScopeText());
if (notFoundActions.isEmpty()) {
List<String> lines = new ArrayList<String>();
lines.add(StringUtil.escapeXml(message));
if (myOutOfScopeUsages.get() != 0) {
lines.add(UsageViewManagerImpl.outOfScopeMessage(myOutOfScopeUsages.get(), mySearchScopeToWarnOfFallingOutOf));
}
if (myProcessPresentation.isShowFindOptionsPrompt()) {
lines.add(createOptionsHtml(mySearchFor));
}
MessageType type = myOutOfScopeUsages.get() == 0 ? MessageType.INFO : MessageType.WARNING;
notifyByFindBalloon(createGotToOptionsListener(mySearchFor),
type, myProcessPresentation, myProject, lines);
findStartedBalloonShown.set(false);
}
else {
List<String> titles = new ArrayList<String>(notFoundActions.size() + 1);
titles.add(UsageViewBundle.message("dialog.button.ok"));
for (Action action : notFoundActions) {
Object value = action.getValue(FindUsagesProcessPresentation.NAME_WITH_MNEMONIC_KEY);
if (value == null) value = action.getValue(Action.NAME);
titles.add((String)value);
}
int option = Messages.showDialog(myProject, message, UsageViewBundle.message("dialog.title.information"),
ArrayUtil.toStringArray(titles), 0, Messages.getInformationIcon());
if (option > 0) {
notFoundActions.get(option - 1).actionPerformed(new ActionEvent(this, 0, titles.get(option)));
}
}
}
}, ModalityState.NON_MODAL, myProject.getDisposed());
}
else if (usageCount == 1 && !myProcessPresentation.isShowPanelIfOnlyOneUsage()) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
Usage usage = myFirstUsage.get();
if (usage.canNavigate()) {
usage.navigate(true);
flashUsageScriptaculously(usage);
}
List<String> lines = new ArrayList<String>();
lines.add("Only one usage found.");
if (myOutOfScopeUsages.get() != 0) {
lines.add(UsageViewManagerImpl.outOfScopeMessage(myOutOfScopeUsages.get(), mySearchScopeToWarnOfFallingOutOf));
}
lines.add(createOptionsHtml(mySearchFor));
MessageType type = myOutOfScopeUsages.get() == 0 ? MessageType.INFO : MessageType.WARNING;
notifyByFindBalloon(createGotToOptionsListener(mySearchFor),
type, myProcessPresentation, myProject,
lines);
}
}, ModalityState.NON_MODAL, myProject.getDisposed());
}
else {
final UsageViewImpl usageView = myUsageViewRef.get();
if (usageView != null) {
usageView.drainQueuedUsageNodes();
usageView.setSearchInProgress(false);
}
final List<String> lines;
final HyperlinkListener hyperlinkListener;
if (myOutOfScopeUsages.get() == 0 || getPsiElement(mySearchFor)==null) {
lines = Collections.emptyList();
hyperlinkListener = null;
}
else {
lines = Arrays.asList(UsageViewManagerImpl.outOfScopeMessage(myOutOfScopeUsages.get(), mySearchScopeToWarnOfFallingOutOf), createSearchInProjectHtml());
hyperlinkListener = createSearchInProjectListener();
}
if (!myProcessPresentation.getLargeFiles().isEmpty() ||
myOutOfScopeUsages.get() != 0 ||
myProcessPresentation.searchIncludingProjectFileUsages() != null) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
MessageType type = myOutOfScopeUsages.get() == 0 ? MessageType.INFO : MessageType.WARNING;
notifyByFindBalloon(hyperlinkListener, type, myProcessPresentation, myProject, lines);
}
}, ModalityState.NON_MODAL, myProject.getDisposed());
}
}
if (myListener != null) {
myListener.findingUsagesFinished(myUsageViewRef.get());
}
}
}