package com.jetbrains.lang.dart.ide.documentation;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.lang.dart.DartTokenTypesSets;
import com.jetbrains.lang.dart.psi.*;
import com.jetbrains.lang.dart.util.DartGenericSpecialization;
import com.jetbrains.lang.dart.util.DartPresentableUtil;
import com.jetbrains.lang.dart.util.DartResolveUtil;
import com.jetbrains.lang.dart.util.UsefulPsiTreeUtil;
import com.petebevin.markdown.MarkdownProcessor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class DartDocUtil {
public static final String SINGLE_LINE_DOC_COMMENT = "///";
public static String generateDoc(final PsiElement element) {
if (!(element instanceof DartComponent) && !(element.getParent() instanceof DartComponent)) {
return null;
}
final DartComponent namedComponent = (DartComponent)(element instanceof DartComponent ? element : element.getParent());
final String signatureHtml;
{
final StringBuilder builder = new StringBuilder();
appendSignature(namedComponent, builder);
signatureHtml = builder.toString();
}
final String containingLibraryName;
final PsiFile file = element.getContainingFile();
if (file != null) {
containingLibraryName = DartResolveUtil.getLibraryName(file);
}
else {
containingLibraryName = null;
}
final String containingClassDescription;
final DartClass dartClass = PsiTreeUtil.getParentOfType(namedComponent, DartClass.class);
if (dartClass != null) {
final StringBuilder builder = new StringBuilder();
builder.append(dartClass.getName());
appendTypeParams(builder, dartClass.getTypeParameters());
containingClassDescription = builder.toString();
}
else {
containingClassDescription = null;
}
final String docText = getDocumentationText(namedComponent);
return generateDoc(signatureHtml, true, docText, containingLibraryName, containingClassDescription, null, null, false);
}
public static String generateDoc(@Nullable final String signature,
final boolean signatureIsHtml,
@Nullable final String docText,
@Nullable final String containingLibraryName,
@Nullable final String containingClassDescription,
@Nullable final String staticType,
@Nullable final String propagatedType,
final boolean compactPresentation) {
final boolean hasContainingLibraryName = !StringUtil.isEmpty(containingLibraryName);
final boolean hasContainingClassDescription = !StringUtil.isEmpty(containingClassDescription);
final boolean hasStaticType = !StringUtil.isEmpty(staticType);
final boolean hasPropagatedType = !StringUtil.isEmpty(propagatedType);
// generate
final StringBuilder builder = new StringBuilder();
builder.append("<code>");
if (signature != null) {
if (signatureIsHtml) {
builder.append(signature);
}
else {
builder.append(StringUtil.escapeXml(signature));
}
builder.append("<br>");
}
if (hasContainingLibraryName || hasContainingClassDescription) {
builder.append("<br>");
if (hasContainingLibraryName) {
builder.append("<b>Containing library:</b> ");
builder.append(StringUtil.escapeXml(containingLibraryName));
builder.append("<br>");
}
if (hasContainingClassDescription) {
builder.append("<b>Containing class:</b> ");
builder.append(StringUtil.escapeXml(containingClassDescription));
builder.append("<br>");
}
}
if (hasStaticType || hasPropagatedType) {
if (!compactPresentation) {
builder.append("<br>");
}
if (hasStaticType) {
builder.append("<b>Static type:</b> ");
builder.append(StringUtil.escapeXml(staticType));
builder.append("<br>");
}
if (hasPropagatedType) {
builder.append("<b>Propagated type:</b> ");
builder.append(StringUtil.escapeXml(propagatedType));
builder.append("<br>");
}
}
builder.append("</code>\n");
if (docText != null) {
final MarkdownProcessor processor = new MarkdownProcessor();
builder.append(processor.markdown(docText.trim()));
}
// done
return builder.toString().trim();
}
@Nullable
public static String getSignature(@NotNull PsiElement element) {
if (!(element instanceof DartComponent)) {
element = element.getParent();
}
if (element instanceof DartComponent) {
final StringBuilder sb = new StringBuilder();
appendSignature((DartComponent)element, sb);
if (sb.length() > 0) return sb.toString();
}
return null;
}
private static void appendSignature(final DartComponent namedComponent, final StringBuilder builder) {
if (namedComponent instanceof DartClass) {
appendClassSignature(builder, (DartClass)namedComponent);
}
else if (namedComponent instanceof DartFunctionDeclarationWithBodyOrNative) {
appendFunctionSignature(builder, namedComponent, ((DartFunctionDeclarationWithBodyOrNative)namedComponent).getReturnType());
}
else if (namedComponent instanceof DartFunctionTypeAlias) {
builder.append("typedef ");
appendFunctionSignature(builder, namedComponent, ((DartFunctionTypeAlias)namedComponent).getReturnType());
}
else if (namedComponent.isConstructor()) {
appendConstructorSignature(builder, namedComponent, PsiTreeUtil.getParentOfType(namedComponent, DartClass.class));
}
else if (namedComponent instanceof DartMethodDeclaration) {
appendFunctionSignature(builder, namedComponent, ((DartMethodDeclaration)namedComponent).getReturnType());
}
else if (namedComponent instanceof DartVarAccessDeclaration) {
appendVariableSignature(builder, namedComponent, ((DartVarAccessDeclaration)namedComponent).getType());
}
else if (namedComponent instanceof DartGetterDeclaration) {
builder.append("get ");
appendFunctionSignature(builder, namedComponent, ((DartGetterDeclaration)namedComponent).getReturnType());
}
else if (namedComponent instanceof DartSetterDeclaration) {
builder.append("set ");
appendFunctionSignature(builder, namedComponent, ((DartSetterDeclaration)namedComponent).getReturnType());
}
else if (namedComponent instanceof DartEnumConstantDeclaration) {
builder.append(((DartEnumDefinition)namedComponent.getParent()).getName()).append(" ");
builder.append("<b>").append(namedComponent.getName()).append("</b>");
}
}
@Nullable
private static String getDocumentationText(final DartComponent dartComponent) {
// PSI is not perfect currently, doc comment may be not part of the corresponding DartComponent element, so docs are searched for in several places:
// - direct child of this DartComponent
// - previous sibling (or previous sibling of parent element if this element is first child of its parent DartClassMembers)
// Consequent line doc comments (///) are joined
// 1. Look for multiline doc comment as direct child
final DartDocComment multilineComment = PsiTreeUtil.getChildOfType(dartComponent, DartDocComment.class);
if (multilineComment != null) return getMultilineDocCommentText(multilineComment);
// 2. Look for single line doc comments as direct children
final PsiComment[] childComments = PsiTreeUtil.getChildrenOfType(dartComponent, PsiComment.class);
if (childComments != null) {
//
final String docText = getSingleLineDocCommentsText(childComments);
if (docText != null) return docText;
}
PsiElement anchorElement = dartComponent;
final PsiElement parent = dartComponent.getParent();
if (parent instanceof DartClassMembers && parent.getFirstChild() == dartComponent ||
dartComponent instanceof DartVarAccessDeclaration) {
anchorElement = parent;
}
// 3. Look for multiline doc comment or line doc comments as previous siblings
final List<PsiComment> siblingComments = new ArrayList<>();
PsiElement previous = anchorElement;
while ((previous = UsefulPsiTreeUtil.getPrevSiblingSkipWhiteSpaces(previous, true)) instanceof PsiComment) {
if (previous instanceof DartDocComment) {
return getMultilineDocCommentText((DartDocComment)previous);
}
siblingComments.add(0, (PsiComment)previous);
}
if (!siblingComments.isEmpty()) {
return getSingleLineDocCommentsText(siblingComments.toArray(new PsiComment[siblingComments.size()]));
}
return null;
}
@NotNull
private static String getMultilineDocCommentText(final @NotNull DartDocComment docComment) {
final StringBuilder buf = new StringBuilder();
boolean afterAsterisk = false;
for (PsiElement child = docComment.getFirstChild(); child != null; child = child.getNextSibling()) {
final IElementType elementType = child.getNode().getElementType();
final String text = child.getText();
if (elementType != DartTokenTypesSets.MULTI_LINE_DOC_COMMENT_START &&
elementType != DartTokenTypesSets.DOC_COMMENT_LEADING_ASTERISK &&
elementType != DartTokenTypesSets.MULTI_LINE_COMMENT_END) {
int newLinesCount;
if (child instanceof PsiWhiteSpace && (newLinesCount = StringUtil.countNewLines(text)) > 0) {
buf.append(StringUtil.repeatSymbol('\n', newLinesCount));
}
else {
if (afterAsterisk && text.startsWith(" ")) {
buf.append(text.substring(1));
}
else {
buf.append(text);
}
}
}
afterAsterisk = elementType == DartTokenTypesSets.DOC_COMMENT_LEADING_ASTERISK;
}
return buf.toString();
}
@Nullable
private static String getSingleLineDocCommentsText(final @NotNull PsiComment[] comments) {
StringBuilder buf = null;
for (PsiComment comment : comments) {
if (comment.getNode().getElementType() == DartTokenTypesSets.SINGLE_LINE_DOC_COMMENT) {
if (buf == null) {
buf = new StringBuilder();
}
else {
buf.append('\n');
}
final String text = comment.getText();
if (text.startsWith(SINGLE_LINE_DOC_COMMENT + " ")) {
buf.append(StringUtil.trimStart(text, SINGLE_LINE_DOC_COMMENT + " "));
}
else {
buf.append(StringUtil.trimStart(text, SINGLE_LINE_DOC_COMMENT));
}
}
}
return buf == null ? null : buf.toString();
}
private static void appendConstructorSignature(final StringBuilder builder, final DartComponent component, final DartClass dartClass) {
if (component instanceof DartNamedConstructorDeclaration || component instanceof DartFactoryConstructorDeclaration) {
builder.append("<b>").append(dartClass.getName()).append(".</b>");
}
appendFunctionSignature(builder, component, dartClass.getName());
}
private static void appendVariableSignature(@NotNull final StringBuilder builder,
@NotNull final DartComponent component,
@Nullable final DartType type) {
if (type == null) {
builder.append("var ");
}
else {
appendDartType(builder, type);
builder.append(" ");
}
builder.append("<b>").append(component.getName()).append("</b>");
}
private static void appendDartType(@NotNull final StringBuilder builder, @NotNull final DartType type) {
final DartReferenceExpression expression = type.getReferenceExpression();
if (expression != null) {
builder.append(StringUtil.escapeXml(expression.getText()));
appendTypeArguments(builder, type);
}
else {
builder.append("Function"); // functionType
}
}
private static void appendTypeArguments(final @NotNull StringBuilder builder, final @NotNull DartType type) {
final DartTypeArguments typeArguments = type.getTypeArguments();
if (typeArguments != null) {
final DartTypeList typeList = typeArguments.getTypeList();
final List<DartType> children = typeList.getTypeList();
if (!children.isEmpty()) {
builder.append("<");
appendDartTypeList(builder, children);
builder.append(">");
}
}
}
private static void appendClassSignature(final StringBuilder builder, final DartClass dartClass) {
if (dartClass.isEnum()) {
builder.append("enum <b>").append(dartClass.getName()).append("</b>");
return;
}
if (dartClass.isAbstract()) {
builder.append("abstract ");
}
builder.append("class <b>").append(dartClass.getName()).append("</b>");
appendTypeParams(builder, dartClass.getTypeParameters());
final List<DartType> mixins = dartClass.getMixinsList();
final DartType superClass = dartClass.getSuperClass();
if (superClass != null) {
builder.append(" extends ").append(StringUtil.escapeXml(superClass.getText()));
}
if (!mixins.isEmpty()) {
builder.append(" with ");
appendDartTypeList(builder, mixins);
}
final List<DartType> implementsList = dartClass.getImplementsList();
if (!implementsList.isEmpty()) {
builder.append(" implements ");
appendDartTypeList(builder, implementsList);
}
}
private static void appendDartTypeList(final StringBuilder builder, final List<DartType> dartTypes) {
for (Iterator<DartType> iter = dartTypes.iterator(); iter.hasNext(); ) {
appendDartType(builder, iter.next());
if (iter.hasNext()) {
builder.append(", ");
}
}
}
private static void appendTypeParams(final StringBuilder builder, final DartTypeParameters typeParameters) {
if (typeParameters != null) {
final List<DartTypeParameter> parameters = typeParameters.getTypeParameterList();
if (!parameters.isEmpty()) {
builder.append("<");
for (Iterator<DartTypeParameter> iter = parameters.iterator(); iter.hasNext(); ) {
builder.append(iter.next().getText());
if (iter.hasNext()) {
builder.append(", ");
}
}
builder.append(">");
}
}
}
private static void appendFunctionSignature(final StringBuilder builder, final DartComponent function, final DartReturnType returnType) {
final String returnString =
returnType == null ? "dynamic" : StringUtil.escapeXml(DartPresentableUtil.buildTypeText(null, returnType, null));
appendFunctionSignature(builder, function, returnString);
}
private static void appendFunctionSignature(final StringBuilder builder, final DartComponent function, final String returnType) {
builder.append("<b>").append(function.getName()).append("</b>");
if (!function.isGetter()) {
builder.append('(');
builder.append(StringUtil.escapeXml(
DartPresentableUtil.getPresentableParameterList(function, new DartGenericSpecialization(), true, true, false)));
builder.append(')');
}
builder.append(' ');
builder.append(DartPresentableUtil.RIGHT_ARROW);
builder.append(' ');
builder.append(returnType);
}
}