blob: 098a3e7f9377c79a5c1606869ece5db8e7405398 [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.codeHighlighting.Pass;
import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo;
import com.intellij.ide.util.DefaultPsiElementCellRenderer;
import com.intellij.ide.util.PsiElementListCellRenderer;
import com.intellij.lang.annotation.Annotation;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.navigation.GotoRelatedItem;
import com.intellij.openapi.editor.markup.GutterIconRenderer;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Factory;
import com.intellij.openapi.util.NotNullLazyValue;
import com.intellij.openapi.util.Ref;
import com.intellij.psi.PsiElement;
import com.intellij.psi.SmartPointerManager;
import com.intellij.psi.SmartPsiElementPointer;
import com.intellij.util.ConstantFunction;
import com.intellij.util.NotNullFunction;
import com.intellij.util.NullableFunction;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.xml.DomElement;
import com.intellij.util.xml.ElementPresentationManager;
import com.intellij.util.xml.highlighting.DomElementAnnotationHolder;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.text.MessageFormat;
import java.util.*;
/**
* DOM-specific builder for {@link com.intellij.openapi.editor.markup.GutterIconRenderer}
* and {@link com.intellij.codeInsight.daemon.LineMarkerInfo}.
*
* @author peter
*/
public class NavigationGutterIconBuilder<T> {
@NonNls private static final String PATTERN = "&nbsp;&nbsp;&nbsp;&nbsp;{0}";
private static final NotNullFunction<PsiElement,Collection<? extends PsiElement>> DEFAULT_PSI_CONVERTOR = new NotNullFunction<PsiElement, Collection<? extends PsiElement>>() {
@Override
@NotNull
public Collection<? extends PsiElement> fun(final PsiElement element) {
return ContainerUtil.createMaybeSingletonList(element);
}
};
private final Icon myIcon;
private final NotNullFunction<T,Collection<? extends PsiElement>> myConverter;
private NotNullLazyValue<Collection<T>> myTargets;
private boolean myLazy;
private String myTooltipText;
private String myPopupTitle;
private String myEmptyText;
private String myTooltipTitle;
private GutterIconRenderer.Alignment myAlignment = GutterIconRenderer.Alignment.CENTER;
private Computable<PsiElementListCellRenderer> myCellRenderer;
private NullableFunction<T,String> myNamer = ElementPresentationManager.namer();
private final NotNullFunction<T, Collection<? extends GotoRelatedItem>> myGotoRelatedItemProvider;
public static final NotNullFunction<DomElement,Collection<? extends PsiElement>> DEFAULT_DOM_CONVERTOR = new NotNullFunction<DomElement, Collection<? extends PsiElement>>() {
@Override
@NotNull
public Collection<? extends PsiElement> fun(final DomElement o) {
return ContainerUtil.createMaybeSingletonList(o.getXmlElement());
}
};
public static final NotNullFunction<DomElement, Collection<? extends GotoRelatedItem>> DOM_GOTO_RELATED_ITEM_PROVIDER = new NotNullFunction<DomElement, Collection<? extends GotoRelatedItem>>() {
@NotNull
@Override
public Collection<? extends GotoRelatedItem> fun(DomElement dom) {
if (dom.getXmlElement() != null) {
return Collections.singletonList(new DomGotoRelatedItem(dom));
}
return Collections.emptyList();
}
};
public static final NotNullFunction<PsiElement, Collection<? extends GotoRelatedItem>> PSI_GOTO_RELATED_ITEM_PROVIDER = new NotNullFunction<PsiElement, Collection<? extends GotoRelatedItem>>() {
@NotNull
@Override
public Collection<? extends GotoRelatedItem> fun(PsiElement dom) {
return Collections.singletonList(new GotoRelatedItem(dom, "XML"));
}
};
protected NavigationGutterIconBuilder(@NotNull final Icon icon, @NotNull NotNullFunction<T, Collection<? extends PsiElement>> converter) {
this(icon, converter, null);
}
protected NavigationGutterIconBuilder(@NotNull final Icon icon,
@NotNull NotNullFunction<T, Collection<? extends PsiElement>> converter,
final @Nullable NotNullFunction<T, Collection<? extends GotoRelatedItem>> gotoRelatedItemProvider) {
myIcon = icon;
myConverter = converter;
myGotoRelatedItemProvider = gotoRelatedItemProvider;
}
public static NavigationGutterIconBuilder<PsiElement> create(@NotNull final Icon icon) {
return create(icon, DEFAULT_PSI_CONVERTOR, PSI_GOTO_RELATED_ITEM_PROVIDER);
}
public static <T> NavigationGutterIconBuilder<T> create(@NotNull final Icon icon,
@NotNull NotNullFunction<T, Collection<? extends PsiElement>> converter) {
return create(icon, converter, null);
}
public static <T> NavigationGutterIconBuilder<T> create(@NotNull final Icon icon,
@NotNull NotNullFunction<T, Collection<? extends PsiElement>> converter,
final @Nullable NotNullFunction<T, Collection<? extends GotoRelatedItem>> gotoRelatedItemProvider) {
return new NavigationGutterIconBuilder<T>(icon, converter, gotoRelatedItemProvider);
}
public NavigationGutterIconBuilder<T> setTarget(@Nullable T target) {
return setTargets(ContainerUtil.createMaybeSingletonList(target));
}
public NavigationGutterIconBuilder<T> setTargets(@NotNull T... targets) {
return setTargets(Arrays.asList(targets));
}
public NavigationGutterIconBuilder<T> setTargets(@NotNull final NotNullLazyValue<Collection<? extends T>> targets) {
//noinspection unchecked
myTargets = (NotNullLazyValue)targets;
myLazy = true;
return this;
}
public NavigationGutterIconBuilder<T> setTargets(@NotNull final Collection<? extends T> targets) {
//noinspection unchecked
myTargets = NotNullLazyValue.createConstantValue((Collection<T>)targets);
return this;
}
public NavigationGutterIconBuilder<T> setTooltipText(@NotNull String tooltipText) {
myTooltipText = tooltipText;
return this;
}
public NavigationGutterIconBuilder<T> setAlignment(@NotNull final GutterIconRenderer.Alignment alignment) {
myAlignment = alignment;
return this;
}
public NavigationGutterIconBuilder<T> setPopupTitle(@NotNull String popupTitle) {
myPopupTitle = popupTitle;
return this;
}
public NavigationGutterIconBuilder<T> setEmptyPopupText(@NotNull String emptyText) {
myEmptyText = emptyText;
return this;
}
public NavigationGutterIconBuilder<T> setTooltipTitle(@NotNull final String tooltipTitle) {
myTooltipTitle = tooltipTitle;
return this;
}
public NavigationGutterIconBuilder<T> setNamer(@NotNull NullableFunction<T,String> namer) {
myNamer = namer;
return this;
}
public NavigationGutterIconBuilder<T> setCellRenderer(@NotNull final PsiElementListCellRenderer cellRenderer) {
myCellRenderer = new Computable.PredefinedValueComputable<PsiElementListCellRenderer>(cellRenderer);
return this;
}
@Nullable
public Annotation install(@NotNull DomElementAnnotationHolder holder, @Nullable DomElement element) {
if (!myLazy && myTargets.getValue().isEmpty() || element == null) return null;
return doInstall(holder.createAnnotation(element, HighlightSeverity.INFORMATION, null), element.getManager().getProject());
}
@Nullable
public Annotation install(@NotNull AnnotationHolder holder, @Nullable PsiElement element) {
if (!myLazy && myTargets.getValue().isEmpty() || element == null) return null;
return doInstall(holder.createInfoAnnotation(element, null), element.getProject());
}
private Annotation doInstall(@NotNull Annotation annotation, @NotNull Project project) {
final MyNavigationGutterIconRenderer renderer = createGutterIconRenderer(project);
annotation.setGutterIconRenderer(renderer);
annotation.setNeedsUpdateOnTyping(false);
return annotation;
}
public RelatedItemLineMarkerInfo<PsiElement> createLineMarkerInfo(@NotNull PsiElement element) {
final MyNavigationGutterIconRenderer renderer = createGutterIconRenderer(element.getProject());
final String tooltip = renderer.getTooltipText();
NotNullLazyValue<Collection<? extends GotoRelatedItem>> gotoTargets = createGotoTargetsThunk(myLazy, myGotoRelatedItemProvider,
evaluateAndForget(myTargets));
return new RelatedItemLineMarkerInfo<PsiElement>(element, element.getTextRange(), renderer.getIcon(), Pass.UPDATE_OVERRIDEN_MARKERS,
tooltip == null ? null : new ConstantFunction<PsiElement, String>(tooltip),
renderer.isNavigateAction() ? renderer : null, renderer.getAlignment(),
gotoTargets);
}
private static <T> NotNullLazyValue<Collection<? extends GotoRelatedItem>> createGotoTargetsThunk(boolean lazy,
final NotNullFunction<T, Collection<? extends GotoRelatedItem>> gotoRelatedItemProvider,
final Factory<Collection<T>> factory) {
if (gotoRelatedItemProvider == null) {
return NotNullLazyValue.<Collection<? extends GotoRelatedItem>>createConstantValue(Collections.<GotoRelatedItem>emptyList());
}
if (lazy) {
return new NotNullLazyValue<Collection<? extends GotoRelatedItem>>() {
@NotNull
@Override
protected Collection<? extends GotoRelatedItem> compute() {
return ContainerUtil.concat(factory.create(), gotoRelatedItemProvider);
}
};
}
Collection<GotoRelatedItem> concat = ContainerUtil.concat(factory.create(), gotoRelatedItemProvider);
return NotNullLazyValue.<Collection<? extends GotoRelatedItem>>createConstantValue(concat);
}
private void checkBuilt() {
assert myTargets != null : "Must have called .setTargets() before calling create()";
}
private static <T> Factory<T> evaluateAndForget(NotNullLazyValue<T> lazyValue) {
final Ref<NotNullLazyValue<T>> ref = Ref.create(lazyValue);
return new Factory<T>() {
@Override
public T create() {
T result = ref.get().getValue();
ref.set(null);
return result;
}
};
}
private MyNavigationGutterIconRenderer createGutterIconRenderer(@NotNull final Project project) {
checkBuilt();
NotNullLazyValue<List<SmartPsiElementPointer>> pointers = createPointersThunk(myLazy, project, evaluateAndForget(myTargets),
myConverter);
final boolean empty = isEmpty();
if (myTooltipText == null && !myLazy) {
final SortedSet<String> names = new TreeSet<String>();
for (T t : myTargets.getValue()) {
final String text = myNamer.fun(t);
if (text != null) {
names.add(MessageFormat.format(PATTERN, text));
}
}
@NonNls StringBuilder sb = new StringBuilder("<html><body>");
if (myTooltipTitle != null) {
sb.append(myTooltipTitle).append("<br>");
}
for (String name : names) {
sb.append(name).append("<br>");
}
sb.append("</body></html>");
myTooltipText = sb.toString();
}
Computable<PsiElementListCellRenderer> renderer =
myCellRenderer == null ? new Computable<PsiElementListCellRenderer>() {
@Override
public PsiElementListCellRenderer compute() {
return new DefaultPsiElementCellRenderer();
}
} : myCellRenderer;
return new MyNavigationGutterIconRenderer(this, myAlignment, myIcon, myTooltipText, pointers, renderer, empty);
}
private static <T> NotNullLazyValue<List<SmartPsiElementPointer>> createPointersThunk(boolean lazy,
final Project project,
final Factory<Collection<T>> targets,
final NotNullFunction<T, Collection<? extends PsiElement>> converter) {
if (!lazy) {
return NotNullLazyValue.createConstantValue(calcPsiTargets(project, targets.create(), converter));
}
return new NotNullLazyValue<List<SmartPsiElementPointer>>() {
@Override
@NotNull
public List<SmartPsiElementPointer> compute() {
return calcPsiTargets(project, targets.create(), converter);
}
};
}
private static <T> List<SmartPsiElementPointer> calcPsiTargets(Project project, Collection<? extends T> targets,
NotNullFunction<T, Collection<? extends PsiElement>> converter) {
SmartPointerManager manager = SmartPointerManager.getInstance(project);
Set<PsiElement> elements = new THashSet<PsiElement>();
final List<SmartPsiElementPointer> list = new ArrayList<SmartPsiElementPointer>(targets.size());
for (final T target : targets) {
for (final PsiElement psiElement : converter.fun(target)) {
if (elements.add(psiElement) && psiElement.isValid()) {
list.add(manager.createSmartPsiElementPointer(psiElement));
}
}
}
return list;
}
private boolean isEmpty() {
if (myLazy) {
return false;
}
Set<PsiElement> elements = new THashSet<PsiElement>();
Collection<? extends T> targets = myTargets.getValue();
for (final T target : targets) {
for (final PsiElement psiElement : myConverter.fun(target)) {
if (elements.add(psiElement)) {
return false;
}
}
}
return true;
}
private static class MyNavigationGutterIconRenderer extends NavigationGutterIconRenderer {
private final Alignment myAlignment;
private final Icon myIcon;
private final String myTooltipText;
private final boolean myEmpty;
public MyNavigationGutterIconRenderer(@NotNull NavigationGutterIconBuilder builder,
final Alignment alignment,
final Icon icon,
@Nullable final String tooltipText,
@NotNull NotNullLazyValue<List<SmartPsiElementPointer>> pointers,
Computable<PsiElementListCellRenderer> cellRenderer,
boolean empty) {
super(builder.myPopupTitle, builder.myEmptyText, cellRenderer, pointers);
myAlignment = alignment;
myIcon = icon;
myTooltipText = tooltipText;
myEmpty = empty;
}
@Override
public boolean isNavigateAction() {
return !myEmpty;
}
@Override
@NotNull
public Icon getIcon() {
return myIcon;
}
@Override
@Nullable
public String getTooltipText() {
return myTooltipText;
}
@Override
public Alignment getAlignment() {
return myAlignment;
}
public boolean equals(final Object o) {
if (this == o) return true;
if (!super.equals(o)) return false;
final MyNavigationGutterIconRenderer that = (MyNavigationGutterIconRenderer)o;
if (myAlignment != that.myAlignment) return false;
if (myIcon != null ? !myIcon.equals(that.myIcon) : that.myIcon != null) return false;
if (myTooltipText != null ? !myTooltipText.equals(that.myTooltipText) : that.myTooltipText != null) return false;
return true;
}
public int hashCode() {
int result = super.hashCode();
result = 31 * result + (myAlignment != null ? myAlignment.hashCode() : 0);
result = 31 * result + (myIcon != null ? myIcon.hashCode() : 0);
result = 31 * result + (myTooltipText != null ? myTooltipText.hashCode() : 0);
return result;
}
}
}