diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/GutterIconTooltipHelper.java b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/GutterIconTooltipHelper.java index 04065e1c9dc4..390e659af992 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/GutterIconTooltipHelper.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/daemon/impl/GutterIconTooltipHelper.java @@ -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 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 elements, @NotNull String start, @NotNull String pattern, diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/GutterTooltipHelper.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/GutterTooltipHelper.java new file mode 100644 index 000000000000..25cb435a94c9 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/GutterTooltipHelper.java @@ -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 String getTooltipText(@NotNull Collection elements, + @NotNull String prefix, + boolean skipFirstMember, + @Nullable String actionId) { + String elementPrefix = 1 < elements.size() ? "
     " : " "; + 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 String getTooltipText(@NotNull Collection elements, + @NotNull Function function, + @NotNull Predicate predicate, + @Nullable String actionId) { + return getTooltipText(null, elements, function, predicate, actionId); + } + + @NotNull + private static String getTooltipText(@Nullable String prefix, + @NotNull Collection elements, + @NotNull Function function, + @NotNull Predicate 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(""); + // sb.append(""); + sb.append(name); + // sb.append(""); + // if (deprecated) sb.append(""); + if (addedLink) sb.append(""); + } + 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(""); + appendPackageName(sb, packageName); + } + + private static void appendPackageName(@NotNull StringBuilder sb, @Nullable String name) { + if (StringUtil.isEmpty(name)) return; // no package name + sb.append(" (").append(name).append(")"); + } + + 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("
Press ").append(text).append(" to navigate
"); + } + + private static boolean appendLink(@NotNull StringBuilder sb, @NotNull PsiElement element) { + try { + String name = getQualifiedName(element); + if (!StringUtil.isEmpty(name)) { + sb.append(""); + return true; + } + VirtualFile file = getVirtualFile(element); + if (file == null) return false; + + int offset = element.getTextOffset(); + sb.append(""); + 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; + } +} diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/MarkerType.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/MarkerType.java index 4817603f2290..1973af1c56f4 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/MarkerType.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/MarkerType.java @@ -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 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 {0}", 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 = "
Click"; - if (shortcut != null) postfix += " or press " + KeymapUtil.getShortcutText(shortcut); - postfix += " to navigate
"; - 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("
"); + 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 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 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 = "    {1}"; - 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 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 = "    {0}"; - 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 diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hint/ElementLinkHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/hint/ElementLinkHandler.java new file mode 100644 index 000000000000..b4ed96511e58 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/hint/ElementLinkHandler.java @@ -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; + } +} diff --git a/platform/platform-resources/src/META-INF/LangExtensions.xml b/platform/platform-resources/src/META-INF/LangExtensions.xml index 88e15c834f76..9f0cc0abd4b9 100644 --- a/platform/platform-resources/src/META-INF/LangExtensions.xml +++ b/platform/platform-resources/src/META-INF/LangExtensions.xml @@ -700,6 +700,7 @@ + diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index c43b9760c9cf..2b98b9ec977d 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -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