/* * 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.actions; import com.intellij.codeInsight.TargetElementUtil; import com.intellij.codeInsight.hint.HintManager; import com.intellij.codeInsight.hint.HintUtil; import com.intellij.featureStatistics.FeatureUsageTracker; import com.intellij.find.FindManager; import com.intellij.find.FindSettings; import com.intellij.find.UsagesPreviewPanelProvider; import com.intellij.find.findUsages.*; import com.intellij.find.impl.FindManagerImpl; import com.intellij.icons.AllIcons; import com.intellij.ide.DataManager; import com.intellij.ide.util.gotoByName.ModelDiff; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.application.TransactionGuard; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.fileEditor.FileEditor; import com.intellij.openapi.fileEditor.FileEditorLocation; import com.intellij.openapi.fileEditor.TextEditor; import com.intellij.openapi.fileEditor.impl.text.AsyncEditorLoader; import com.intellij.openapi.keymap.KeymapUtil; import com.intellij.openapi.preview.PreviewManager; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.MessageType; import com.intellij.openapi.ui.popup.JBPopup; import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.openapi.ui.popup.PopupChooserBuilder; import com.intellij.openapi.ui.popup.util.PopupUtil; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.pom.Navigatable; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.search.SearchScope; import com.intellij.ui.*; import com.intellij.ui.awt.RelativePoint; import com.intellij.ui.popup.AbstractPopup; import com.intellij.ui.popup.HintUpdateSupply; import com.intellij.ui.table.JBTable; import com.intellij.usageView.UsageInfo; import com.intellij.usageView.UsageViewBundle; import com.intellij.usageView.UsageViewUtil; import com.intellij.usages.*; import com.intellij.usages.impl.*; import com.intellij.usages.rules.UsageFilteringRuleProvider; import com.intellij.util.Alarm; import com.intellij.util.ArrayUtil; import com.intellij.util.PlatformIcons; import com.intellij.util.Processor; import com.intellij.util.messages.MessageBusConnection; import com.intellij.util.ui.AsyncProcessIcon; import com.intellij.util.ui.ColumnInfo; import com.intellij.util.ui.ListTableModel; 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.table.TableCellRenderer; import javax.swing.table.TableColumn; import java.awt.*; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.*; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; public class ShowUsagesAction extends AnAction implements PopupAction { public static final String ID = "ShowUsages"; public static final int USAGES_PAGE_SIZE = 100; static final Usage MORE_USAGES_SEPARATOR = NullUsage.INSTANCE; static final Usage USAGES_OUTSIDE_SCOPE_SEPARATOR = new UsageAdapter(); private static final UsageNode MORE_USAGES_SEPARATOR_NODE = UsageViewImpl.NULL_NODE; private static final UsageNode USAGES_OUTSIDE_SCOPE_NODE = new UsageNode(null, USAGES_OUTSIDE_SCOPE_SEPARATOR); private static final Comparator<UsageNode> USAGE_NODE_COMPARATOR = (c1, c2) -> { if (c1 instanceof StringNode || c2 instanceof StringNode) { if (c1 instanceof StringNode && c2 instanceof StringNode) { return Comparing.compare(c1.toString(), c2.toString()); } return c1 instanceof StringNode ? 1 : -1; } Usage o1 = c1.getUsage(); Usage o2 = c2.getUsage(); int weight1 = o1 == USAGES_OUTSIDE_SCOPE_SEPARATOR ? 2 : o1 == MORE_USAGES_SEPARATOR ? 1 : 0; int weight2 = o2 == USAGES_OUTSIDE_SCOPE_SEPARATOR ? 2 : o2 == MORE_USAGES_SEPARATOR ? 1 : 0; if (weight1 != weight2) return weight1 - weight2; if (o1 instanceof Comparable && o2 instanceof Comparable) { //noinspection unchecked return ((Comparable)o1).compareTo(o2); } VirtualFile v1 = UsageListCellRenderer.getVirtualFile(o1); VirtualFile v2 = UsageListCellRenderer.getVirtualFile(o2); String name1 = v1 == null ? null : v1.getName(); String name2 = v2 == null ? null : v2.getName(); int i = Comparing.compare(name1, name2); if (i != 0) return i; if (Comparing.equal(v1, v2)) { FileEditorLocation loc1 = o1.getLocation(); FileEditorLocation loc2 = o2.getLocation(); return Comparing.compare(loc1, loc2); } else { String path1 = v1 == null ? null : v1.getPath(); String path2 = v2 == null ? null : v2.getPath(); return Comparing.compare(path1, path2); } }; private final boolean myShowSettingsDialogBefore; private final UsageViewSettings myUsageViewSettings; private Runnable mySearchEverywhereRunnable; // used from plugin.xml @SuppressWarnings("UnusedDeclaration") public ShowUsagesAction() { this(false); } private ShowUsagesAction(boolean showDialogBefore) { setInjectedContext(true); myShowSettingsDialogBefore = showDialogBefore; final UsageViewSettings usageViewSettings = UsageViewSettings.getInstance(); myUsageViewSettings = new UsageViewSettings(); myUsageViewSettings.loadState(usageViewSettings); myUsageViewSettings.GROUP_BY_FILE_STRUCTURE = false; myUsageViewSettings.GROUP_BY_MODULE = false; myUsageViewSettings.GROUP_BY_PACKAGE = false; myUsageViewSettings.GROUP_BY_USAGE_TYPE = false; myUsageViewSettings.GROUP_BY_SCOPE = false; } @Override public boolean startInTransaction() { return true; } @Override public void update(@NotNull AnActionEvent e) { FindUsagesInFileAction.updateFindUsagesAction(e); if (e.getPresentation().isEnabled()) { UsageTarget[] usageTargets = e.getData(UsageView.USAGE_TARGETS_KEY); if (usageTargets != null && !(ArrayUtil.getFirstElement(usageTargets) instanceof PsiElementUsageTarget)) { e.getPresentation().setEnabled(false); } } } @Override public void actionPerformed(@NotNull AnActionEvent e) { final Project project = e.getProject(); if (project == null) return; Runnable searchEverywhere = mySearchEverywhereRunnable; mySearchEverywhereRunnable = null; hideHints(); if (searchEverywhere != null) { searchEverywhere.run(); return; } final RelativePoint popupPosition = JBPopupFactory.getInstance().guessBestPopupLocation(e.getDataContext()); PsiDocumentManager.getInstance(project).commitAllDocuments(); FeatureUsageTracker.getInstance().triggerFeatureUsed("navigation.goto.usages"); UsageTarget[] usageTargets = e.getData(UsageView.USAGE_TARGETS_KEY); final Editor editor = e.getData(CommonDataKeys.EDITOR); if (usageTargets == null) { FindUsagesAction.chooseAmbiguousTargetAndPerform(project, editor, element -> { startFindUsages(element, popupPosition, editor, USAGES_PAGE_SIZE); return false; }); } else if (ArrayUtil.getFirstElement(usageTargets) instanceof PsiElementUsageTarget) { PsiElement element = ((PsiElementUsageTarget)usageTargets[0]).getElement(); if (element != null) { startFindUsages(element, popupPosition, editor, USAGES_PAGE_SIZE); } } } private static void hideHints() { HintManager.getInstance().hideHints(HintManager.HIDE_BY_ANY_KEY, false, false); } public void startFindUsages(@NotNull PsiElement element, @NotNull RelativePoint popupPosition, Editor editor, int maxUsages) { Project project = element.getProject(); FindUsagesManager findUsagesManager = ((FindManagerImpl)FindManager.getInstance(project)).getFindUsagesManager(); FindUsagesHandler handler = findUsagesManager.getFindUsagesHandler(element, false); if (handler == null) return; if (myShowSettingsDialogBefore) { showDialogAndFindUsages(handler, popupPosition, editor, maxUsages); return; } showElementUsages(editor, popupPosition, handler, maxUsages, handler.getFindUsagesOptions(DataManager.getInstance().getDataContext())); } private void showElementUsages(final Editor editor, @NotNull final RelativePoint popupPosition, @NotNull final FindUsagesHandler handler, final int maxUsages, @NotNull final FindUsagesOptions options) { ApplicationManager.getApplication().assertIsDispatchThread(); final UsageViewSettings usageViewSettings = UsageViewSettings.getInstance(); final UsageViewSettings savedGlobalSettings = new UsageViewSettings(); savedGlobalSettings.loadState(usageViewSettings); usageViewSettings.loadState(myUsageViewSettings); final Project project = handler.getProject(); UsageViewManager manager = UsageViewManager.getInstance(project); FindUsagesManager findUsagesManager = ((FindManagerImpl)FindManager.getInstance(project)).getFindUsagesManager(); final UsageViewPresentation presentation = findUsagesManager.createPresentation(handler, options); presentation.setDetachedMode(true); UsageViewImpl usageView = (UsageViewImpl)manager.createUsageView(UsageTarget.EMPTY_ARRAY, Usage.EMPTY_ARRAY, presentation, null); if (editor != null) { PsiReference reference = TargetElementUtil.findReference(editor); if (reference != null) { UsageInfo2UsageAdapter origin = new UsageInfo2UsageAdapter(new UsageInfo(reference)); usageView.setOriginUsage(origin); } } Disposer.register(usageView, () -> { myUsageViewSettings.loadState(usageViewSettings); usageViewSettings.loadState(savedGlobalSettings); }); final MyTable table = new MyTable(); final AsyncProcessIcon processIcon = new AsyncProcessIcon("xxx"); addUsageNodes(usageView.getRoot(), usageView, new ArrayList<>()); final List<Usage> usages = new ArrayList<>(); final Set<UsageNode> visibleNodes = new LinkedHashSet<>(); final List<UsageNode> data = collectData(usages, visibleNodes, usageView, presentation); final AtomicInteger outOfScopeUsages = new AtomicInteger(); setTableModel(table, usageView, data, outOfScopeUsages, options.searchScope); boolean isPreviewMode = Boolean.TRUE == PreviewManager.SERVICE.preview(handler.getProject(), UsagesPreviewPanelProvider.ID, Pair.create(usageView, table), false); Runnable itemChosenCallback = prepareTable(table, editor, popupPosition, handler, maxUsages, options, isPreviewMode); @Nullable final JBPopup popup = isPreviewMode ? null : createUsagePopup(usages, visibleNodes, handler, editor, popupPosition, maxUsages, usageView, options, table, itemChosenCallback, presentation, processIcon); if (popup != null) { Disposer.register(popup, usageView); // show popup only if find usages takes more than 300ms, otherwise it would flicker needlessly Alarm alarm = new Alarm(usageView); alarm.addRequest(() -> showPopupIfNeedTo(popup, popupPosition), 300); } final PingEDT pingEDT = new PingEDT("Rebuild popup in EDT", o -> popup != null && popup.isDisposed(), 100, () -> { if (popup != null && popup.isDisposed()) return; final List<UsageNode> nodes = new ArrayList<>(); List<Usage> copy; synchronized (usages) { // open up popup as soon as several usages 've been found if (popup != null && !popup.isVisible() && (usages.size() <= 1 || !showPopupIfNeedTo(popup, popupPosition))) { return; } addUsageNodes(usageView.getRoot(), usageView, nodes); copy = new ArrayList<>(usages); } rebuildTable(usageView, copy, nodes, table, popup, presentation, popupPosition, !processIcon.isDisposed(), outOfScopeUsages, options.searchScope); }); final MessageBusConnection messageBusConnection = project.getMessageBus().connect(usageView); messageBusConnection.subscribe(UsageFilteringRuleProvider.RULES_CHANGED, pingEDT::ping); final UsageTarget[] myUsageTarget = {new PsiElement2UsageTargetAdapter(handler.getPsiElement())}; Processor<Usage> collect = usage -> { if (!UsageViewManagerImpl.isInScope(usage, options.searchScope)) { if (outOfScopeUsages.getAndIncrement() == 0) { visibleNodes.add(USAGES_OUTSIDE_SCOPE_NODE); usages.add(USAGES_OUTSIDE_SCOPE_SEPARATOR); } return true; } synchronized (usages) { if (visibleNodes.size() >= maxUsages) return false; if(UsageViewManager.isSelfUsage(usage, myUsageTarget)) return true; UsageNode node = ReadAction.compute(() -> usageView.doAppendUsage(usage)); usages.add(usage); if (node != null) { visibleNodes.add(node); boolean continueSearch = true; if (visibleNodes.size() == maxUsages) { visibleNodes.add(MORE_USAGES_SEPARATOR_NODE); usages.add(MORE_USAGES_SEPARATOR); continueSearch = false; } pingEDT.ping(); return continueSearch; } } return true; }; final ProgressIndicator indicator = FindUsagesManager.startProcessUsages(handler, handler.getPrimaryElements(), handler.getSecondaryElements(), collect, options, ()-> ApplicationManager.getApplication().invokeLater(() -> { Disposer.dispose(processIcon); Container parent = processIcon.getParent(); if (parent != null) { parent.remove(processIcon); parent.repaint(); } pingEDT.ping(); // repaint title synchronized (usages) { if (visibleNodes.isEmpty()) { if (usages.isEmpty()) { String text = UsageViewBundle.message("no.usages.found.in", searchScopePresentableName(options)); hint(editor, text, handler, popupPosition, maxUsages, options, false); cancel(popup); } // else all usages filtered out } else if (visibleNodes.size() == 1) { if (usages.size() == 1) { //the only usage Usage usage = visibleNodes.iterator().next().getUsage(); if (usage == USAGES_OUTSIDE_SCOPE_SEPARATOR) { hint(editor, UsageViewManagerImpl.outOfScopeMessage(outOfScopeUsages.get(), options.searchScope), handler, popupPosition, maxUsages, options, true); } else { String message = UsageViewBundle.message("show.usages.only.usage", searchScopePresentableName(options)); navigateAndHint(usage, message, handler, popupPosition, maxUsages, options); } cancel(popup); } else { assert usages.size() > 1 : usages; // usage view can filter usages down to one Usage visibleUsage = visibleNodes.iterator().next().getUsage(); if (areAllUsagesInOneLine(visibleUsage, usages)) { String hint = UsageViewBundle.message("all.usages.are.in.this.line", usages.size(), searchScopePresentableName(options)); navigateAndHint(visibleUsage, hint, handler, popupPosition, maxUsages, options); cancel(popup); } } } else { if (popup != null) { String title = presentation.getTabText(); boolean shouldShowMoreSeparator = visibleNodes.contains(MORE_USAGES_SEPARATOR_NODE); String fullTitle = getFullTitle(usages, title, shouldShowMoreSeparator, visibleNodes.size() - (shouldShowMoreSeparator ? 1 : 0), false); ((AbstractPopup)popup).setCaption(fullTitle); } } } }, project.getDisposed())); if (popup != null) { Disposer.register(popup, indicator::cancel); } } @NotNull private static UsageNode createStringNode(@NotNull final Object string) { return new StringNode(string); } private static class MyModel extends ListTableModel<UsageNode> implements ModelDiff.Model<Object> { private MyModel(@NotNull List<UsageNode> data, int cols) { super(cols(cols), data, 0); } @NotNull private static ColumnInfo[] cols(int cols) { ColumnInfo<UsageNode, UsageNode> o = new ColumnInfo<UsageNode, UsageNode>("") { @Nullable @Override public UsageNode valueOf(UsageNode node) { return node; } }; List<ColumnInfo<UsageNode, UsageNode>> list = Collections.nCopies(cols, o); return list.toArray(new ColumnInfo[list.size()]); } @Override public void addToModel(int idx, Object element) { UsageNode node = element instanceof UsageNode ? (UsageNode)element : createStringNode(element); if (idx < getRowCount()) { insertRow(idx, node); } else { addRow(node); } } @Override public void removeRangeFromModel(int start, int end) { for (int i=end; i>=start; i--) { removeRow(i); } } } private static boolean showPopupIfNeedTo(@NotNull JBPopup popup, @NotNull RelativePoint popupPosition) { if (!popup.isDisposed() && !popup.isVisible()) { popup.show(popupPosition); return true; } else { return false; } } @NotNull private JComponent createHintComponent(@NotNull String text, @NotNull final FindUsagesHandler handler, @NotNull final RelativePoint popupPosition, final Editor editor, @NotNull final Runnable cancelAction, final int maxUsages, @NotNull final FindUsagesOptions options, boolean isWarning) { JComponent label = HintUtil.createInformationLabel(suggestSecondInvocation(options, handler, text + " ")); if (isWarning) { label.setBackground(MessageType.WARNING.getPopupBackground()); } InplaceButton button = createSettingsButton(handler, popupPosition, editor, maxUsages, cancelAction); JPanel panel = new JPanel(new BorderLayout()) { @Override public void addNotify() { mySearchEverywhereRunnable = () -> searchEverywhere(options, handler, editor, popupPosition, maxUsages); super.addNotify(); } @Override public void removeNotify() { mySearchEverywhereRunnable = null; super.removeNotify(); } }; button.setBackground(label.getBackground()); panel.setBackground(label.getBackground()); label.setOpaque(false); label.setBorder(null); panel.setBorder(HintUtil.createHintBorder()); panel.add(label, BorderLayout.CENTER); panel.add(button, BorderLayout.EAST); return panel; } @NotNull private InplaceButton createSettingsButton(@NotNull final FindUsagesHandler handler, @NotNull final RelativePoint popupPosition, final Editor editor, final int maxUsages, @NotNull final Runnable cancelAction) { String shortcutText = ""; KeyboardShortcut shortcut = UsageViewImpl.getShowUsagesWithSettingsShortcut(); if (shortcut != null) { shortcutText = "(" + KeymapUtil.getShortcutText(shortcut) + ")"; } return new InplaceButton("Settings..." + shortcutText, AllIcons.General.Settings, e -> { ApplicationManager.getApplication().invokeLater(() -> showDialogAndFindUsages(handler, popupPosition, editor, maxUsages)); cancelAction.run(); }); } private void showDialogAndFindUsages(@NotNull FindUsagesHandler handler, @NotNull RelativePoint popupPosition, Editor editor, int maxUsages) { AbstractFindUsagesDialog dialog = handler.getFindUsagesDialog(false, false, false); if (dialog.showAndGet()) { dialog.calcFindUsagesOptions(); FindUsagesOptions options = handler.getFindUsagesOptions(DataManager.getInstance().getDataContext()); showElementUsages(editor, popupPosition, handler, maxUsages, options); } } @NotNull private static String searchScopePresentableName(@NotNull FindUsagesOptions options) { return options.searchScope.getDisplayName(); } @NotNull private Runnable prepareTable(final MyTable table, final Editor editor, final RelativePoint popupPosition, final FindUsagesHandler handler, final int maxUsages, @NotNull final FindUsagesOptions options, final boolean previewMode) { SpeedSearchBase<JTable> speedSearch = new MySpeedSearch(table); speedSearch.setComparator(new SpeedSearchComparator(false)); table.setRowHeight(PlatformIcons.CLASS_ICON.getIconHeight()+2); table.setShowGrid(false); table.setShowVerticalLines(false); table.setShowHorizontalLines(false); table.setTableHeader(null); table.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); table.setIntercellSpacing(new Dimension(0, 0)); final AtomicReference<List<Object>> selectedUsages = new AtomicReference<>(); final AtomicBoolean moreUsagesSelected = new AtomicBoolean(); final AtomicBoolean outsideScopeUsagesSelected = new AtomicBoolean(); table.getSelectionModel().addListSelectionListener(e -> { selectedUsages.set(null); outsideScopeUsagesSelected.set(false); moreUsagesSelected.set(false); List<Object> usages = null; for (int i : table.getSelectedRows()) { Object value = table.getValueAt(i, 0); if (value instanceof UsageNode) { Usage usage = ((UsageNode)value).getUsage(); if (usage == USAGES_OUTSIDE_SCOPE_SEPARATOR) { outsideScopeUsagesSelected.set(true); usages = null; break; } else if (usage == MORE_USAGES_SEPARATOR) { moreUsagesSelected.set(true); usages = null; break; } else { if (usages == null) usages = new ArrayList<>(); usages.add(usage instanceof UsageInfo2UsageAdapter ? ((UsageInfo2UsageAdapter)usage).getUsageInfo().copy() : usage); } } } selectedUsages.set(usages); }); final Runnable itemChosenCallback = () -> { if (moreUsagesSelected.get()) { appendMoreUsages(editor, popupPosition, handler, maxUsages, options); return; } if (outsideScopeUsagesSelected.get()) { options.searchScope = GlobalSearchScope.projectScope(handler.getProject()); showElementUsages(editor, popupPosition, handler, maxUsages, options); return; } List<Object> usages = selectedUsages.get(); if (usages != null) { for (Object usage : usages) { if (usage instanceof UsageInfo) { UsageViewUtil.navigateTo((UsageInfo)usage, true); } else if (usage instanceof Navigatable) { ((Navigatable)usage).navigate(true); } } } }; if (previewMode) { table.addMouseListener(new MouseAdapter() { @Override public void mouseReleased(MouseEvent e) { if (UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED) && !UIUtil.isSelectionButtonDown(e) && !e.isConsumed()) { itemChosenCallback.run(); } } }); table.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ENTER) { itemChosenCallback.run(); } } }); } return itemChosenCallback; } @NotNull private JBPopup createUsagePopup(@NotNull final List<Usage> usages, @NotNull Set<UsageNode> visibleNodes, @NotNull final FindUsagesHandler handler, final Editor editor, @NotNull final RelativePoint popupPosition, final int maxUsages, @NotNull final UsageViewImpl usageView, @NotNull final FindUsagesOptions options, @NotNull final JTable table, @NotNull final Runnable itemChoseCallback, @NotNull final UsageViewPresentation presentation, @NotNull final AsyncProcessIcon processIcon) { ApplicationManager.getApplication().assertIsDispatchThread(); PopupChooserBuilder builder = new PopupChooserBuilder(table); final String title = presentation.getTabText(); if (title != null) { String result = getFullTitle(usages, title, false, visibleNodes.size() - 1, true); builder.setTitle(result); builder.setAdText(getSecondInvocationTitle(options, handler)); } builder.setMovable(true).setResizable(true); builder.setMovable(true).setResizable(true); builder.setItemChoosenCallback(itemChoseCallback); final JBPopup[] popup = new JBPopup[1]; KeyboardShortcut shortcut = UsageViewImpl.getShowUsagesWithSettingsShortcut(); if (shortcut != null) { new DumbAwareAction() { @Override public void actionPerformed(@NotNull AnActionEvent e) { cancel(popup[0]); showDialogAndFindUsages(handler, popupPosition, editor, maxUsages); } @Override public boolean startInTransaction() { return true; } }.registerCustomShortcutSet(new CustomShortcutSet(shortcut.getFirstKeyStroke()), table); } shortcut = getShowUsagesShortcut(); if (shortcut != null) { new DumbAwareAction() { @Override public void actionPerformed(@NotNull AnActionEvent e) { cancel(popup[0]); searchEverywhere(options, handler, editor, popupPosition, maxUsages); } @Override public boolean startInTransaction() { return true; } }.registerCustomShortcutSet(new CustomShortcutSet(shortcut.getFirstKeyStroke()), table); } InplaceButton settingsButton = createSettingsButton(handler, popupPosition, editor, maxUsages, () -> cancel(popup[0])); ActiveComponent spinningProgress = new ActiveComponent.Adapter() { @Override public JComponent getComponent() { return processIcon; } }; final DefaultActionGroup pinGroup = new DefaultActionGroup(); final ActiveComponent pin = createPinButton(handler, usageView, options, popup, pinGroup); builder.setCommandButton(new CompositeActiveComponent(spinningProgress, settingsButton, pin)); DefaultActionGroup toolbar = new DefaultActionGroup(); usageView.addFilteringActions(toolbar); toolbar.add(UsageGroupingRuleProviderImpl.createGroupByFileStructureAction(usageView)); ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.USAGE_VIEW_TOOLBAR, toolbar, true); actionToolbar.setReservePlaceAutoPopupIcon(false); final JComponent toolBar = actionToolbar.getComponent(); toolBar.setOpaque(false); builder.setSettingButton(toolBar); builder.setCancelKeyEnabled(false); popup[0] = builder.createPopup(); JComponent content = popup[0].getContent(); myWidth = (int)(toolBar.getPreferredSize().getWidth() + new JLabel(getFullTitle(usages, title, false, visibleNodes.size() - 1, true)).getPreferredSize().getWidth() + settingsButton.getPreferredSize().getWidth()); myWidth = -1; for (AnAction action : toolbar.getChildren(null)) { action.unregisterCustomShortcutSet(usageView.getComponent()); action.registerCustomShortcutSet(action.getShortcutSet(), content); } for (AnAction action : pinGroup.getChildren(null)) { action.unregisterCustomShortcutSet(usageView.getComponent()); action.registerCustomShortcutSet(action.getShortcutSet(), content); } return popup[0]; } private ActiveComponent createPinButton(@NotNull final FindUsagesHandler handler, @NotNull final UsageViewImpl usageView, @NotNull final FindUsagesOptions options, @NotNull final JBPopup[] popup, @NotNull DefaultActionGroup pinGroup) { final AnAction pinAction = new AnAction("Open Find Usages Toolwindow", "Show all usages in a separate toolwindow", AllIcons.General.AutohideOff) { { AnAction action = ActionManager.getInstance().getAction(IdeActions.ACTION_FIND_USAGES); setShortcutSet(action.getShortcutSet()); } @Override public boolean startInTransaction() { return true; } @Override public void actionPerformed(@NotNull AnActionEvent e) { hideHints(); cancel(popup[0]); FindUsagesManager findUsagesManager = ((FindManagerImpl)FindManager.getInstance(usageView.getProject())).getFindUsagesManager(); findUsagesManager.findUsages(handler.getPrimaryElements(), handler.getSecondaryElements(), handler, options, FindSettings.getInstance().isSkipResultsWithOneUsage()); } }; pinGroup.add(pinAction); final ActionToolbar pinToolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.USAGE_VIEW_TOOLBAR, pinGroup, true); pinToolbar.setReservePlaceAutoPopupIcon(false); final JComponent pinToolBar = pinToolbar.getComponent(); pinToolBar.setBorder(null); pinToolBar.setOpaque(false); return new ActiveComponent.Adapter() { @Override public JComponent getComponent() { return pinToolBar; } }; } private static void cancel(@Nullable JBPopup popup) { if (popup != null) { popup.cancel(); } } @NotNull private static String getFullTitle(@NotNull List<Usage> usages, @NotNull String title, boolean hadMoreSeparator, int visibleNodesCount, boolean findUsagesInProgress) { String s; String soFarSuffix = findUsagesInProgress ? " so far" : ""; if (hadMoreSeparator) { s = "<b>Some</b> " + title + " " + "<b>(Only " + visibleNodesCount + " usages shown" + soFarSuffix + ")</b>"; } else { s = title + " (" + UsageViewBundle.message("usages.n", usages.size()) + soFarSuffix + ")"; } return "<html><nobr>" + s + "</nobr></html>"; } @NotNull private static String suggestSecondInvocation(@NotNull FindUsagesOptions options, @NotNull FindUsagesHandler handler, @NotNull String text) { final String title = getSecondInvocationTitle(options, handler); if (title != null) { text += "<br><small> " + title + "</small>"; } return XmlStringUtil.wrapInHtml(UIUtil.convertSpace2Nbsp(text)); } @Nullable private static String getSecondInvocationTitle(@NotNull FindUsagesOptions options, @NotNull FindUsagesHandler handler) { if (getShowUsagesShortcut() != null) { GlobalSearchScope maximalScope = FindUsagesManager.getMaximalScope(handler); if (!options.searchScope.equals(maximalScope)) { return "Press " + KeymapUtil.getShortcutText(getShowUsagesShortcut()) + " again to search in " + maximalScope.getDisplayName(); } } return null; } private void searchEverywhere(@NotNull FindUsagesOptions options, @NotNull FindUsagesHandler handler, Editor editor, @NotNull RelativePoint popupPosition, int maxUsages) { FindUsagesOptions cloned = options.clone(); cloned.searchScope = FindUsagesManager.getMaximalScope(handler); showElementUsages(editor, popupPosition, handler, maxUsages, cloned); } @Nullable private static KeyboardShortcut getShowUsagesShortcut() { return ActionManager.getInstance().getKeyboardShortcut(ID); } private static int filtered(@NotNull List<Usage> usages, @NotNull UsageViewImpl usageView) { return (int)usages.stream().filter(usage -> !usageView.isVisible(usage)).count(); } private static int getUsageOffset(@NotNull Usage usage) { if (!(usage instanceof UsageInfo2UsageAdapter)) return -1; PsiElement element = ((UsageInfo2UsageAdapter)usage).getElement(); if (element == null) return -1; return element.getTextRange().getStartOffset(); } private static boolean areAllUsagesInOneLine(@NotNull Usage visibleUsage, @NotNull List<Usage> usages) { Editor editor = getEditorFor(visibleUsage); if (editor == null) return false; int offset = getUsageOffset(visibleUsage); if (offset == -1) return false; int lineNumber = editor.getDocument().getLineNumber(offset); for (Usage other : usages) { Editor otherEditor = getEditorFor(other); if (otherEditor != editor) return false; int otherOffset = getUsageOffset(other); if (otherOffset == -1) return false; int otherLine = otherEditor.getDocument().getLineNumber(otherOffset); if (otherLine != lineNumber) return false; } return true; } @NotNull private static MyModel setTableModel(@NotNull JTable table, @NotNull UsageViewImpl usageView, @NotNull final List<UsageNode> data, @NotNull AtomicInteger outOfScopeUsages, @NotNull SearchScope searchScope) { ApplicationManager.getApplication().assertIsDispatchThread(); final int columnCount = calcColumnCount(data); MyModel model = table.getModel() instanceof MyModel ? (MyModel)table.getModel() : null; if (model == null || model.getColumnCount() != columnCount) { model = new MyModel(data, columnCount); table.setModel(model); ShowUsagesTableCellRenderer renderer = new ShowUsagesTableCellRenderer(usageView, outOfScopeUsages, searchScope); for (int i=0;i<table.getColumnModel().getColumnCount();i++) { TableColumn column = table.getColumnModel().getColumn(i); column.setPreferredWidth(0); column.setCellRenderer(renderer); } } return model; } private static int calcColumnCount(@NotNull List<UsageNode> data) { return data.isEmpty() || data.get(0) instanceof StringNode ? 1 : 3; } @NotNull private static List<UsageNode> collectData(@NotNull List<Usage> usages, @NotNull Collection<UsageNode> visibleNodes, @NotNull UsageViewImpl usageView, @NotNull UsageViewPresentation presentation) { @NotNull List<UsageNode> data = new ArrayList<>(); int filtered = filtered(usages, usageView); if (filtered != 0) { data.add(createStringNode(UsageViewBundle.message("usages.were.filtered.out", filtered))); } data.addAll(visibleNodes); if (data.isEmpty()) { String progressText = StringUtil.escapeXml(UsageViewManagerImpl.getProgressTitle(presentation)); data.add(createStringNode(progressText)); } Collections.sort(data, USAGE_NODE_COMPARATOR); return data; } private static int calcMaxWidth(JTable table) { int colsNum = table.getColumnModel().getColumnCount(); int totalWidth = 0; for (int col = 0; col < colsNum -1; col++) { TableColumn column = table.getColumnModel().getColumn(col); int preferred = column.getPreferredWidth(); int width = Math.max(preferred, columnMaxWidth(table, col)); totalWidth += width; column.setMinWidth(width); column.setMaxWidth(width); column.setWidth(width); column.setPreferredWidth(width); } totalWidth += columnMaxWidth(table, colsNum - 1); return totalWidth; } private static int columnMaxWidth(@NotNull JTable table, int col) { TableColumn column = table.getColumnModel().getColumn(col); int width = 0; for (int row = 0; row < table.getRowCount(); row++) { Component component = table.prepareRenderer(column.getCellRenderer(), row, col); int rendererWidth = component.getPreferredSize().width; width = Math.max(width, rendererWidth + table.getIntercellSpacing().width); } return width; } private int myWidth; private void rebuildTable(@NotNull final UsageViewImpl usageView, @NotNull final List<Usage> usages, @NotNull List<UsageNode> nodes, @NotNull final JTable table, @Nullable final JBPopup popup, @NotNull final UsageViewPresentation presentation, @NotNull final RelativePoint popupPosition, boolean findUsagesInProgress, @NotNull AtomicInteger outOfScopeUsages, @NotNull SearchScope searchScope) { ApplicationManager.getApplication().assertIsDispatchThread(); boolean shouldShowMoreSeparator = usages.contains(MORE_USAGES_SEPARATOR); if (shouldShowMoreSeparator) { nodes.add(MORE_USAGES_SEPARATOR_NODE); } boolean hasOutsideScopeUsages = usages.contains(USAGES_OUTSIDE_SCOPE_SEPARATOR); if (hasOutsideScopeUsages && !shouldShowMoreSeparator) { nodes.add(USAGES_OUTSIDE_SCOPE_NODE); } String title = presentation.getTabText(); String fullTitle = getFullTitle(usages, title, shouldShowMoreSeparator || hasOutsideScopeUsages, nodes.size() - (shouldShowMoreSeparator || hasOutsideScopeUsages ? 1 : 0), findUsagesInProgress); if (popup != null) { ((AbstractPopup)popup).setCaption(fullTitle); } List<UsageNode> data = collectData(usages, nodes, usageView, presentation); MyModel tableModel = setTableModel(table, usageView, data, outOfScopeUsages, searchScope); List<UsageNode> existingData = tableModel.getItems(); int row = table.getSelectedRow(); int newSelection = updateModel(tableModel, existingData, data, row == -1 ? 0 : row); if (newSelection < 0 || newSelection >= tableModel.getRowCount()) { ScrollingUtil.ensureSelectionExists(table); newSelection = table.getSelectedRow(); } else { // do not pre-select the usage under caret by default if (newSelection == 0 && table.getModel().getRowCount() > 1) { Object valueInTopRow = table.getModel().getValueAt(0, 0); if (valueInTopRow instanceof UsageNode && usageView.isOriginUsage(((UsageNode)valueInTopRow).getUsage())) { newSelection++; } } table.getSelectionModel().setSelectionInterval(newSelection, newSelection); } ScrollingUtil.ensureIndexIsVisible(table, newSelection, 0); if (popup != null) { setSizeAndDimensions(table, popup, popupPosition, data); } } // returns new selection private static int updateModel(@NotNull MyModel tableModel, @NotNull List<UsageNode> listOld, @NotNull List<UsageNode> listNew, int oldSelection) { UsageNode[] oa = listOld.toArray(new UsageNode[listOld.size()]); UsageNode[] na = listNew.toArray(new UsageNode[listNew.size()]); List<ModelDiff.Cmd> cmds = ModelDiff.createDiffCmds(tableModel, oa, na); int selection = oldSelection; if (cmds != null) { for (ModelDiff.Cmd cmd : cmds) { selection = cmd.translateSelection(selection); cmd.apply(); } } return selection; } private void setSizeAndDimensions(@NotNull JTable table, @NotNull JBPopup popup, @NotNull RelativePoint popupPosition, @NotNull List<UsageNode> data) { JComponent content = popup.getContent(); Window window = SwingUtilities.windowForComponent(content); Dimension d = window.getSize(); int width = calcMaxWidth(table); width = (int)Math.max(d.getWidth(), width); Dimension headerSize = ((AbstractPopup)popup).getHeaderPreferredSize(); width = Math.max((int)headerSize.getWidth(), width); width = Math.max(myWidth, width); if (myWidth == -1) myWidth = width; int newWidth = Math.max(width, d.width + width - myWidth); myWidth = newWidth; int rowsToShow = Math.min(30, data.size()); Dimension dimension = new Dimension(newWidth, table.getRowHeight() * rowsToShow); Rectangle rectangle = fitToScreen(dimension, popupPosition, table); if (!data.isEmpty()) { ScrollingUtil.ensureSelectionExists(table); } table.setSize(rectangle.getSize()); //table.setPreferredSize(dimension); //table.setMaximumSize(dimension); //table.setPreferredScrollableViewportSize(dimension); Dimension footerSize = ((AbstractPopup)popup).getFooterPreferredSize(); int footer = footerSize.height; int footerBorder = footer == 0 ? 0 : 1; Insets insets = ((AbstractPopup)popup).getPopupBorder().getBorderInsets(content); rectangle.height += headerSize.height + footer + footerBorder + insets.top + insets.bottom; ScreenUtil.fitToScreen(rectangle); Dimension newDim = rectangle.getSize(); window.setBounds(rectangle); window.setMinimumSize(newDim); window.setMaximumSize(newDim); window.validate(); window.repaint(); } private static Rectangle fitToScreen(@NotNull Dimension newDim, @NotNull RelativePoint popupPosition, JTable table) { Rectangle rectangle = new Rectangle(popupPosition.getScreenPoint(), newDim); ScreenUtil.fitToScreen(rectangle); if (rectangle.getHeight() != newDim.getHeight()) { int newHeight = (int)rectangle.getHeight(); int roundedHeight = newHeight - newHeight % table.getRowHeight(); rectangle.setSize((int)rectangle.getWidth(), Math.max(roundedHeight, table.getRowHeight())); } return rectangle; } private void appendMoreUsages(Editor editor, @NotNull RelativePoint popupPosition, @NotNull FindUsagesHandler handler, int maxUsages, @NotNull FindUsagesOptions options) { TransactionGuard.submitTransaction(handler.getProject(), () -> showElementUsages(editor, popupPosition, handler, maxUsages + USAGES_PAGE_SIZE, options)); } private static void addUsageNodes(@NotNull GroupNode root, @NotNull final UsageViewImpl usageView, @NotNull List<UsageNode> outNodes) { for (UsageNode node : root.getUsageNodes()) { Usage usage = node.getUsage(); if (usageView.isVisible(usage)) { node.setParent(root); outNodes.add(node); } } for (GroupNode groupNode : root.getSubGroups()) { groupNode.setParent(root); addUsageNodes(groupNode, usageView, outNodes); } } private void navigateAndHint(@NotNull Usage usage, @Nullable final String hint, @NotNull final FindUsagesHandler handler, @NotNull final RelativePoint popupPosition, final int maxUsages, @NotNull final FindUsagesOptions options) { usage.navigate(true); if (hint == null) return; final Editor newEditor = getEditorFor(usage); if (newEditor == null) return; hint(newEditor, hint, handler, popupPosition, maxUsages, options, false); } private void showHint(@Nullable final Editor editor, @NotNull String hint, @NotNull FindUsagesHandler handler, @NotNull final RelativePoint popupPosition, int maxUsages, @NotNull FindUsagesOptions options, boolean isWarning) { Runnable runnable = () -> { if (!handler.getPsiElement().isValid()) return; JComponent label = createHintComponent(hint, handler, popupPosition, editor, ShowUsagesAction::hideHints, maxUsages, options, isWarning); if (editor == null || editor.isDisposed() || !editor.getComponent().isShowing()) { HintManager.getInstance().showHint(label, popupPosition, HintManager.HIDE_BY_ANY_KEY | HintManager.HIDE_BY_TEXT_CHANGE | HintManager.HIDE_BY_SCROLLING, 0); } else { HintManager.getInstance().showInformationHint(editor, label); } }; if (editor == null) { runnable.run(); } else { AsyncEditorLoader.performWhenLoaded(editor, runnable); } } private void hint(@Nullable final Editor editor, @NotNull final String hint, @NotNull final FindUsagesHandler handler, @NotNull final RelativePoint popupPosition, final int maxUsages, @NotNull final FindUsagesOptions options, final boolean isWarning) { final Project project = handler.getProject(); //opening editor is performing in invokeLater IdeFocusManager.getInstance(project).doWhenFocusSettlesDown(() -> { Runnable runnable = () -> { // after new editor created, some editor resizing events are still bubbling. To prevent hiding hint, invokeLater this IdeFocusManager.getInstance(project).doWhenFocusSettlesDown( () -> showHint(editor, hint, handler, popupPosition, maxUsages, options, isWarning)); }; if (editor == null) { runnable.run(); } else { editor.getScrollingModel().runActionOnScrollingFinished(runnable); } }); } @Nullable private static Editor getEditorFor(@NotNull Usage usage) { FileEditorLocation location = usage.getLocation(); FileEditor newFileEditor = location == null ? null : location.getEditor(); return newFileEditor instanceof TextEditor ? ((TextEditor)newFileEditor).getEditor() : null; } private static class MyTable extends JBTable implements DataProvider { private static final int MARGIN = 2; public MyTable() { ScrollingUtil.installActions(this); HintUpdateSupply.installDataContextHintUpdateSupply(this); } @Override public boolean getScrollableTracksViewportWidth() { return true; } @Override public Object getData(@NonNls String dataId) { if (CommonDataKeys.PSI_ELEMENT.is(dataId)) { final int[] selected = getSelectedRows(); if (selected.length == 1) { return getPsiElementForHint(getValueAt(selected[0], 0)); } } else if (LangDataKeys.POSITION_ADJUSTER_POPUP.is(dataId)) { return PopupUtil.getPopupContainerFor(this); } return null; } @Override public int getRowHeight() { return super.getRowHeight() + 2 * MARGIN; } @NotNull @Override public Component prepareRenderer(@NotNull TableCellRenderer renderer, int row, int column) { Component component = super.prepareRenderer(renderer, row, column); if (component instanceof JComponent) { ((JComponent)component).setBorder(IdeBorderFactory.createEmptyBorder(MARGIN, MARGIN, MARGIN, 0)); } return component; } @Nullable private static PsiElement getPsiElementForHint(Object selectedValue) { if (selectedValue instanceof UsageNode) { final Usage usage = ((UsageNode)selectedValue).getUsage(); if (usage instanceof UsageInfo2UsageAdapter) { final PsiElement element = ((UsageInfo2UsageAdapter)usage).getElement(); if (element != null) { final PsiElement view = UsageToPsiElementProvider.findAppropriateParentFrom(element); return view == null ? element : view; } } } return null; } } static class StringNode extends UsageNode { @NotNull private final Object myString; StringNode(@NotNull Object string) { super(null, NullUsage.INSTANCE); myString = string; } @Override public String toString() { return myString.toString(); } } private static class MySpeedSearch extends SpeedSearchBase<JTable> { MySpeedSearch(@NotNull MyTable table) { super(table); } @Override protected int getSelectedIndex() { return getTable().getSelectedRow(); } @Override protected int convertIndexToModel(int viewIndex) { return getTable().convertRowIndexToModel(viewIndex); } @NotNull @Override protected Object[] getAllElements() { return ((MyModel)getTable().getModel()).getItems().toArray(); } @Override protected String getElementText(@NotNull Object element) { if (!(element instanceof UsageNode)) return element.toString(); UsageNode node = (UsageNode)element; if (node instanceof StringNode) return ""; Usage usage = node.getUsage(); if (usage == MORE_USAGES_SEPARATOR || usage == USAGES_OUTSIDE_SCOPE_SEPARATOR) return ""; GroupNode group = (GroupNode)node.getParent(); String groupText = group == null ? "" : group.getGroup().getText(null); return groupText + usage.getPresentation().getPlainText(); } @Override protected void selectElement(Object element, String selectedText) { List<UsageNode> data = ((MyModel)getTable().getModel()).getItems(); int i = data.indexOf(element); if (i == -1) return; final int viewRow = getTable().convertRowIndexToView(i); getTable().getSelectionModel().setSelectionInterval(viewRow, viewRow); TableUtil.scrollSelectionToVisible(getTable()); } private MyTable getTable() { return (MyTable)myComponent; } } }