| /* |
| * 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 = " (" + 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) |
| + " (" |
| + 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()); |
| } |
| } |
| } |