com.intellij.lang.jsgraphql.ide.documentation.JSGraphQLDocumentationProvider.java Source code

Java tutorial

Introduction

Here is the source code for com.intellij.lang.jsgraphql.ide.documentation.JSGraphQLDocumentationProvider.java

Source

/**
 *  Copyright (c) 2015-present, Jim Kynde Meyer
 *  All rights reserved.
 *
 *  This source code is licensed under the MIT license found in the
 *  LICENSE file in the root directory of this source tree.
 */
package com.intellij.lang.jsgraphql.ide.documentation;

import com.google.common.collect.Lists;
import com.intellij.codeInsight.documentation.DocumentationManagerProtocol;
import com.intellij.lang.documentation.DocumentationProviderEx;
import com.intellij.lang.jsgraphql.endpoint.psi.JSGraphQLEndpointDocumentationAware;
import com.intellij.lang.jsgraphql.ide.injection.JSGraphQLLanguageInjectionUtil;
import com.intellij.lang.jsgraphql.languageservice.JSGraphQLNodeLanguageServiceClient;
import com.intellij.lang.jsgraphql.languageservice.api.TokenDocumentationResponse;
import com.intellij.lang.jsgraphql.languageservice.api.TypeDocumentationResponse;
import com.intellij.lang.jsgraphql.psi.JSGraphQLAttributePsiElement;
import com.intellij.lang.jsgraphql.psi.JSGraphQLFragmentDefinitionPsiElement;
import com.intellij.lang.jsgraphql.psi.JSGraphQLNamedPropertyPsiElement;
import com.intellij.lang.jsgraphql.psi.JSGraphQLNamedPsiElement;
import com.intellij.lang.jsgraphql.psi.JSGraphQLNamedTypePsiElement;
import com.intellij.lang.jsgraphql.schema.ide.project.JSGraphQLSchemaLanguageProjectService;
import com.intellij.lang.jsgraphql.schema.psi.JSGraphQLSchemaFile;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.ui.GuiUtils;
import com.intellij.util.containers.ContainerUtil;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.awt.*;
import java.util.List;

public class JSGraphQLDocumentationProvider extends DocumentationProviderEx {

    public final static String GRAPHQL_DOC_PREFIX = "GraphQL";
    public final static String GRAPHQL_DOC_TYPE = "Type";

    @Nullable
    @Override
    public String getQuickNavigateInfo(PsiElement element, PsiElement originalElement) {
        if (isDocumentationSupported(element)) {
            return createQuickNavigateDocumentation(resolveDocumentationElement(element, originalElement), false);
        }
        return null;
    }

    @Override
    public String generateDoc(PsiElement element, @Nullable PsiElement originalElement) {
        if (isDocumentationSupported(element)) {
            return createQuickNavigateDocumentation(resolveDocumentationElement(element, originalElement), true);
        }
        return null;
    }

    @Override
    public PsiElement getDocumentationElementForLink(PsiManager psiManager, String link, PsiElement context) {
        if (link.startsWith(GRAPHQL_DOC_PREFIX)) {
            return new JSGraphQLDocumentationPsiElement(context, link);
        }
        return super.getDocumentationElementForLink(psiManager, link, context);
    }

    private boolean isDocumentationSupported(PsiElement element) {
        final PsiFile file = element.getContainingFile();
        if (file instanceof JSGraphQLSchemaFile) {
            return JSGraphQLSchemaLanguageProjectService.isProjectSchemaFile(file.getVirtualFile());
        }
        return true;
    }

    // ensures that the built-in schema info that points to the schema file are sent to the doc methods as the originalElement
    private PsiElement resolveDocumentationElement(PsiElement element, PsiElement originalElement) {
        if (element instanceof JSGraphQLSchemaFile) {
            if (originalElement instanceof JSGraphQLNamedPsiElement) {
                return originalElement;
            } else if (originalElement.getParent() instanceof JSGraphQLNamedPsiElement) {
                // the doc can be invoked on the leaf node, so move up to the parent which contains the type/field name
                return originalElement.getParent();
            }
        }
        return element;
    }

    @Nullable
    private String createQuickNavigateDocumentation(PsiElement element, boolean fullDocumentation) {
        if (!isDocumentationSupported(element)) {
            return null;
        }
        if (element instanceof JSGraphQLDocumentationPsiElement) {
            JSGraphQLDocumentationPsiElement docElement = (JSGraphQLDocumentationPsiElement) element;
            return getTypeDocumentation(docElement);
        }

        if (element instanceof JSGraphQLNamedPsiElement) {

            final Project project = element.getProject();

            if (element instanceof JSGraphQLNamedPropertyPsiElement) {
                final JSGraphQLNamedPropertyPsiElement propertyPsiElement = (JSGraphQLNamedPropertyPsiElement) element;
                String typeName = JSGraphQLSchemaLanguageProjectService.getService(project)
                        .getTypeName(propertyPsiElement);
                if (typeName == null) {
                    // in structure view we don't get the schema psi element, so try to find it using the reference
                    final PsiReference reference = propertyPsiElement.getReference();
                    if (reference != null) {
                        final PsiElement schemaReference = reference.resolve();
                        if (schemaReference instanceof JSGraphQLNamedPropertyPsiElement) {
                            typeName = JSGraphQLSchemaLanguageProjectService.getService(project)
                                    .getTypeName((JSGraphQLNamedPropertyPsiElement) schemaReference);
                        } else if (schemaReference instanceof JSGraphQLSchemaFile) {
                            // field belongs to a built in type which isn't shown in the schema file
                            // hence our reference to the file -- see JSGraphQLSchemaLanguageProjectService.getReference()
                            final String buffer = element.getContainingFile().getText();
                            final LogicalPosition pos = getTokenPos(buffer, element);
                            final String environment = JSGraphQLLanguageInjectionUtil
                                    .getEnvironment(element.getContainingFile());
                            final TokenDocumentationResponse tokenDocumentation = JSGraphQLNodeLanguageServiceClient
                                    .getTokenDocumentation(buffer, pos.line, pos.column, project, environment);
                            if (tokenDocumentation != null) {
                                String doc = "";
                                if (tokenDocumentation.getDescription() != null) {
                                    if (tokenDocumentation.getType() != null
                                            && !JSGraphQLSchemaLanguageProjectService.SCALAR_TYPES
                                                    .contains(tokenDocumentation.getType())) {
                                        doc += "<div style=\"margin-bottom: 4px\">"
                                                + tokenDocumentation.getDescription() + "</div>";
                                    }
                                }
                                doc += "<code>" + element.getText() + ": "
                                        + getTypeHyperLink(tokenDocumentation.getType()) + "</code>";
                                return getDocTemplate(fullDocumentation).replace("${body}", doc);
                            }
                        }
                    }
                }
                if (typeName != null) {
                    final TokenDocumentationResponse fieldDocumentation = JSGraphQLNodeLanguageServiceClient
                            .getFieldDocumentation(typeName, propertyPsiElement.getName(), project);
                    if (fieldDocumentation != null) {
                        String doc = "";
                        if (fieldDocumentation.getDescription() != null) {
                            doc += "<div style=\"margin-bottom: 4px\">"
                                    + StringEscapeUtils.escapeHtml(fieldDocumentation.getDescription()) + "</div>";
                        }
                        String typeNameOrLink = fullDocumentation ? getTypeHyperLink(typeName) : typeName;
                        doc += "<code>" + typeNameOrLink + " <b>" + element.getText() + "</b>: "
                                + getTypeHyperLink(fieldDocumentation.getType()) + "</code>";
                        return getDocTemplate(fullDocumentation).replace("${body}", doc);
                    }
                }
            } else if (element instanceof JSGraphQLNamedTypePsiElement) {
                if (((JSGraphQLNamedTypePsiElement) element).isDefinition()) {
                    if (element.getParent() instanceof JSGraphQLFragmentDefinitionPsiElement) {
                        // the named type represents the name of a fragment definition,
                        // so return doc along the lines of 'fragment MyFrag on SomeType'
                        final StringBuilder doc = new StringBuilder("<code>fragment ");
                        doc.append("<b>").append(element.getText()).append("</b>");
                        final JSGraphQLNamedTypePsiElement fragmentType = PsiTreeUtil.getNextSiblingOfType(element,
                                JSGraphQLNamedTypePsiElement.class);
                        if (fragmentType != null) {
                            doc.append(" on ").append(getTypeHyperLink(fragmentType.getName()));
                        }
                        doc.append("</code>");
                        return getDocTemplate(fullDocumentation).replace("${body}", doc);
                    }
                }
                if (fullDocumentation) {
                    final PsiManager psiManager = PsiManager.getInstance(project);
                    final String link = GRAPHQL_DOC_PREFIX + "/" + GRAPHQL_DOC_TYPE + "/" + element.getText();
                    final PsiElement documentationElement = getDocumentationElementForLink(psiManager, link,
                            element);
                    if (documentationElement instanceof JSGraphQLDocumentationPsiElement) {
                        return getTypeDocumentation((JSGraphQLDocumentationPsiElement) documentationElement);
                    }
                }
                TypeDocumentationResponse typeDocumentation = JSGraphQLNodeLanguageServiceClient
                        .getTypeDocumentation(element.getText(), project);
                if (typeDocumentation != null) {
                    String doc = "";
                    if (typeDocumentation.description != null) {
                        doc += "<div style=\"margin-bottom: 4px\">"
                                + StringEscapeUtils.escapeHtml(typeDocumentation.description) + "</div>";
                    }
                    doc += "<code><b>" + element.getText() + "</b>";
                    if (!ContainerUtil.isEmpty(typeDocumentation.interfaces)) {
                        doc += ": ";
                        for (int i = 0; i < typeDocumentation.interfaces.size(); i++) {
                            if (i > 0) {
                                doc += ", ";
                            }
                            doc += getTypeHyperLink(typeDocumentation.interfaces.get(i));
                        }
                    }
                    doc += "</code>";
                    return getDocTemplate(fullDocumentation).replace("${body}", doc);
                }
            } else if (element instanceof JSGraphQLAttributePsiElement) {
                String doc = "";
                PsiElement prevLeaf = PsiTreeUtil.prevLeaf(element);
                String documentation = "";
                while (prevLeaf instanceof PsiWhiteSpace || prevLeaf instanceof PsiComment) {
                    documentation = StringUtils.removeStart(prevLeaf.getText(), "# ") + documentation;
                    prevLeaf = PsiTreeUtil.prevLeaf(prevLeaf);
                }
                documentation = documentation.trim();
                if (StringUtils.isNotBlank(documentation)) {
                    doc += "<div style=\"margin-bottom: 4px\">" + StringEscapeUtils.escapeHtml(documentation)
                            + "</div>";
                }
                doc += "<code>" + element.getText();
                PsiElement nextLeaf = PsiTreeUtil.nextLeaf(element);
                // include the attribute type (stopping at newline, ",", and ")")
                while (nextLeaf != null && !nextLeaf.getText().contains("\n") && !nextLeaf.getText().contains(",")
                        && !nextLeaf.getText().contains(")")) {
                    doc += nextLeaf.getText();
                    nextLeaf = PsiTreeUtil.nextLeaf(nextLeaf);
                }
                doc += "</code>";
                return getDocTemplate(fullDocumentation).replace("${body}", doc);
            }

        } else if (element instanceof JSGraphQLEndpointDocumentationAware) {
            final JSGraphQLEndpointDocumentationAware documentationAware = (JSGraphQLEndpointDocumentationAware) element;
            final String documentation = documentationAware.getDocumentation(fullDocumentation);
            String doc = "";
            if (documentation != null) {
                doc += "<div style=\"margin-bottom: 4px\">" + StringEscapeUtils.escapeHtml(documentation)
                        + "</div>";
            }
            doc += "<code>" + documentationAware.getDeclaration() + "</code>";
            return getDocTemplate(fullDocumentation).replace("${body}", doc);
        }

        return null;
    }

    private LogicalPosition getTokenPos(String buffer, PsiElement element) {
        int line = 0;
        int column = 0;
        int targetPos = element.getTextOffset();
        int pos = 0;
        while (pos < targetPos) {
            char c = buffer.charAt(pos);
            switch (c) {
            case '\n':
                line++;
                column = 0;
                break;
            default:
                column++;
            }
            pos++;
        }
        return new LogicalPosition(line, column);
    }

    private String getDocTemplate(boolean fullDocumentation) {
        String doc = "<body style=\"margin: 0\">";
        if (fullDocumentation) {
            doc += createIndex(null);
        }
        doc += "<div style=\"margin: 4px 8px 4px 8px;\">${body}</div></body>";
        return doc;
    }

    private String getTypeDocumentation(JSGraphQLDocumentationPsiElement docElement) {

        final EditorColorsScheme globalScheme = EditorColorsManager.getInstance().getGlobalScheme();
        final Color borderColor = globalScheme.getDefaultForeground();
        //final TextAttributes attributes = EditorColorsManager.getInstance().getGlobalScheme().getAttributes(JSGraphQLSyntaxHighlighter.PROPERTY).clone();

        final StringBuilder sb = new StringBuilder();
        TypeDocumentationResponse typeDocumentation = JSGraphQLNodeLanguageServiceClient
                .getTypeDocumentation(docElement.getType(), docElement.getProject());
        if (typeDocumentation != null) {
            sb.append("<html style=\"margin: 0;\"><body style=\"margin: 0;\">");
            sb.append(createIndex(borderColor));
            sb.append("<div style=\"margin: 4px 8px 4px 8px;\"");
            sb.append("<h1 style=\"margin: 0 0 8px 0; font-size: 200%\">").append(docElement.getType())
                    .append("</h1>");
            if (typeDocumentation.description != null) {
                sb.append("<div>").append(StringEscapeUtils.escapeHtml(typeDocumentation.description))
                        .append("</div><br>");
            }
            if (typeDocumentation.implementations != null && !typeDocumentation.implementations.isEmpty()) {
                sb.append(getSection(borderColor, "IMPLEMENTATIONS"));
                for (String implementation : typeDocumentation.implementations) {
                    sb.append("<div><code>").append(getTypeHyperLink(implementation)).append("</code></div>");
                }
                sb.append("<br><br>");
            }
            if (typeDocumentation.interfaces != null && !typeDocumentation.interfaces.isEmpty()) {
                sb.append(getSection(borderColor, "IMPLEMENTS"));
                for (String intf : typeDocumentation.interfaces) {
                    sb.append("<div><code>").append(getTypeHyperLink(intf)).append("</code></div>");
                }
                sb.append("<br><br>");
            }
            if (typeDocumentation.fields != null && !typeDocumentation.fields.isEmpty()) {
                sb.append(getSection(borderColor, "FIELDS"));
                for (TypeDocumentationResponse.Field field : typeDocumentation.fields) {
                    sb.append("<div style=\"margin-bottom: 4px\">- <code><b>").append(field.name)
                            .append("</b></code>");
                    if (field.args != null && !field.args.isEmpty()) {
                        sb.append("<code>(");
                        List<String> fieldArgs = Lists.newArrayListWithCapacity(field.args.size());
                        for (TypeDocumentationResponse.FieldArgument arg : field.args) {
                            fieldArgs.add(arg.name + ": " + getTypeHyperLink(arg.type));
                        }
                        sb.append(StringUtils.join(fieldArgs, ", "));
                        sb.append(")");
                    }
                    sb.append(": ").append(getTypeHyperLink(field.type));
                    if (field.description != null) {
                        sb.append("<div style=\"margin-left: 8px; margin-top: 4px; margin-bottom: 4px;\">");
                        sb.append(StringEscapeUtils.escapeHtml(field.description)).append("</div>");
                    }
                    sb.append("</code></div>");
                }
                sb.append("<br><br>");
            }
            sb.append("</div>");
            sb.append("</html></body>");
            return sb.toString();
        }
        return null;
    }

    @NotNull
    private String createIndex(Color borderColor) {
        if (borderColor == null) {
            final EditorColorsScheme globalScheme = EditorColorsManager.getInstance().getGlobalScheme();
            borderColor = globalScheme.getDefaultForeground();
        }
        return "<div style=\"border-bottom: 1px outset " + GuiUtils.colorToHex(borderColor)
                + "; padding: 0 2px 8px 0; margin-bottom: 16px; text-align: right;\">Index: "
                + getTypeHyperLink("Query", "Queries") + " - " + getTypeHyperLink("Mutation", "Mutations")
                + "</div>";
    }

    @NotNull
    private String getSection(Color borderColor, String label) {
        return "<div style=\"margin-bottom: 8px; border-bottom: 1px solid; padding-bottom: 4px; "
                + GuiUtils.colorToHex(borderColor) + ";\">" + label + "</div>";
    }

    private String getTypeHyperLink(String type) {
        return getTypeHyperLink(type, null);
    }

    private String getTypeHyperLink(String type, String text) {
        // TODO: Investigate the use of colors like https://upsource.jetbrains.com/idea-community/file/1731d054af4ca27aa827c03929e27eeb0e6a8366/plugins%2Fproperties%2Fsrc%2Fcom%2Fintellij%2Flang%2Fproperties%2FPropertiesDocumentationProvider.java
        if (type == null)
            return "";
        final String typeName = type.replaceAll("[\\[\\]!]", "");
        final StringBuilder sb = new StringBuilder().append(type.startsWith("[") ? "[" : "")
                .append("<a style=\"\" href=\"").append(getTypeLink(typeName)).append("\">")
                .append(text == null ? typeName : text).append("</a>").append(type.contains("!") ? "!" : "")
                .append(type.endsWith("]") ? "]" : "").append(type.endsWith("]!") ? "]!" : "");
        return sb.toString();
    }

    private String getTypeLink(String typeName) {
        return new StringBuilder(DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL).append(GRAPHQL_DOC_PREFIX)
                .append("/").append(GRAPHQL_DOC_TYPE).append("/").append(typeName).toString();
    }

}