From ceaf5f8608ff6f9eec453a9ca18a970b764f0e85 Mon Sep 17 00:00:00 2001 From: "Denis.Zhdanov" Date: Wed, 18 Jul 2012 16:57:24 +0400 Subject: [PATCH] IDEA-66333 Quick documentation lookup on mouse hover Correct links insertion when target text contains either fully qualified or simple names --- .../navigation/DocPreviewUtil.java | 75 ++++++++++++++++--- .../navigation/DocPreviewUtilTest.groovy | 17 +++++ 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/platform/lang-impl/src/com/intellij/codeInsight/navigation/DocPreviewUtil.java b/platform/lang-impl/src/com/intellij/codeInsight/navigation/DocPreviewUtil.java index 898a06f1bc12..2b951b765701 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/navigation/DocPreviewUtil.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/navigation/DocPreviewUtil.java @@ -17,12 +17,12 @@ package com.intellij.codeInsight.navigation; import com.intellij.codeInsight.documentation.DocumentationManager; import com.intellij.lang.documentation.DocumentationProvider; +import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -36,13 +36,33 @@ import java.util.regex.Pattern; */ public class DocPreviewUtil { + /** + * We shorten links text from fully qualified name to short names (e.g. from 'java.lang.String' to 'String'). + * There is a possible situation then that we have two replacements where one key is a simple name and another one is a fully qualified + * one. We want to apply 'from fully qualified name' replacement first then. + */ + private static final Comparator REPLACEMENTS_COMPARATOR = new Comparator() { + @Override + public int compare(@NotNull String o1, @NotNull String o2) { + if (o1.endsWith(o2)) { + return -1; + } + else if (o2.endsWith(o1)) { + return 1; + } + else { + return o1.compareTo(o2); + } + } + }; + private DocPreviewUtil() { } /** * Allows to build a documentation preview from the given arguments. Basically, takes given 'header' text and tries to modify * it by using hyperlink information encapsulated at the given 'full text'. - * + * * @param header target documentation header. Is expected to be a result of the * {@link DocumentationProvider#getQuickNavigateInfo(PsiElement, PsiElement)} call * @param qName there is a possible case that not all documentation text will be included to the preview @@ -55,25 +75,60 @@ public class DocPreviewUtil { if (fullText == null) { return header; } - + // Build links info. Map links = new HashMap(); process(fullText, new LinksCollector(links)); if (qName != null) { links.put(qName, DocumentationManager.PSI_ELEMENT_PROTOCOL + qName); } - + // Apply links info to the header template. - String result = header.replace("\n", "
"); - for (Map.Entry entry : links.entrySet()) { - String visibleName = entry.getKey(); + List modifiedRanges = new ArrayList(); + List sortedReplacements = new ArrayList(links.keySet()); + Collections.sort(sortedReplacements, REPLACEMENTS_COMPARATOR); + StringBuilder buffer = new StringBuilder(header); + replace(buffer, "\n", "
", modifiedRanges); + for (String replaceFrom : sortedReplacements) { + String visibleName = replaceFrom; int i = visibleName.lastIndexOf('.'); if (i > 0 && i < visibleName.length() - 1) { visibleName = visibleName.substring(i + 1); } - result = result.replace(entry.getKey(), String.format("%s", entry.getValue(), visibleName)); + replace(buffer, replaceFrom, String.format("%s", links.get(replaceFrom), visibleName), modifiedRanges); } - return result; + return buffer.toString(); + } + + private static void replace(@NotNull StringBuilder text, + @NotNull String replaceFrom, + @NotNull String replaceTo, + @NotNull List readOnlyChanges) + { + for (int i = text.indexOf(replaceFrom); i >= 0 && i < text.length() - 1; i = text.indexOf(replaceFrom, i + 1)) { + int end = i + replaceFrom.length(); + if (intersects(readOnlyChanges, i, end)) { + continue; + } + text.replace(i, end, replaceTo); + int diff = replaceTo.length() - replaceFrom.length(); + for (int j = 0; j < readOnlyChanges.size(); j++) { + TextRange range = readOnlyChanges.get(j); + if (range.getStartOffset() >= end) { + readOnlyChanges.set(j, range.shiftRight(diff)); + } + } + readOnlyChanges.add(new TextRange(i, i + replaceTo.length())); + } + } + + private static boolean intersects(@NotNull List ranges, int start, int end) { + for (TextRange range : ranges) { + if (range.intersectsStrict(start, end)) { + return true; + } + } + return false; } private enum State {TEXT, INSIDE_OPEN_TAG, INSIDE_CLOSE_TAG} diff --git a/platform/lang-impl/testSources/com/intellij/codeInsight/navigation/DocPreviewUtilTest.groovy b/platform/lang-impl/testSources/com/intellij/codeInsight/navigation/DocPreviewUtilTest.groovy index e9f4e1ebf11d..a67400a5e9fc 100644 --- a/platform/lang-impl/testSources/com/intellij/codeInsight/navigation/DocPreviewUtilTest.groovy +++ b/platform/lang-impl/testSources/com/intellij/codeInsight/navigation/DocPreviewUtilTest.groovy @@ -128,4 +128,21 @@ Bar def actual = DocPreviewUtil.buildPreview(header, "java.lang.String", fullText) assertEquals(expected, actual) } + + @Test + void crossingLinks() { + def header = '''\ +OCCompletionPriority +com.jetbrains.objc.lang.completion.OCCompletionPriority SMART_COMPLETION_PRIORITY''' + + def fullText = '''\ + com.jetbrains.objc.lang.completion.OCCompletionPriority
OCCompletionPriority SMART_COMPLETION_PRIORITY
+ +Qname''' + + def expected = '''\ +OCCompletionPriority
OCCompletionPriority SMART_COMPLETION_PRIORITY''' + + assertEquals(expected, DocPreviewUtil.buildPreview(header, null, fullText)) + } }