/* * 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.usages.impl; import com.intellij.diagnostic.PerformanceWatcher; 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.ModalityState; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.application.TransactionGuard; 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.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.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.Processor; import com.intellij.util.Processors; import com.intellij.util.ui.RangeBlinker; import com.intellij.util.ui.UIUtil; 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<>(); @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 = " (" + 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<VirtualFile> largeFiles = processPresentation.getLargeFiles(); List<String> resultLines = new ArrayList<>(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, () -> { String detailedMessage = detailedLargeFilesMessage(largeFiles); List<String> strings = new ArrayList<>(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 project configuration 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<VirtualFile> largeFiles) { String message = ""; if (largeFiles.size() == 1) { final VirtualFile vFile = largeFiles.iterator().next(); message += "File " + presentableFileInfo(vFile) + " is "; } else { message += "Files<br> "; int counter = 0; for (VirtualFile vFile : largeFiles) { 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) + " (" + UsageViewManagerImpl.presentableSize(UsageViewManagerImpl.getFileLength(vFile)) + ")"; } @NotNull private static String getPresentablePath(@NotNull final VirtualFile virtualFile) { return "'" + ReadAction.compute(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)) { TransactionGuard.getInstance().submitTransactionAndWait( () -> 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) { TransactionGuard.getInstance().submitTransactionAndWait( () -> FindManager.getInstance(myProject).findUsagesInScope(psiElement, GlobalSearchScope.projectScope(myProject))); } } } }; } private static PsiElement getPsiElement(@NotNull UsageTarget[] searchFor) { final UsageTarget target = searchFor[0]; if (!(target instanceof PsiElementUsageTarget)) return null; return ReadAction.compute(((PsiElementUsageTarget)target)::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<>(); Processor<Segment> processor = Processors.cancelableCollectProcessor(segments); usageInfo.processRangeMarkers(processor); rangeBlinker.resetMarkers(segments); rangeBlinker.startBlinking(); } private UsageViewImpl getUsageView(@NotNull 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(() -> finalUsageView.appendUsage(firstUsage)); } } else { UsageViewImpl finalUsageView = usageView; // later because dispose does some sort of swing magic e.g. AnAction.unregisterCustomShortcutSet() UIUtil.invokeLaterIfNeeded(() -> Disposer.dispose(finalUsageView)); } return myUsageViewRef.get(); } return null; } private void openView(@NotNull final UsageViewImpl usageView) { SwingUtilities.invokeLater(() -> { if (myProject.isDisposed()) return; myUsageViewManager.addContent(usageView, myPresentation); if (myListener != null) { myListener.usageViewCreated(usageView); } myUsageViewManager.showToolWindow(false); }); } @Override public void run() { PerformanceWatcher.Snapshot snapshot = PerformanceWatcher.takeSnapshot(); AtomicBoolean findUsagesStartedShown = new AtomicBoolean(); searchUsages(findUsagesStartedShown); endSearchForUsages(findUsagesStartedShown); snapshot.logResponsivenessSinceCreation("Find Usages"); } private void searchUsages(@NotNull final AtomicBoolean findStartedBalloonShown) { ProgressIndicator indicator = ProgressWrapper.unwrap(ProgressManager.getInstance().getProgressIndicator()); assert indicator != null : "must run find usages under progress"; TooManyUsagesStatus.createFor(indicator); Alarm findUsagesStartedBalloon = new Alarm(); findUsagesStartedBalloon.addRequest(() -> { 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(usage -> { ProgressIndicator indicator1 = ProgressWrapper.unwrap(ProgressManager.getInstance().getProgressIndicator()); assert indicator1 != null : "must run find usages under progress"; if (indicator1.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(indicator1); TooManyUsagesStatus tooManyUsagesStatus= TooManyUsagesStatus.getFrom(indicator1); if (usageCount > UsageLimitUtil.USAGES_LIMIT && tooManyUsagesStatus.switchTooManyUsagesStatus()) { UsageViewManagerImpl.showTooManyUsagesWarning(myProject, tooManyUsagesStatus, indicator1, myPresentation, usageCount, usageView); } tooManyUsagesStatus.pauseProcessingIfTooManyUsages(); if (usageView != null) { ApplicationManager.getApplication().runReadAction(() -> usageView.appendUsage(usage)); } } return !indicator1.isCanceled(); }); if (getUsageView(indicator) != null) { ApplicationManager.getApplication().invokeLater(() -> myUsageViewManager.showToolWindow(true), myProject.getDisposed()); } Disposer.dispose(findUsagesStartedBalloon); ApplicationManager.getApplication().invokeLater(() -> { 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, Collections.singletonList("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(), myPresentation.getContextText() ); if (notFoundActions.isEmpty()) { List<String> lines = new ArrayList<>(); 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<>(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(() -> { Usage usage = myFirstUsage.get(); if (usage.canNavigate()) { usage.navigate(true); flashUsageScriptaculously(usage); } List<String> lines = new ArrayList<>(); 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(() -> { 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()); } } }