IDEA-189709: implement/override tooltip for gutter icons

This commit is contained in:
Sergey Malenkov
2018-10-23 00:47:08 +03:00
parent 9d7de4ae6c
commit d4e87d1ded
6 changed files with 286 additions and 47 deletions
@@ -36,14 +36,26 @@ public class GutterIconTooltipHelper {
private GutterIconTooltipHelper() {
}
/**
* @deprecated by com.intellij.codeInsight.daemon.impl.GutterTooltipHelper
*/
@Deprecated
public static String composeText(@NotNull PsiElement[] elements, @NotNull String start, @NotNull String pattern) {
return composeText(Arrays.asList(elements), start, pattern);
}
/**
* @deprecated by com.intellij.codeInsight.daemon.impl.GutterTooltipHelper
*/
@Deprecated
public static String composeText(@NotNull Iterable<? extends PsiElement> elements, @NotNull String start, @NotNull String pattern) {
return composeText(elements, start, pattern, "");
}
/**
* @deprecated by com.intellij.codeInsight.daemon.impl.GutterTooltipHelper
*/
@Deprecated
static String composeText(@NotNull Iterable<? extends PsiElement> elements,
@NotNull String start,
@NotNull String pattern,
@@ -0,0 +1,190 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.daemon.impl;
import com.intellij.ide.actions.QualifiedNameProvider;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.ui.JBColor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.function.Function;
import java.util.function.Predicate;
import static com.intellij.openapi.keymap.KeymapUtil.getPreferredShortcutText;
import static com.intellij.openapi.util.io.FileUtil.toSystemIndependentName;
import static com.intellij.psi.util.PsiTreeUtil.getStubOrPsiParentOfType;
import static com.intellij.psi.util.PsiUtilCore.getVirtualFile;
import static com.intellij.ui.ColorUtil.toHex;
public final class GutterTooltipHelper {
private static final JBColor CONTEXT_HELP_FOREGROUND
= JBColor.namedColor("GutterTooltip.ContextHelp.foreground", new JBColor(0x787878, 0x878787));
private GutterTooltipHelper() {
}
/**
* @param elements a collection of elements to create a formatted tooltip text
* @param prefix a text to insert before all elements
* @param skipFirstMember {@code true} to skip a method (or field) name in the link to element
* @param actionId an action identifier to generate context help or {@code null} if not applicable
*/
@NotNull
public static <E extends PsiElement> String getTooltipText(@NotNull Collection<E> elements,
@NotNull String prefix,
boolean skipFirstMember,
@Nullable String actionId) {
String elementPrefix = 1 < elements.size() ? "<br>&nbsp;&nbsp;&nbsp;&nbsp; " : " ";
return getTooltipText(prefix, elements, e -> elementPrefix, e -> skipFirstMember, actionId);
}
/**
* @param elements a collection of elements to create a formatted tooltip text
* @param function a function that returns a text to insert before the current element
* @param predicate a function that returns {@code true} to skip a method (or field) name for the current element
* @param actionId an action identifier to generate context help or {@code null} if not applicable
*/
@NotNull
public static <E extends PsiElement> String getTooltipText(@NotNull Collection<E> elements,
@NotNull Function<E, String> function,
@NotNull Predicate<E> predicate,
@Nullable String actionId) {
return getTooltipText(null, elements, function, predicate, actionId);
}
@NotNull
private static <E extends PsiElement> String getTooltipText(@Nullable String prefix,
@NotNull Collection<E> elements,
@NotNull Function<E, String> function,
@NotNull Predicate<E> predicate,
@Nullable String actionId) {
StringBuilder sb = new StringBuilder();
if (prefix != null) sb.append(prefix);
for (E element : elements) {
String elementPrefix = function.apply(element);
if (elementPrefix != null) sb.append(elementPrefix);
appendElement(sb, element, predicate.test(element));
}
appendContextHelp(sb, actionId);
return sb.toString();
}
private static void appendElement(@NotNull StringBuilder sb, @NotNull PsiElement element, boolean skip) {
boolean useSingleLink = Registry.is("gutter.tooltip.single.link");
String packageName = null;
boolean addedSingleLink = useSingleLink && appendLink(sb, element);
PsiElement skipped = null;
if (skip && (element instanceof PsiMethod || element instanceof PsiField)) {
skipped = element; // use skipped member as first separate link
element = getContainingElement(element);
}
while (element != null) {
String name = getPresentableName(element);
if (name != null) {
boolean addedLink = !useSingleLink && appendLink(sb, skipped != null ? skipped : element);
// Swing uses simple HTML processing and paints a link incorrectly if it contains different fonts.
// This is the reason why I use monospaced font not only for element name, but for a whole link.
// By the same reason I have to comment out support for deprecated elements.
//
// boolean deprecated = RefJavaUtil.isDeprecated(element);
// if (deprecated) sb.append("<strike>");
// sb.append("<code>");
sb.append(name);
// sb.append("</code>");
// if (deprecated) sb.append("</strike>");
if (addedLink) sb.append("</code></a>");
}
if (element instanceof PsiFile) break;
PsiElement parent = getContainingElement(element);
if (parent == null || parent instanceof PsiFile) {
if (element instanceof PsiClass && !(element instanceof PsiAnonymousClass)) {
String qualifiedName = ((PsiClass)element).getQualifiedName();
if (qualifiedName != null) packageName = StringUtil.getPackageName(qualifiedName);
break;
}
}
if (parent != null) sb.append(" in ");
element = parent;
skipped = null;
}
if (addedSingleLink) sb.append("</code></a>");
appendPackageName(sb, packageName);
}
private static void appendPackageName(@NotNull StringBuilder sb, @Nullable String name) {
if (StringUtil.isEmpty(name)) return; // no package name
sb.append(" <font color=").append(toHex(CONTEXT_HELP_FOREGROUND));
sb.append("><code>(").append(name).append(")</code></font>");
}
private static void appendContextHelp(@NotNull StringBuilder sb, @Nullable String actionId) {
if (actionId == null) return; // action id is not set
AnAction action = ActionManager.getInstance().getAction(actionId);
if (action == null) return; // action is not exist
String text = getPreferredShortcutText(action.getShortcutSet().getShortcuts());
if (StringUtil.isEmpty(text)) return; // action have no shortcuts
sb.append("<br><div style='margin-top: 5px'><font size='2' color='");
sb.append(toHex(CONTEXT_HELP_FOREGROUND));
sb.append("'>Press ").append(text).append(" to navigate</font></div>");
}
private static boolean appendLink(@NotNull StringBuilder sb, @NotNull PsiElement element) {
try {
String name = getQualifiedName(element);
if (!StringUtil.isEmpty(name)) {
sb.append("<a href=\"#element/").append(name).append("\"><code>");
return true;
}
VirtualFile file = getVirtualFile(element);
if (file == null) return false;
int offset = element.getTextOffset();
sb.append("<a href=\"#navigation/");
sb.append(toSystemIndependentName(file.getPath()));
sb.append(":").append(offset).append("\"><code>");
return true;
}
catch (Exception ignored) {
return false;
}
}
@Nullable
private static String getQualifiedName(@NotNull PsiElement element) {
PsiClass psiClass = element instanceof PsiClass ? (PsiClass)element : getStubOrPsiParentOfType(element, PsiClass.class);
if (psiClass instanceof PsiAnonymousClass) return null;
for (QualifiedNameProvider provider : QualifiedNameProvider.EP_NAME.getExtensionList()) {
String name = provider.getQualifiedName(element);
if (name != null) return name;
}
return null;
}
@Nullable
private static PsiElement getContainingElement(@NotNull PsiElement element) {
PsiMember member = getStubOrPsiParentOfType(element, PsiMember.class);
return member != null ? member : element.getContainingFile();
}
@Nullable
private static String getPresentableName(@NotNull PsiElement element) {
if (element instanceof PsiEnumConstantInitializer) {
PsiEnumConstantInitializer initializer = (PsiEnumConstantInitializer)element;
return initializer.getEnumConstant().getName();
}
if (element instanceof PsiAnonymousClass) {
return "Anonymous";
}
if (element instanceof PsiNamedElement) {
PsiNamedElement named = (PsiNamedElement)element;
return named.getName();
}
return null;
}
}
@@ -10,17 +10,13 @@ import com.intellij.codeInsight.daemon.DaemonBundle;
import com.intellij.codeInsight.daemon.GutterIconNavigationHandler;
import com.intellij.codeInsight.navigation.BackgroundUpdaterTask;
import com.intellij.ide.util.*;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.actionSystem.Shortcut;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.DumbService;
import com.intellij.psi.*;
import com.intellij.psi.impl.FindSuperElementsHelper;
import com.intellij.psi.presentation.java.ClassPresentationUtil;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.psi.search.PsiElementProcessorAdapter;
@@ -35,14 +31,13 @@ import com.intellij.util.Function;
import com.intellij.util.NullableFunction;
import com.intellij.util.containers.ContainerUtil;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.event.MouseEvent;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
public class MarkerType {
private final GutterIconNavigationHandler<PsiElement> handler;
@@ -115,44 +110,44 @@ public class MarkerType {
PsiMethod[] superMethods = composeSuperMethods(method, acceptSelf);
if (superMethods.length == 0) return null;
PsiMethod superMethod = superMethods[0];
boolean isAbstract = method.hasModifierProperty(PsiModifier.ABSTRACT);
boolean isSuperAbstract = superMethod.hasModifierProperty(PsiModifier.ABSTRACT);
final boolean sameSignature = superMethod.getSignature(PsiSubstitutor.EMPTY).equals(method.getSignature(PsiSubstitutor.EMPTY));
@NonNls final String key;
if (isSuperAbstract && !isAbstract){
key = sameSignature ? "method.implements" : "method.implements.in";
}
else{
key = sameSignature ? "method.overrides" : "method.overrides.in";
}
return composeText(superMethods, "", DaemonBundle.message(key), IdeActions.ACTION_GOTO_SUPER);
AtomicBoolean first = new AtomicBoolean(true);
return GutterTooltipHelper.getTooltipText(
Arrays.asList(superMethods),
superMethod -> getTooltipPrefix(method, superMethod, first.getAndSet(false)),
superMethod -> isSameSignature(method, superMethod),
IdeActions.ACTION_GOTO_SUPER);
}
@Nullable
private static String calculateOverridingSiblingMethodTooltip(@NotNull PsiMethod method) {
FindSuperElementsHelper.SiblingInfo pair = FindSuperElementsHelper.getSiblingInfoInheritedViaSubClass(method);
if (pair == null) return null;
PsiMethod superMethod = pair.superMethod;
PsiClass subClass = pair.subClass;
boolean isAbstract = method.hasModifierProperty(PsiModifier.ABSTRACT);
boolean isSuperAbstract = superMethod.hasModifierProperty(PsiModifier.ABSTRACT);
String postfix = MessageFormat.format(" via sub-class <a href=\"#javaClass/{0}\">{0}</a>", ClassPresentationUtil.getNameForClass(subClass, true));
@NonNls String pattern = DaemonBundle.message(isSuperAbstract && !isAbstract ?
"method.implements" :
"method.overrides") + postfix;
return composeText(new PsiElement[]{superMethod}, "", pattern, IdeActions.ACTION_GOTO_SUPER);
return GutterTooltipHelper.getTooltipText(
Arrays.asList(pair.superMethod, pair.subClass),
element -> element instanceof PsiMethod ? getTooltipPrefix(method, (PsiMethod)element, true) : " via sub-class ",
element -> element instanceof PsiMethod && isSameSignature(method, (PsiMethod)element),
IdeActions.ACTION_GOTO_SUPER);
}
@NotNull
private static String composeText(@NotNull PsiElement[] methods, @NotNull String start, @NotNull String pattern, @NotNull String actionId) {
Shortcut[] shortcuts = ActionManager.getInstance().getAction(actionId).getShortcutSet().getShortcuts();
Shortcut shortcut = ArrayUtil.getFirstElement(shortcuts);
String postfix = "<br><div style='margin-top: 5px'><font size='2'>Click";
if (shortcut != null) postfix += " or press " + KeymapUtil.getShortcutText(shortcut);
postfix += " to navigate</font></div>";
return GutterIconTooltipHelper.composeText(Arrays.asList(methods), start, pattern, postfix);
private static String getTooltipPrefix(@NotNull PsiMethod method, @NotNull PsiMethod superMethod, boolean first) {
StringBuilder sb = new StringBuilder();
if (!first) sb.append("<br>");
boolean isAbstract = method.hasModifierProperty(PsiModifier.ABSTRACT);
boolean isSuperAbstract = superMethod.hasModifierProperty(PsiModifier.ABSTRACT);
sb.append(isSuperAbstract && !isAbstract ? "Implements method " : "Overrides method ");
if (isSameSignature(method, superMethod)) sb.append("in ");
return sb.toString();
}
private static boolean isSameSignature(@NotNull PsiMethod method, @NotNull PsiMethod superMethod) {
return method.getSignature(PsiSubstitutor.EMPTY).equals(superMethod.getSignature(PsiSubstitutor.EMPTY));
}
@NotNull
private static String getImplementationTooltip(@NotNull String prefix, @NotNull PsiElement... elements) {
return GutterTooltipHelper.getTooltipText(Arrays.asList(elements), prefix, true, IdeActions.ACTION_GOTO_IMPLEMENTATION);
}
private static void navigateToOverridingMethod(MouseEvent e, @NotNull PsiMethod method, boolean acceptSelf) {
@@ -164,6 +159,7 @@ public class MarkerType {
DaemonBundle.message("navigation.findUsages.title.super.method", method.getName()),
new MethodCellRenderer(showMethodNames));
}
private static void navigateToSiblingOverridingMethod(MouseEvent e, @NotNull PsiMethod method) {
PsiMethod superMethod = FindSuperElementsHelper.getSiblingInheritedViaSubClass(method);
if (superMethod == null) return;
@@ -203,14 +199,13 @@ public class MarkerType {
PsiElement parent = element.getParent();
if (!(parent instanceof PsiMethod)) return;
navigateToOverriddenMethod(e, (PsiMethod)parent);
}
});
private static String getOverriddenMethodTooltip(@NotNull PsiMethod method) {
final PsiClass aClass = method.getContainingClass();
if (aClass != null && CommonClassNames.JAVA_LANG_OBJECT.equals(aClass.getQualifiedName())) {
return DaemonBundle.message("method.is.implemented.too.many");
return getImplementationTooltip("Has implementations");
}
PsiElementProcessor.CollectElementsWithLimit<PsiMethod> processor = new PsiElementProcessor.CollectElementsWithLimit<>(5);
@@ -220,13 +215,13 @@ public class MarkerType {
boolean isAbstract = method.hasModifierProperty(PsiModifier.ABSTRACT);
if (processor.isOverflow()){
return DaemonBundle.message(isAbstract ? "method.is.implemented.too.many" : "method.is.overridden.too.many");
return getImplementationTooltip(isAbstract ? "Has implementations" : "Is overridden in subclasses");
}
PsiMethod[] overridings = processor.toArray(PsiMethod.EMPTY_ARRAY);
if (overridings.length == 0) {
if (aClass != null && isAbstract && FunctionalExpressionSearch.search(aClass).findFirst() != null) {
return "Has functional implementations";
return getImplementationTooltip("Has functional implementations");
}
return null;
}
@@ -234,9 +229,7 @@ public class MarkerType {
Comparator<PsiMethod> comparator = new MethodCellRenderer(false).getComparator();
Arrays.sort(overridings, comparator);
String start = DaemonBundle.message(isAbstract ? "method.is.implemented.header" : "method.is.overriden.header");
@NonNls String pattern = "&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#javaClass/{1}\">{1}</a>";
return composeText(overridings, start, pattern, IdeActions.ACTION_GOTO_IMPLEMENTATION);
return getImplementationTooltip(isAbstract ? "Is implemented in" : "Is overridden in", overridings);
}
private static void navigateToOverriddenMethod(MouseEvent e, @NotNull final PsiMethod method) {
@@ -298,7 +291,7 @@ public class MarkerType {
ClassInheritorsSearch.search(aClass).forEach(new PsiElementProcessorAdapter<>(processor));
if (processor.isOverflow()) {
return DaemonBundle.message(aClass.isInterface() ? "interface.is.implemented.too.many" : "class.is.subclassed.too.many");
return getImplementationTooltip(aClass.isInterface() ? "Has implementations" : "Has subclasses");
}
PsiClass[] subclasses = processor.toArray(PsiClass.EMPTY_ARRAY);
@@ -307,7 +300,7 @@ public class MarkerType {
new PsiElementProcessor.CollectElementsWithLimit<>(2, new THashSet<>());
FunctionalExpressionSearch.search(aClass).forEach(new PsiElementProcessorAdapter<>(functionalImplementations));
if (!functionalImplementations.getCollection().isEmpty()) {
return "Has functional implementations";
return getImplementationTooltip("Has functional implementations");
}
return null;
}
@@ -315,9 +308,7 @@ public class MarkerType {
Comparator<PsiClass> comparator = new PsiClassListCellRenderer().getComparator();
Arrays.sort(subclasses, comparator);
String start = DaemonBundle.message(aClass.isInterface() ? "interface.is.implemented.by.header" : "class.is.subclassed.by.header");
@NonNls String pattern = "&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#javaClass/{0}\">{0}</a>";
return composeText(subclasses, start, pattern, IdeActions.ACTION_GOTO_IMPLEMENTATION);
return getImplementationTooltip(aClass.isInterface() ? "Is implemented by" : "Is subclassed by", subclasses);
}
// Used in Kotlin, please don't make private
@@ -0,0 +1,42 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hint;
import com.intellij.codeInsight.highlighting.TooltipLinkHandler;
import com.intellij.ide.actions.QualifiedNameProvider;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.pom.Navigatable;
import com.intellij.psi.PsiElement;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Handles tooltip links in format {@code #element/qualified.name}.
* On a click opens specified element in an editor and positions caret to the corresponding offset.
*/
public final class ElementLinkHandler extends TooltipLinkHandler {
@Override
public boolean handleLink(@NotNull String name, @NotNull Editor editor) {
Project project = editor.getProject();
if (project != null) {
PsiElement element = qualifiedNameToElement(name, project);
if (element instanceof Navigatable) {
Navigatable navigatable = (Navigatable)element;
if (navigatable.canNavigate()) {
navigatable.navigate(true);
return true;
}
}
}
return false;
}
@Nullable
private static PsiElement qualifiedNameToElement(@NotNull String name, @NotNull Project project) {
for (QualifiedNameProvider provider : QualifiedNameProvider.EP_NAME.getExtensionList()) {
PsiElement element = provider.qualifiedNameToElement(name, project);
if (element != null) return element;
}
return null;
}
}
@@ -700,6 +700,7 @@
<codeInsight.linkHandler prefix="#inspection/" handlerClass="com.intellij.codeInsight.hint.InspectionDescriptionLinkHandler"/>
<codeInsight.linkHandler prefix="#navigation/" handlerClass="com.intellij.codeInsight.hint.NavigationLinkHandler"/>
<codeInsight.linkHandler prefix="#element/" handlerClass="com.intellij.codeInsight.hint.ElementLinkHandler"/>
<codeInsight.lineMarkerProvider language="" implementationClass="com.intellij.ui.ColorLineMarkerProvider"/>
<codeFoldingOptionsProvider instance="com.intellij.application.options.editor.BaseCodeFoldingOptionsProvider" order="first"/>
@@ -277,6 +277,9 @@ ide.balloon.shadow.size=15
ide.tooltip.show.with.actions=false
ide.tooltip.show.with.actions.description=Show new error tooltips with fixes
gutter.tooltip.single.link=true
gutter.tooltip.single.link.description=Shows a single link for all classes in a chain
ide.helptooltip.enabled=false
ide.helptooltip.enabled.restartRequired=true
ide.helptooltip.enabled.description=Enables new style help context tooltips