void (PsiInlineDocTag tag, StringBuilder buffer) { boolean isCodeBlock = isCodeBlock(tag); if (isCodeBlock) { // remove excess whitespaces between tags e.g. in `<pre> {@code` int lastNonWhite = buffer.length() - 1; while (buffer.charAt(lastNonWhite) == ' ') lastNonWhite--; buffer.setLength(lastNonWhite + 1); } final int offset = isCodeBlock ? getCodeTagOffset(tag) : 0; buffer.append("<code style='font-size:"); buffer.append(DocumentationSettings.getMonospaceFontSizeCorrection(isRendered())); buffer.append("%;'>"); int pos = buffer.length(); StringBuilder codeSnippetBuilder = new StringBuilder(); generateLiteralValue(codeSnippetBuilder, tag, !isCodeBlock && getInlineCodeHighlightingMode() == InlineCodeHighlightingMode.NO_HIGHLIGHTING); String codeSnippet = codeSnippetBuilder.toString(); if (isCodeBlock) { codeSnippet = StringsKt.trimIndent(codeSnippet); } if (isCodeBlock && doHighlightCodeBlocks() || !isCodeBlock && getInlineCodeHighlightingMode() == InlineCodeHighlightingMode.SEMANTIC_HIGHLIGHTING) { // highlights code by lexer codeSnippetBuilder.setLength(0); appendHighlightedByLexerAndEncodedAsHtmlCodeSnippet(true, codeSnippetBuilder, tag.getProject(), tag.getLanguage(), codeSnippet); codeSnippet = codeSnippetBuilder.toString(); } if (isCodeBlock && doHighlightCodeBlocks() || !isCodeBlock && getInlineCodeHighlightingMode() != InlineCodeHighlightingMode.NO_HIGHLIGHTING) { // highlights plain code as HighlighterColors.TEXT codeSnippetBuilder.setLength(0); TextAttributes codeAttributes = EditorColorsManager.getInstance().getGlobalScheme().getAttributes(HighlighterColors.TEXT).clone(); codeAttributes.setBackgroundColor(null); appendStyledSpan(true, codeSnippetBuilder, codeAttributes, codeSnippet); codeSnippet = codeSnippetBuilder.toString(); } if (isCodeBlock) { // indent code block codeSnippet = Arrays.stream(codeSnippet.contains(BR_TAG) ? codeSnippet.split(BR_TAG) : codeSnippet.split("\n")) .map(it -> " ".repeat(offset + 1) + it) .collect(Collectors.joining(BR_TAG)); } else { // we are in inline @code block, we should remove all new lines codeSnippet = codeSnippet.replace(BR_TAG, ""); } buffer.append(codeSnippet); buffer.append("</code>"); if (buffer.charAt(pos) == '\n') buffer.insert(pos, ' '); // line break immediately after opening tag is ignored by JEditorPane } private static int getCodeTagOffset(@NotNull PsiInlineDocTag tag) { final PsiElement sibling = tag.getPrevSibling(); if (sibling == null || !sibling.getText().isBlank()) return 0; return sibling.getTextLength(); } private void generateLiteralValue(StringBuilder buffer, PsiDocTag tag, boolean doEscaping) { StringBuilder tmpBuffer = new StringBuilder(); PsiElement[] children = tag.getChildren(); for (int i = 2; i < children.length - 1; i++) { // process all children except tag opening/closing elements PsiElement child = children[i]; if (isLeadingAsterisks(child)) continue; String elementText = child.getText(); if (child instanceof PsiWhiteSpace) { int pos = elementText.lastIndexOf('\n'); if (pos >= 0) elementText = elementText.substring(0, pos + 1); // skip whitespace before leading asterisk } appendPlainText(tmpBuffer, doEscaping ? StringUtil.escapeXmlEntities(elementText) : elementText); } if ((mySdkVersion == null || mySdkVersion.isAtLeast(JavaSdkVersion.JDK_1_8)) && isInPre(tag)) { buffer.append(tmpBuffer); } else { buffer.append(StringUtil.trimLeading(tmpBuffer)); } } private static boolean isInPre(PsiDocTag tag) { PsiElement sibling = tag.getPrevSibling(); while (sibling != null) { if (sibling instanceof PsiDocToken) { String text = StringUtil.toLowerCase(sibling.getText()); int pos = text.lastIndexOf("pre>"); if (pos > 0) { switch (text.charAt(pos - 1)) { case '<' -> { return true; } case '/' -> { return false; } } } } sibling = sibling.getPrevSibling(); } return false; } private static void appendPlainText(StringBuilder buffer, String text) { buffer.append(StringUtil.replaceUnicodeEscapeSequences(text)); } protected boolean isLeadingAsterisks(@Nullable PsiElement element) { return (element instanceof PsiDocToken) && ((PsiDocToken)element).getTokenType() == JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS; } private void generateLinkValue(PsiInlineDocTag tag, StringBuilder buffer, boolean plainLink) { PsiElement[] tagElements = tag.getDataElements(); String text = createLinkText(tagElements); if (!text.isEmpty()) { generateLink(buffer, text, tagElements[0], plainLink); } } private void generateValueValue(PsiInlineDocTag tag, StringBuilder buffer, PsiElement element) { String text = createLinkText(tag.getDataElements()); PsiField valueField = null; if (text.isEmpty()) { if (myElement instanceof PsiField) valueField = (PsiField)myElement; } else { if (text.indexOf('#') == -1) { text = '#' + text; } PsiElement target = null; try { target = JavaDocUtil.findReferenceTarget(PsiManager.getInstance(myProject), text, myElement); } catch (IndexNotReadyException e) { LOG.debug(e); } if (target instanceof PsiField) { valueField = (PsiField)target; } } Object value = null; if (valueField != null) { PsiExpression initializer = valueField.getInitializer(); value = DumbService.isDumb(myProject) ? null : JavaConstantExpressionEvaluator.computeConstantExpression(initializer, false); } if (value != null) { String valueText = StringUtil.escapeXmlEntities(value.toString()); if (value instanceof String) valueText = '"' + valueText + '"'; if (valueField.equals(myElement)) { buffer.append(valueText); // don't generate link to itself } else { generateLink(buffer, valueField, valueText, true); } } else { buffer.append(element.getText()); } } protected String createLinkText(PsiElement[] tagElements) { int predictOffset = tagElements.length > 0 ? tagElements[0].getTextOffset() + tagElements[0].getText().length() : 0; StringBuilder buffer = new StringBuilder(); for (PsiElement tagElement : tagElements) { if (tagElement.getTextOffset() > predictOffset) buffer.append(' '); predictOffset = tagElement.getTextOffset() + tagElement.getText().length(); collectElementText(buffer, tagElement); } return buffer.toString().trim(); } protected void collectElementText(StringBuilder buffer, PsiElement element) { element.accept(new PsiRecursiveElementWalkingVisitor() { @Override public void visitElement(@NotNull PsiElement element) { super.visitElement(element); if (element instanceof PsiWhiteSpace || element instanceof PsiJavaToken || element instanceof PsiDocToken && ((PsiDocToken)element).getTokenType() != JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS) { buffer.append(element.getText()); } } }); } private void generateDeprecatedSection(StringBuilder buffer, PsiDocComment comment) { generateSingleTagSection(buffer, comment, "deprecated", JavaBundle.messagePointer("javadoc.deprecated")); } private void generateSingleTagSection(StringBuilder buffer, PsiDocComment comment, String tagName, Supplier<String> computePresentableName) { generateSingleTagSection(buffer, computePresentableName, comment.findTagByName(tagName)); } private void generateSingleTagSection(StringBuilder buffer, Supplier<String> computePresentableName, PsiDocTag tag) { if (tag != null) { startHeaderSection(buffer, computePresentableName.get()).append("<p>"); final PsiElement[] elements = Arrays.stream(tag.getChildren()) .skip(1) .filter(e -> e.getNode().getElementType() != JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS) .toArray(PsiElement[]::new); generateValue(buffer, elements, ourEmptyElementsProvider); buffer.append(DocumentationMarkup.SECTION_END); } } private void generateSinceSection(StringBuilder buffer, PsiDocComment comment) { generateSingleTagSection(buffer, comment, "since", JavaBundle.messagePointer("javadoc.since")); } protected void generateSeeAlsoSection(StringBuilder buffer, PsiDocComment comment) { PsiDocTag[] tags = comment.findTagsByName("see"); if (tags.length > 0) { startHeaderSection(buffer, JavaBundle.message("javadoc.see.also")).append("<p>"); for (int i = 0; i < tags.length; i++) { PsiDocTag tag = tags[i]; PsiElement[] elements = tag.getDataElements(); if (elements.length > 0) { String text = createLinkText(elements); if (StringUtil.startsWithChar(text, '<')) { buffer.append(text); } else if (StringUtil.startsWithChar(text, '"')) { appendPlainText(buffer, text); } else { generateLink(buffer, text, comment, false); } } if (i < tags.length - 1) { buffer.append(","); buffer.append(BR_TAG); } } buffer.append(DocumentationMarkup.SECTION_END); } } protected void generateAuthorSection(StringBuilder buffer, PsiDocComment comment) { PsiDocTag[] tags = comment.findTagsByName("author"); if (tags.length > 0) { startHeaderSection(buffer, JavaBundle.message("javadoc.author")).append("<p>"); for (int i = 0; i < tags.length; i++) { StringBuilder tmp = new StringBuilder(); generateValue(tmp, tags[i].getDataElements(), ourEmptyElementsProvider); buffer.append(tmp.toString().trim()); if (i < tags.length - 1) { buffer.append(", "); } } buffer.append(DocumentationMarkup.SECTION_END); } } private void generateParametersSection(StringBuilder buffer, PsiMethod method, PsiDocComment comment) { PsiParameter[] params = method.getParameterList().getParameters(); PsiDocTag[] localTags = comment != null ? comment.findTagsByName("param") : PsiDocTag.EMPTY_ARRAY; List<ParamInfo> collectedTags = new ArrayList<>(); for (int i = 0; i < params.length; i++) { PsiParameter param = params[i]; String presentableName = generateOneParameterPresentableName(param); DocTagLocator<PsiDocTag> tagLocator = parameterLocator(i); ParamInfo parmTag = findDocTag(localTags, param.getName(), presentableName, method, tagLocator); if (parmTag != null) { collectedTags.add(parmTag); } } generateParametersSection(buffer, CodeInsightBundle.message("javadoc.parameters"), collectedTags); } private void generateTypeParametersSection(StringBuilder buffer, PsiMethod method, PsiDocComment comment) { PsiDocTag[] localTags = comment == null ? PsiDocTag.EMPTY_ARRAY : comment.findTagsByName("param"); PsiTypeParameter[] typeParameters = method.getTypeParameters(); List<ParamInfo> collectedTags = new ArrayList<>(); for (int i = 0; i < typeParameters.length; i++) { PsiTypeParameter typeParameter = typeParameters[i]; String presentableName = generateOneTypeParameterPresentableName(typeParameter); DocTagLocator<PsiDocTag> tagLocator = typeParameterLocator(i); ParamInfo parmTag = findDocTag(localTags, typeParameter.getName(), presentableName, method, tagLocator); if (parmTag != null) { collectedTags.add(parmTag); } } generateParametersSection(buffer, JavaBundle.message("javadoc.type.parameters"), collectedTags); } private void generateParametersSection(StringBuilder buffer, String titleMessage, List<ParamInfo> collectedTags) { if (!collectedTags.isEmpty()) { startHeaderSection(buffer, titleMessage) .append(StringUtil.join(collectedTags, tag -> generateOneParameter(tag), "<br/>")) .append(DocumentationMarkup.SECTION_END); } } @Nullable private ParamInfo findDocTag(PsiDocTag[] localTags, String paramName, String presentableName, PsiMethod method, DocTagLocator<PsiDocTag> tagLocator) { PsiDocTag localTag = getTagByName(localTags, paramName); if (localTag != null) { return new ParamInfo(paramName, presentableName, localTag, new InheritDocProvider<>() { @Override public Pair<PsiDocTag, InheritDocProvider<PsiDocTag>> getInheritDoc() { return findInheritDocTag(method, tagLocator); } @Override public PsiClass getElement() { return method.getContainingClass(); } }); } if (isRendered()) return null; Pair<PsiDocTag, InheritDocProvider<PsiDocTag>> tag = findInheritDocTag(method, tagLocator); return tag == null ? null : new ParamInfo(paramName, presentableName, tag); } private String generateOneParameter(ParamInfo tag) { PsiElement[] elements = tag.docTag.getDataElements(); if (elements.length == 0) return ""; String text = elements[0].getText(); StringBuilder buffer = new StringBuilder(); buffer.append("<code>"); buffer.append(tag.presentableName); buffer.append("</code>"); StringBuilder descriptionBuffer = new StringBuilder(); generateValue(descriptionBuffer, tag, elements, text); if (!StringUtil.isEmptyOrSpaces(descriptionBuffer)) { buffer.append(" &ndash; "); buffer.append(descriptionBuffer); } return buffer.toString(); } private void generateReturnsSection(StringBuilder buffer, PsiMethod method, PsiDocComment comment) { PsiDocTag tag = comment == null ? null : new ReturnTagLocator().find(method, comment); Pair<PsiDocTag, InheritDocProvider<PsiDocTag>> pair = tag == null ? null : new Pair<>(tag, new InheritDocProvider<>() { @Override public Pair<PsiDocTag, InheritDocProvider<PsiDocTag>> getInheritDoc() { return findInheritDocTag(method, new ReturnTagLocator()); } @Override public PsiClass getElement() { return method.getContainingClass(); } }); if (!isRendered() && pair == null && myElement instanceof PsiMethod) { pair = findInheritDocTag((PsiMethod)myElement, new ReturnTagLocator()); } if (pair != null) { startHeaderSection(buffer, CodeInsightBundle.message("javadoc.returns")) .append("<p>"); generateValue(buffer, pair.first.getDataElements(), mapProvider(pair.second, false)); buffer.append(DocumentationMarkup.SECTION_END); } } private static PsiDocTag[] getThrowsTags(PsiDocComment comment) { if (comment == null) return PsiDocTag.EMPTY_ARRAY; PsiDocTag[] tags1 = comment.findTagsByName("throws"); PsiDocTag[] tags2 = comment.findTagsByName("exception"); return ArrayUtil.mergeArrays(tags1, tags2); } private static boolean areWeakEqual(String one, String two) { return one.equals(two) || one.endsWith('.' + two) || two.endsWith('.' + one); } private void generateThrowsSection(StringBuilder buffer, PsiMethod method, PsiDocComment comment) { final PsiDocTag[] throwsJavadocTags = getThrowsTags(comment); final PsiJavaCodeReferenceElement[] methodThrows = isRendered() ? PsiJavaCodeReferenceElement.EMPTY_ARRAY : method.getThrowsList().getReferenceElements(); final int totalThrowsToDocument = throwsJavadocTags.length + methodThrows.length; if (totalThrowsToDocument == 0) return; final class Data { final @NotNull PsiJavaCodeReferenceElement ref; final @Nullable PsiDocTag tag; private Data(@NotNull PsiJavaCodeReferenceElement ref, @Nullable PsiDocTag tag) { this.ref = ref; this.tag = tag; } } final Map<@NotNull String, @NotNull Data> throwTags = new LinkedHashMap<>(totalThrowsToDocument, 1.0f); for (PsiDocTag tag : throwsJavadocTags) { final PsiDocTagValue value = tag.getValueElement(); if (value == null) continue; final ASTNode[] children = value.getNode().getChildren(TokenSet.create(JavaDocElementType.DOC_REFERENCE_HOLDER)); if (children.length != 1) continue; final PsiJavaCodeReferenceElement element = ObjectUtils.tryCast(children[0].getFirstChildNode(), PsiJavaCodeReferenceElement.class); if (element != null) { throwTags.put(element.getQualifiedName(), new Data(element, tag)); } } Arrays.stream(methodThrows) .filter(e -> !throwTags.containsKey(e.getQualifiedName())) .forEach(e -> throwTags.put(e.getQualifiedName(), new Data(e, null))); if (throwTags.isEmpty()) return; startHeaderSection(buffer, CodeInsightBundle.message("javadoc.throws")); for (Map.Entry<String, Data> throwTag : throwTags.entrySet()) { final Data value = throwTag.getValue(); final PsiJavaCodeReferenceElement exceptionType = value.ref; final PsiClass target = ObjectUtils.tryCast(exceptionType.resolve(), PsiClass.class); final PsiDocTag tag = value.tag; buffer.append("<p>"); final PsiElement[] elements = tag != null ? tag.getDataElements() : PsiElement.EMPTY_ARRAY; if (target != null) { generateLink(buffer, target); } else if (elements.length != 0) { generateLink(buffer, elements[0].getText(), null, method, false); } else { generateUnresolvedLink(buffer, exceptionType); } if (elements.length < 2) continue; buffer.append(" &ndash; "); final Pair<PsiDocTag, InheritDocProvider<PsiDocTag>> tagToInheritDocProvider = findInheritDocTag(method, exceptionLocator(exceptionType.getQualifiedName())); generateValue(buffer, elements, 1, tagToInheritDocProvider == null ? null : new InheritDocProvider<>() { @Override public Pair<PsiElement[], InheritDocProvider<PsiElement[]>> getInheritDoc() { final PsiElement[] docElements = tagToInheritDocProvider.first.getDataElements(); final PsiElement[] result = Arrays.stream(docElements) .skip(1) .toArray(PsiElement[]::new); return Pair.pair(result, null); } @Override public PsiClass getElement() { return tagToInheritDocProvider.getSecond().getElement(); } }); } buffer.append(DocumentationMarkup.SECTION_END); } private void generateUnresolvedLink(StringBuilder buffer, @NotNull PsiJavaCodeReferenceElement exceptionType) { final String label = JavaDocUtil.getLabelText(exceptionType.getProject(), exceptionType.getManager(), exceptionType.getQualifiedName(), exceptionType); appendMaybeUnresolvedLink(buffer, null, label, myProject, false); } @Contract(mutates = "param1") private void generateLink(@NotNull StringBuilder buffer, @NotNull PsiClass target) { final String label = JavaDocUtil.getLabelText(target.getProject(), target.getManager(), target.getName(), target); appendMaybeUnresolvedLink(buffer, target, label, target.getProject(), false); } private static @Nullable @NlsSafe String generateLink(@NotNull PsiElement element, String label, boolean plainLink, boolean isRenderedDoc) { String refText = JavaDocUtil.getReferenceText(element.getProject(), element); if (refText != null) { StringBuilder linkBuilder = new StringBuilder(); DocumentationManagerUtil.createHyperlink(linkBuilder, element, refText, label, plainLink, isRenderedDoc); return linkBuilder.toString(); } return null; } private void generateSuperMethodsSection(StringBuilder buffer, PsiMethod method, boolean overrides) { PsiClass parentClass = method.getContainingClass(); if (parentClass == null) return; if (parentClass.isInterface() && !overrides) return; PsiMethod[] supers = method.findSuperMethods(); Arrays.sort(supers, Comparator.comparing(m -> { PsiClass aClass = m.getContainingClass(); return aClass == null ? null : aClass.getName(); })); if (supers.length == 0) return; boolean headerGenerated = false; for (PsiMethod superMethod : supers) { boolean isAbstract = superMethod.hasModifierProperty(PsiModifier.ABSTRACT); if (overrides) { if (parentClass.isInterface() != isAbstract) continue; } else { if (!isAbstract) continue; } PsiClass superClass = superMethod.getContainingClass(); if (superClass == null) continue; if (!headerGenerated) { startHeaderSection(buffer, JavaBundle.message(overrides ? "javadoc.method.overrides" : "javadoc.method.specified.by")); buffer.append("<p>"); headerGenerated = true; } else { buffer.append(BR_TAG); } StringBuilder methodBuffer = new StringBuilder(); generateLink( methodBuffer, superMethod, getStyledSpan(doSemanticHighlightingOfLinks(), getHighlightingManager().getMethodDeclarationAttributes(superMethod), superMethod.getName()), false); StringBuilder classBuffer = new StringBuilder(); generateLink(classBuffer, superClass, getStyledSpan(doSemanticHighlightingOfLinks(), getHighlightingManager().getClassDeclarationAttributes(superClass), superClass.getName()), false); if (superClass.isInterface()) { buffer.append(JavaBundle.message("javadoc.method.in.interface", methodBuffer.toString(), classBuffer.toString())); } else { buffer.append(JavaBundle.message("javadoc.method.in.class", methodBuffer.toString(), classBuffer.toString())); } } if (headerGenerated) { buffer.append(DocumentationMarkup.SECTION_END); } } private void generateLink(StringBuilder buffer, String linkText, @NotNull PsiElement context, boolean plainLink) { int index = JavaDocUtil.extractReference(linkText); String refText = linkText.substring(0, index).trim(); String label = StringUtil.nullize(linkText.substring(index).trim()); generateLink(buffer, refText, label, context, plainLink); } void generateLink(StringBuilder buffer, PsiElement element, String label, boolean plainLink) { String refText = JavaDocUtil.getReferenceText(element.getProject(), element); if (refText != null) { DocumentationManagerUtil.createHyperlink(buffer, element, refText, label, plainLink, isRendered()); } } /** * @return Length of the generated label. */ int generateLink(StringBuilder buffer, String refText, String label, @NotNull PsiElement context, boolean plainLink) { if (label == null) { PsiManager manager = context.getManager(); label = JavaDocUtil.getLabelText(manager.getProject(), manager, refText, context); } LOG.assertTrue(refText != null, "refText appears to be null."); PsiElement target = null; try { target = JavaDocUtil.findReferenceTarget(context.getManager(), refText, context, false); } catch (IndexNotReadyException e) { LOG.debug(e); } appendMaybeUnresolvedLink(buffer, target, label, context.getProject(), plainLink); return StringUtil.stripHtml(label, true).length(); } public void appendMaybeUnresolvedLink( StringBuilder buffer, @Nullable PsiElement target, String label, @NotNull Project project, boolean plainLink ) { if (target == null && DumbService.isDumb(project)) { buffer.append(label); } else if (target == null) { buffer.append(getSpanForUnresolvedItem()).append(label).append("</span>"); } else { String highlightedLabel = myIsSignatureGenerationInProgress && doHighlightSignatures() || doSemanticHighlightingOfLinks() ? tryHighlightLinkLabel(target, label) : label; generateLink(buffer, target, highlightedLabel, plainLink); } } static String getSpanForUnresolvedItem() { TextAttributes attributes = EditorColorsManager.getInstance().getGlobalScheme().getAttributes(CodeInsightColors.WRONG_REFERENCES_ATTRIBUTES); Color color = attributes.getForegroundColor(); String htmlColor = color == null ? "red" : ColorUtil.toHtmlColor(color); return "<span style=\"color:" + htmlColor + "\">"; } /** * If highlighted links has the same color as highlighted inline code blocks they will be indistinguishable. * In this case we should change link color to standard hyperlink color which we believe is apriori different. */ private @NotNull TextAttributes tuneAttributesForLink(@NotNull TextAttributes attributes) { EditorColorsScheme globalScheme = EditorColorsManager.getInstance().getGlobalScheme(); TextAttributes defaultText = globalScheme.getAttributes(HighlighterColors.TEXT); TextAttributes identifier = globalScheme.getAttributes(DefaultLanguageHighlighterColors.IDENTIFIER); if (!myIsSignatureGenerationInProgress && (Objects.equals(attributes.getForegroundColor(), defaultText.getForegroundColor()) || Objects.equals(attributes.getForegroundColor(), identifier.getForegroundColor()))) { TextAttributes tuned = attributes.clone(); if (ApplicationManager.getApplication().isUnitTestMode()) { tuned.setForegroundColor(globalScheme.getAttributes(CodeInsightColors.HYPERLINK_ATTRIBUTES).getForegroundColor()); } else { tuned.setForegroundColor(globalScheme.getColor(DefaultLanguageHighlighterColors.DOC_COMMENT_LINK)); } return tuned; } return attributes; } private @NotNull String tryHighlightLinkLabel(@NotNull PsiElement element, @NotNull String label) { if (element instanceof PsiClass) { return getStyledSpan(true, tuneAttributesForLink(getHighlightingManager().getClassDeclarationAttributes((PsiClass)element)), label); } if (element instanceof PsiPackage) { return getStyledSpan(true, tuneAttributesForLink(getHighlightingManager().getClassNameAttributes()), label); } else if (element instanceof PsiMethod) { return tryHighlightLinkOnClassMember( (PsiMember)element, tuneAttributesForLink(getHighlightingManager().getMethodDeclarationAttributes((PsiMethod)element)), label); } else if (element instanceof PsiField) { return tryHighlightLinkOnClassMember( (PsiField)element, tuneAttributesForLink(getHighlightingManager().getFieldDeclarationAttributes((PsiField)element)), label); } else { return getHighlightedByLexerAndEncodedAsHtmlCodeSnippet(element.getProject(), element.getLanguage(), label); } } private @NotNull String tryHighlightLinkOnClassMember( @NotNull PsiMember member, @NotNull TextAttributes labelAttributes, @NotNull String label ) { StringBuilder buffer = new StringBuilder(); int openParenIndex = label.indexOf("("); if (openParenIndex == -1) openParenIndex = label.length(); int classNameIndex = label.substring(0, openParenIndex).lastIndexOf("."); if (classNameIndex != -1) { PsiClass containingClass = member.getContainingClass(); TextAttributes containingClassAttributes = containingClass != null ? getHighlightingManager().getClassDeclarationAttributes(containingClass) : getHighlightingManager().getClassNameAttributes(); containingClassAttributes = tuneAttributesForLink(containingClassAttributes); appendStyledSpan(true, buffer, containingClassAttributes, label.substring(0, classNameIndex)); appendStyledSpan(true, buffer, getHighlightingManager().getDotAttributes(), "."); } classNameIndex++; appendStyledSpan(true, buffer, labelAttributes, label.substring(classNameIndex, openParenIndex)); if (openParenIndex == label.length()) return buffer.toString(); appendHighlightedByLexerAndEncodedAsHtmlCodeSnippet( true, buffer, member.getProject(), member.getLanguage(), label.substring(openParenIndex)); return buffer.toString(); } /** * @return Length of the generated label. */ public int generateType(StringBuilder buffer, PsiType type, PsiElement context) { return generateType(buffer, type, context, true); } /** * @return Length of the generated label. */ public int generateType(StringBuilder buffer, PsiType type, PsiElement context, boolean generateLink) { return generateType(buffer, type, context, generateLink, false); } /** * @return Length of the generated label. */ public int generateType(StringBuilder buffer, PsiType type, PsiElement context, boolean generateLink, boolean useShortNames) { if (type instanceof PsiArrayType) { int rest = generateType(buffer, ((PsiArrayType)type).getComponentType(), context, generateLink, useShortNames); int len = generateTypeAnnotations(buffer, type, context, generateLink, true); if (type instanceof PsiEllipsisType) { buffer.append("..."); return len + rest + 3; } else { appendStyledSpan(buffer, getHighlightingManager().getBracketsAttributes(), "[]"); return len + rest + 2; } } int typAnnoLength = generateTypeAnnotations(buffer, type, context, generateLink, false); if (type instanceof PsiPrimitiveType) { String text = type.getCanonicalText(); appendStyledSpan(buffer, getHighlightingManager().getKeywordAttributes(), StringUtil.escapeXmlEntities(text)); return typAnnoLength + text.length(); } if (type instanceof PsiCapturedWildcardType) { type = ((PsiCapturedWildcardType)type).getWildcard(); } if (type instanceof PsiWildcardType wt) { appendStyledSpan(buffer, getHighlightingManager().getOperationSignAttributes(), "?"); PsiType bound = wt.getBound(); if (bound != null) { String keyword = wt.isExtends() ? " extends " : " super "; appendStyledSpan(buffer, getHighlightingManager().getKeywordAttributes(), keyword); return typAnnoLength + generateType(buffer, bound, context, generateLink, useShortNames) + 1 + keyword.length(); } else { return typAnnoLength + 1; } } if (type instanceof PsiClassType) { PsiClass psiClass = null; PsiSubstitutor psiSubst = null; try { PsiClassType.ClassResolveResult result = ((PsiClassType)type).resolveGenerics(); psiClass = result.getElement(); psiSubst = result.getSubstitutor(); } catch (IndexNotReadyException e) { LOG.debug(e); } if (psiClass == null) { if (DumbService.isDumb(context.getProject())) { String text = ((PsiClassType)type).getClassName(); buffer.append(StringUtil.escapeXmlEntities(text)); return typAnnoLength + text.length(); } String canonicalText = type.getCanonicalText(); String text = getSpanForUnresolvedItem() + StringUtil.escapeXmlEntities(canonicalText) + "</span>"; buffer.append(text); return typAnnoLength + canonicalText.length(); } String qName = psiClass.getQualifiedName(); if (qName == null || psiClass instanceof PsiTypeParameter) { String typeText = useShortNames ? type.getPresentableText() : type.getCanonicalText(); appendStyledSpan( buffer, getHighlightingManager().getTypeParameterNameAttributes(), StringUtil.escapeXmlEntities(typeText)); return typAnnoLength + typeText.length(); } String name = useShortNames ? getClassNameWithOuterClasses(psiClass) : qName; int length = typAnnoLength; if (generateLink) { length += generateLink(buffer, name, null, context, false); } else { appendStyledSpan(buffer, getHighlightingManager().getClassDeclarationAttributes(psiClass), name); length += name.length(); } if (psiClass.hasTypeParameters()) { StringBuilder subst = new StringBuilder(); PsiTypeParameter[] params = psiClass.getTypeParameters(); appendStyledSpan(subst, getHighlightingManager().getOperationSignAttributes(), LT); length += 1; boolean goodSubst = true; for (int i = 0; i < params.length; i++) { PsiType t = psiSubst.substitute(params[i]); if (t == null) { goodSubst = false; break; } length += generateType(subst, t, context, generateLink, useShortNames); if (i < params.length - 1) { appendStyledSpan(subst, getHighlightingManager().getCommaAttributes(), ", "); length += 2; } } appendStyledSpan(subst, getHighlightingManager().getOperationSignAttributes(), GT); length++; if (goodSubst) { buffer.append(subst); } } return length; } if (type instanceof PsiDisjunctionType || type instanceof PsiIntersectionType) { if (!generateLink) { String canonicalText = useShortNames ? type.getPresentableText() : type.getCanonicalText(); buffer.append(StringUtil.escapeXmlEntities(canonicalText)); return typAnnoLength + canonicalText.length(); } else { String separator = type instanceof PsiDisjunctionType ? " | " : " & "; List<PsiType> componentTypes; if (type instanceof PsiIntersectionType) { componentTypes = Arrays.asList(((PsiIntersectionType)type).getConjuncts()); } else { componentTypes = ((PsiDisjunctionType)type).getDisjunctions(); } int length = typAnnoLength; for (PsiType psiType : componentTypes) { if (length > 0) { appendStyledSpan(buffer, getHighlightingManager().getOperationSignAttributes(), separator); length += 3; } length += generateType(buffer, psiType, context, true, useShortNames); } return length; } } return 0; } private static String getClassNameWithOuterClasses(@NotNull PsiClass cls) { StringBuilder result = new StringBuilder(); for (; cls != null; cls = cls.getContainingClass()) { String name = cls.getName(); if (name == null) break; if (!result.isEmpty()) result.insert(0, '.'); result.insert(0, name); } return result.toString(); } String generateTypeParameters(PsiTypeParameterListOwner owner, boolean useShortNames) { if (owner.hasTypeParameters()) { PsiTypeParameter[] parameters = owner.getTypeParameters(); StringBuilder buffer = new StringBuilder(); appendStyledSpan(buffer, getHighlightingManager().getOperationSignAttributes(), LT); PsiFile file = owner.getContainingFile(); boolean allExtends = parameters.length > 1 & ContainerUtil.and(parameters, parameter -> parameter.getExtendsList().getReferenceElements().length > 0); int indent = getIndent(!allExtends, file); if (indent > 0) { buffer.append("\n"); } for (int i = 0; i < parameters.length; i++) { buffer.append(StringUtil.repeatSymbol(' ', indent)); PsiTypeParameter p = parameters[i]; generateTypeAnnotations(buffer, p, p, true, false); appendStyledSpan( buffer, getHighlightingManager().getTypeParameterNameAttributes(), Objects.requireNonNullElse(p.getName(), CommonBundle.getErrorTitle())); PsiClassType[] refs = p.getExtendsList().getReferencedTypes(); if (refs.length > 0) { appendStyledSpan(buffer, getHighlightingManager().getKeywordAttributes(), " extends "); for (int j = 0; j < refs.length; j++) { generateType(buffer, refs[j], owner, true, useShortNames); if (j < refs.length - 1) { appendStyledSpan(buffer, getHighlightingManager().getOperationSignAttributes(), " & "); } } } if (i < parameters.length - 1) { appendStyledSpan(buffer, getHighlightingManager().getCommaAttributes(), ", "); } if (indent > 0) buffer.append("\n"); } appendStyledSpan(buffer, getHighlightingManager().getOperationSignAttributes(), GT); return buffer.toString(); } return ""; } private static <T> Pair<T, InheritDocProvider<T>> searchDocTagInOverriddenMethod(PsiMethod method, PsiClass aSuper, DocTagLocator<T> loc) { if (aSuper != null) { PsiMethod overridden = findMethodInSuperClass(method, aSuper); if (overridden != null) { T tag = loc.find(overridden, getDocComment(overridden)); if (tag != null) { return new Pair<>(tag, new InheritDocProvider<>() { @Override public Pair<T, InheritDocProvider<T>> getInheritDoc() { return findInheritDocTag(overridden, loc); } @Override public PsiClass getElement() { return aSuper; } }); } } } return null; } @Nullable private static PsiMethod findMethodInSuperClass(PsiMethod method, PsiClass aSuper) { for (PsiMethod superMethod : method.findDeepestSuperMethods()) { PsiMethod overridden = aSuper.findMethodBySignature(superMethod, false); if (overridden != null) return overridden; } return null; } @Nullable private static <T> Pair<T, InheritDocProvider<T>> searchDocTagInSupers(PsiClassType[] supers, PsiMethod method, DocTagLocator<T> loc, Set<PsiClass> visitedClasses) { try { for (PsiClassType superType : supers) { PsiClass aSuper = superType.resolve(); if (aSuper != null) { Pair<T, InheritDocProvider<T>> tag = searchDocTagInOverriddenMethod(method, aSuper, loc); if (tag != null) return tag; } } for (PsiClassType superType : supers) { PsiClass aSuper = superType.resolve(); if (aSuper != null && visitedClasses.add(aSuper)) { Pair<T, InheritDocProvider<T>> tag = findInheritDocTagInClass(method, aSuper, loc, visitedClasses); if (tag != null) { return tag; } } } } catch (IndexNotReadyException e) { LOG.debug(e); } return null; } private static <T> Pair<T, InheritDocProvider<T>> findInheritDocTagInClass(PsiMethod aMethod, PsiClass aClass, DocTagLocator<T> loc, Set<PsiClass> visitedClasses) { if (aClass == null) return null; Pair<T, InheritDocProvider<T>> delegate = findInheritDocTagInDelegate(aMethod, loc); if (delegate != null) return delegate; if (aClass instanceof PsiAnonymousClass) { return searchDocTagInSupers(new PsiClassType[]{((PsiAnonymousClass)aClass).getBaseClassType()}, aMethod, loc, visitedClasses); } PsiClassType[] implementsTypes = aClass.getImplementsListTypes(); Pair<T, InheritDocProvider<T>> tag = searchDocTagInSupers(implementsTypes, aMethod, loc, visitedClasses); if (tag != null) return tag; PsiClassType[] extendsTypes = aClass.getExtendsListTypes(); return searchDocTagInSupers(extendsTypes, aMethod, loc, visitedClasses); } @Nullable private static <T> Pair<T, InheritDocProvider<T>> findInheritDocTagInDelegate(PsiMethod method, DocTagLocator<T> loc) { PsiMethod delegateMethod = findDelegateMethod(method); if (delegateMethod == null) return null; PsiClass containingClass = delegateMethod.getContainingClass(); if (containingClass == null) return null; T tag = loc.find(delegateMethod, getDocComment(delegateMethod)); if (tag == null) return null; return Pair.create(tag, new InheritDocProvider<>() { @Override public Pair<T, InheritDocProvider<T>> getInheritDoc() { return findInheritDocTag(delegateMethod, loc); } @Override public PsiClass getElement() { return containingClass; } }); } @Nullable private static PsiMethod findDelegateMethod(@NotNull PsiMethod method) { PsiDocCommentOwner delegate = DocumentationDelegateProvider.findDocumentationDelegate(method); return delegate instanceof PsiMethod ? (PsiMethod)delegate : null; } /** * Finds the most specific applicable JavaDoc tag for the given method parameter * * @param method method for which parameter the JavaDoc tag is searched for * @param index parameter index * @return the most specific applicable JavaDoc tag if found, {@code null} otherwise */ @Nullable public static PsiDocTag findInheritDocTag(PsiMethod method, int index) { Pair<PsiDocTag, InheritDocProvider<PsiDocTag>> pair = findInheritDocTag(method, parameterLocator(index)); return pair != null ? pair.first : null; } @Nullable private static <T> Pair<T, InheritDocProvider<T>> findInheritDocTag(PsiMethod method, DocTagLocator<T> loc) { PsiClass aClass = method.getContainingClass(); return aClass != null ? findInheritDocTagInClass(method, aClass, loc, new HashSet<>()) : null; } private static final class ParamInfo { private final String name; private final String presentableName; private final PsiDocTag docTag; private final InheritDocProvider<PsiDocTag> inheritDocTagProvider; private ParamInfo(String paramName, String presentableName, PsiDocTag tag, InheritDocProvider<PsiDocTag> provider) { name = paramName; this.presentableName = presentableName; docTag = tag; inheritDocTagProvider = provider; } private ParamInfo( String paramName, String presentableName, @NotNull Pair<PsiDocTag, InheritDocProvider<PsiDocTag>> tagWithInheritProvider ) { this(paramName, presentableName, tagWithInheritProvider.first, tagWithInheritProvider.second); } } private static class ReturnTagLocator implements DocTagLocator<PsiDocTag> { @Override public PsiDocTag find(PsiDocCommentOwner owner, PsiDocComment comment) { if (comment != null) { PsiDocTag returnTag = comment.findTagByName(RETURN_TAG); if (returnTag != null) { return returnTag; } if (PsiUtil.isLanguageLevel16OrHigher(comment)) { for (PsiElement child : comment.getChildren()) { if (child instanceof PsiDocTag && RETURN_TAG.equals(((PsiDocTag)child).getName())) { return (PsiDocTag)child; } } } } return null; } } private class MyVisitor extends JavaElementVisitor { private final StringBuilder myBuffer; MyVisitor(@NotNull StringBuilder buffer) { myBuffer = buffer; } @Override public void visitNewExpression(@NotNull PsiNewExpression expression) { appendStyledSpan(myBuffer, getHighlightingManager().getKeywordAttributes(), "new "); PsiType type = expression.getType(); if (type != null) { generateType(myBuffer, type, expression); } PsiExpression[] dimensions = expression.getArrayDimensions(); if (dimensions.length > 0) { int closeBracketIndex = myBuffer.length() - 1; while (closeBracketIndex > 0 && myBuffer.charAt(closeBracketIndex) != ']') closeBracketIndex--; LOG.assertTrue(myBuffer.charAt(closeBracketIndex) == ']'); myBuffer.setLength(closeBracketIndex); for (int i = 0; i < dimensions.length; i++) { PsiExpression dimension = dimensions[i]; dimension.accept(this); if (i + 1 != dimensions.length) appendStyledSpan(myBuffer, getHighlightingManager().getCommaAttributes(), ", "); } appendStyledSpan(myBuffer, getHighlightingManager().getBracketsAttributes(), "]"); } else { expression.acceptChildren(this); } } @Override public void visitExpressionList(@NotNull PsiExpressionList list) { appendStyledSpan(myBuffer, getHighlightingManager().getParenthesesAttributes(), "("); String separator = ", "; PsiExpression[] expressions = list.getExpressions(); for (int i = 0; i < expressions.length; i++) { PsiExpression expression = expressions[i]; expression.accept(this); if (i + 1 != expressions.length) appendStyledSpan(myBuffer, getHighlightingManager().getCommaAttributes(), separator); } appendStyledSpan(myBuffer, getHighlightingManager().getParenthesesAttributes(), ")"); } @Override public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) { appendStyledSpan( myBuffer, getHighlightingManager().getMethodCallAttributes(), StringUtil.escapeXmlEntities(expression.getMethodExpression().getText())); expression.getArgumentList().accept(this); } @Override public void visitExpression(@NotNull PsiExpression expression) { appendHighlightedByLexerAndEncodedAsHtmlCodeSnippet( doHighlightSignatures(), myBuffer, expression.getProject(), expression.getLanguage(), expression.getText()); } @Override public void visitReferenceExpression(@NotNull PsiReferenceExpression expression) { appendHighlightedByLexerAndEncodedAsHtmlCodeSnippet( doHighlightSignatures(), myBuffer, expression.getProject(), expression.getLanguage(), expression.getText()); } } public void generateTooltipAnnotations(PsiModifierListOwner owner, @Nls StringBuilder buffer) { generateAnnotations(buffer, owner, SignaturePlace.ToolTip, true, true, false); } private enum SignaturePlace { Javadoc, ToolTip } }