blob: 0d49a89d9c436404c4bb7f8bdaf835bced0231bf [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.codeInsight.navigation;
import com.intellij.codeInsight.CodeInsightActionHandler;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.find.FindUtil;
import com.intellij.ide.util.EditSourceUtil;
import com.intellij.ide.util.PsiElementListCellRenderer;
import com.intellij.navigation.ItemPresentation;
import com.intellij.navigation.NavigationItem;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.PopupChooserBuilder;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Ref;
import com.intellij.pom.Navigatable;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiNamedElement;
import com.intellij.ui.CollectionListModel;
import com.intellij.ui.JBListWithHintProvider;
import com.intellij.ui.popup.AbstractPopup;
import com.intellij.ui.popup.HintUpdateSupply;
import com.intellij.usages.UsageView;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Function;
import com.intellij.util.Processor;
import com.intellij.util.containers.HashSet;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.util.*;
import java.util.List;
public abstract class GotoTargetHandler implements CodeInsightActionHandler {
private static final PsiElementListCellRenderer ourDefaultTargetElementRenderer = new DefaultPsiElementListCellRenderer();
private final DefaultListCellRenderer myActionElementRenderer = new ActionCellRenderer();
@Override
public boolean startInWriteAction() {
return false;
}
@Override
public void invoke(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
FeatureUsageTracker.getInstance().triggerFeatureUsed(getFeatureUsedKey());
try {
GotoData gotoData = getSourceAndTargetElements(editor, file);
if (gotoData != null && gotoData.source != null) {
show(project, editor, file, gotoData);
}
}
catch (IndexNotReadyException e) {
DumbService.getInstance(project).showDumbModeNotification("Navigation is not available here during index update");
}
}
@NonNls
protected abstract String getFeatureUsedKey();
@Nullable
protected abstract GotoData getSourceAndTargetElements(Editor editor, PsiFile file);
private void show(@NotNull final Project project,
@NotNull Editor editor,
@NotNull PsiFile file,
@NotNull final GotoData gotoData) {
final PsiElement[] targets = gotoData.targets;
final List<AdditionalAction> additionalActions = gotoData.additionalActions;
if (targets.length == 0 && additionalActions.isEmpty()) {
HintManager.getInstance().showErrorHint(editor, getNotFoundMessage(project, editor, file));
return;
}
if (targets.length == 1 && additionalActions.isEmpty()) {
Navigatable descriptor = targets[0] instanceof Navigatable ? (Navigatable)targets[0] : EditSourceUtil.getDescriptor(targets[0]);
if (descriptor != null && descriptor.canNavigate()) {
navigateToElement(descriptor);
}
return;
}
for (PsiElement eachTarget : targets) {
gotoData.renderers.put(eachTarget, createRenderer(gotoData, eachTarget));
}
final String name = ((PsiNamedElement)gotoData.source).getName();
final String title = getChooserTitle(gotoData.source, name, targets.length);
if (shouldSortTargets()) {
Arrays.sort(targets, createComparator(gotoData.renderers, gotoData));
}
List<Object> allElements = new ArrayList<Object>(targets.length + additionalActions.size());
Collections.addAll(allElements, targets);
allElements.addAll(additionalActions);
final JBListWithHintProvider list = new JBListWithHintProvider(new CollectionListModel<Object>(allElements)) {
@Override
protected PsiElement getPsiElementForHint(final Object selectedValue) {
return selectedValue instanceof PsiElement ? (PsiElement) selectedValue : null;
}
};
list.setCellRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
if (value == null) return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if (value instanceof AdditionalAction) {
return myActionElementRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
}
PsiElementListCellRenderer renderer = getRenderer(value, gotoData.renderers, gotoData);
return renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
}
});
final Runnable runnable = new Runnable() {
@Override
public void run() {
int[] ids = list.getSelectedIndices();
if (ids == null || ids.length == 0) return;
Object[] selectedElements = list.getSelectedValues();
for (Object element : selectedElements) {
if (element instanceof AdditionalAction) {
((AdditionalAction)element).execute();
}
else {
Navigatable nav = element instanceof Navigatable ? (Navigatable)element : EditSourceUtil.getDescriptor((PsiElement)element);
if (nav != null && nav.canNavigate()) {
navigateToElement(nav);
}
}
}
}
};
final PopupChooserBuilder builder = new PopupChooserBuilder(list);
builder.setFilteringEnabled(new Function<Object, String>() {
@Override
public String fun(Object o) {
if (o instanceof AdditionalAction) {
return ((AdditionalAction)o).getText();
}
return getRenderer(o, gotoData.renderers, gotoData).getElementText((PsiElement)o);
}
});
final Ref<UsageView> usageView = new Ref<UsageView>();
final JBPopup popup = builder.
setTitle(title).
setItemChoosenCallback(runnable).
setMovable(true).
setCancelCallback(new Computable<Boolean>() {
@Override
public Boolean compute() {
HintUpdateSupply.hideHint(list);
return true;
}
}).
setCouldPin(new Processor<JBPopup>() {
@Override
public boolean process(JBPopup popup) {
usageView.set(FindUtil.showInUsageView(gotoData.source, gotoData.targets, getFindUsagesTitle(gotoData.source, name, gotoData.targets.length), project));
popup.cancel();
return false;
}
}).
setAdText(getAdText(gotoData.source, targets.length)).
createPopup();
if (gotoData.listUpdaterTask != null) {
gotoData.listUpdaterTask.init((AbstractPopup)popup, list, usageView);
ProgressManager.getInstance().run(gotoData.listUpdaterTask);
}
popup.showInBestPositionFor(editor);
}
private static PsiElementListCellRenderer getRenderer(Object value,
Map<Object, PsiElementListCellRenderer> targetsWithRenderers,
GotoData gotoData) {
PsiElementListCellRenderer renderer = targetsWithRenderers.get(value);
if (renderer == null) {
renderer = gotoData.getRenderer(value);
}
if (renderer != null) {
return renderer;
}
else {
return ourDefaultTargetElementRenderer;
}
}
protected static Comparator<PsiElement> createComparator(final Map<Object, PsiElementListCellRenderer> targetsWithRenderers,
final GotoData gotoData) {
return new Comparator<PsiElement>() {
@Override
public int compare(PsiElement o1, PsiElement o2) {
return getComparingObject(o1).compareTo(getComparingObject(o2));
}
private Comparable getComparingObject(PsiElement o1) {
return getRenderer(o1, targetsWithRenderers, gotoData).getComparingObject(o1);
}
};
}
protected static PsiElementListCellRenderer createRenderer(GotoData gotoData, PsiElement eachTarget) {
PsiElementListCellRenderer renderer = null;
for (GotoTargetRendererProvider eachProvider : Extensions.getExtensions(GotoTargetRendererProvider.EP_NAME)) {
renderer = eachProvider.getRenderer(eachTarget, gotoData);
if (renderer != null) break;
}
if (renderer == null) {
renderer = ourDefaultTargetElementRenderer;
}
return renderer;
}
protected void navigateToElement(Navigatable descriptor) {
descriptor.navigate(true);
}
protected boolean shouldSortTargets() {
return true;
}
@NotNull
protected abstract String getChooserTitle(PsiElement sourceElement, String name, int length);
@NotNull
protected String getFindUsagesTitle(PsiElement sourceElement, String name, int length) {
return getChooserTitle(sourceElement, name, length);
}
@NotNull
protected abstract String getNotFoundMessage(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file);
@Nullable
protected String getAdText(PsiElement source, int length) {
return null;
}
public interface AdditionalAction {
@NotNull
String getText();
Icon getIcon();
void execute();
}
public static class GotoData {
@NotNull public final PsiElement source;
public PsiElement[] targets;
public final List<AdditionalAction> additionalActions;
private boolean hasDifferentNames;
public ListBackgroundUpdaterTask listUpdaterTask;
protected final Set<String> myNames;
public Map<Object, PsiElementListCellRenderer> renderers = new HashMap<Object, PsiElementListCellRenderer>();
public GotoData(@NotNull PsiElement source, @NotNull PsiElement[] targets, @NotNull List<AdditionalAction> additionalActions) {
this.source = source;
this.targets = targets;
this.additionalActions = additionalActions;
myNames = new HashSet<String>();
for (PsiElement target : targets) {
if (target instanceof PsiNamedElement) {
myNames.add(((PsiNamedElement)target).getName());
if (myNames.size() > 1) break;
}
}
hasDifferentNames = myNames.size() > 1;
}
public boolean hasDifferentNames() {
return hasDifferentNames;
}
public boolean addTarget(final PsiElement element) {
if (ArrayUtil.find(targets, element) > -1) return false;
targets = ArrayUtil.append(targets, element);
renderers.put(element, createRenderer(this, element));
if (!hasDifferentNames && element instanceof PsiNamedElement) {
final String name = ApplicationManager.getApplication().runReadAction(new Computable<String>() {
@Override
public String compute() {
return ((PsiNamedElement)element).getName();
}
});
myNames.add(name);
hasDifferentNames = myNames.size() > 1;
}
return true;
}
public PsiElementListCellRenderer getRenderer(Object value) {
return renderers.get(value);
}
}
private static class DefaultPsiElementListCellRenderer extends PsiElementListCellRenderer {
@Override
public String getElementText(final PsiElement element) {
if (element instanceof PsiNamedElement) {
String name = ((PsiNamedElement)element).getName();
if (name != null) {
return name;
}
}
return element.getContainingFile().getName();
}
@Override
protected String getContainerText(final PsiElement element, final String name) {
if (element instanceof NavigationItem) {
final ItemPresentation presentation = ((NavigationItem)element).getPresentation();
return presentation != null ? presentation.getLocationString():null;
}
return null;
}
@Override
protected int getIconFlags() {
return 0;
}
}
private static class ActionCellRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
Component result = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if (value != null) {
AdditionalAction action = (AdditionalAction)value;
setText(action.getText());
setIcon(action.getIcon());
}
return result;
}
}
}