/* * Copyright 2013-2016 Sergey Ignatov, Alexander Zolotov, Florin Patan * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.goide; import com.goide.editor.GoParameterInfoHandler; import com.goide.project.GoVendoringUtil; import com.goide.psi.*; import com.goide.psi.impl.GoCType; import com.goide.psi.impl.GoLightType; import com.goide.psi.impl.GoPsiImplUtil; import com.goide.sdk.GoPackageUtil; import com.goide.sdk.GoSdkUtil; import com.goide.stubs.index.GoAllPrivateNamesIndex; import com.goide.stubs.index.GoAllPublicNamesIndex; import com.goide.stubs.index.GoIdFilter; import com.goide.util.GoUtil; import com.intellij.codeInsight.documentation.DocumentationManagerProtocol; import com.intellij.lang.ASTNode; import com.intellij.lang.documentation.AbstractDocumentationProvider; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.stubs.StubIndex; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.Function; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.indexing.IdFilter; import com.intellij.xml.util.XmlStringUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Collections; import java.util.List; public class GoDocumentationProvider extends AbstractDocumentationProvider { private static final Logger LOG = Logger.getInstance(GoDocumentationProvider.class); private static final GoCommentsConverter COMMENTS_CONVERTER = new GoCommentsConverter(); @NotNull public static String getCommentText(@NotNull List<PsiComment> comments, boolean withHtml) { return withHtml ? COMMENTS_CONVERTER.toHtml(comments) : COMMENTS_CONVERTER.toText(comments); } @NotNull public static List<PsiComment> getCommentsForElement(@Nullable PsiElement element) { List<PsiComment> comments = getCommentsInner(element); if (comments.isEmpty()) { if (element instanceof GoVarDefinition || element instanceof GoConstDefinition) { PsiElement parent = element.getParent(); // spec comments = getCommentsInner(parent); if (comments.isEmpty() && parent != null) { return getCommentsInner(parent.getParent()); // declaration } } else if (element instanceof GoTypeSpec) { return getCommentsInner(element.getParent()); // type declaration } } return comments; } @NotNull private static List<PsiComment> getCommentsInner(@Nullable PsiElement element) { if (element == null) { return ContainerUtil.emptyList(); } List<PsiComment> result = ContainerUtil.newArrayList(); PsiElement e; for (e = element.getPrevSibling(); e != null; e = e.getPrevSibling()) { if (e instanceof PsiWhiteSpace) { if (e.getText().contains("\n\n")) return result; continue; } if (e instanceof PsiComment) { result.add(0, (PsiComment)e); } else { return result; } } return result; } @Nullable private static GoFile findDocFileForDirectory(@NotNull PsiDirectory directory) { PsiFile file = directory.findFile("doc.go"); if (file instanceof GoFile) { return (GoFile)file; } PsiFile directoryFile = directory.findFile(GoUtil.suggestPackageForDirectory(directory) + ".go"); return directoryFile instanceof GoFile ? (GoFile)directoryFile : null; } @Nullable private static String getPackageComment(@Nullable GoFile file) { if (file != null) { boolean vendoringEnabled = GoVendoringUtil.isVendoringEnabled(ModuleUtilCore.findModuleForPsiElement(file)); // todo: remove after correct stubbing (comments needed in stubs) GoPackageClause pack = PsiTreeUtil.findChildOfType(file, GoPackageClause.class); String title = "<b>Package " + GoUtil.suggestPackageForDirectory(file.getParent()) + "</b>\n"; String importPath = "<p><code>import \"" + StringUtil.notNullize(file.getImportPath(vendoringEnabled)) + "\"</code></p>\n"; return title + importPath + getCommentText(getCommentsForElement(pack), true); } return null; } @Nullable private static PsiElement adjustDocElement(@Nullable PsiElement element) { return element instanceof GoImportSpec ? ((GoImportSpec)element).getImportString().resolve() : element; } @NotNull private static String getSignature(PsiElement element, PsiElement context) { if (element instanceof GoTypeSpec) { String name = ((GoTypeSpec)element).getName(); return StringUtil.isNotEmpty(name) ? "type " + name : ""; } GoDocumentationPresentationFunction presentationFunction = new GoDocumentationPresentationFunction(getImportPathForElement(element)); if (element instanceof GoConstDefinition) { String name = ((GoConstDefinition)element).getName(); if (StringUtil.isNotEmpty(name)) { String type = getTypePresentation(((GoConstDefinition)element).getGoType(null), presentationFunction); GoExpression value = ((GoConstDefinition)element).getValue(); return "const " + name + (!type.isEmpty() ? " " + type : "") + (value != null ? " = " + value.getText() : ""); } } if (element instanceof GoVarDefinition) { String name = ((GoVarDefinition)element).getName(); if (StringUtil.isNotEmpty(name)) { String type = getTypePresentation(((GoVarDefinition)element).getGoType(GoPsiImplUtil.createContextOnElement(context)), presentationFunction); GoExpression value = ((GoVarDefinition)element).getValue(); return "var " + name + (!type.isEmpty() ? " " + type : "") + (value != null ? " = " + value.getText() : ""); } } if (element instanceof GoParamDefinition) { String name = ((GoParamDefinition)element).getName(); if (StringUtil.isNotEmpty(name)) { String type = getTypePresentation(((GoParamDefinition)element).getGoType(GoPsiImplUtil.createContextOnElement(context)), presentationFunction); return "var " + name + (!type.isEmpty() ? " " + type : ""); } } if (element instanceof GoReceiver) { String name = ((GoReceiver)element).getName(); if (StringUtil.isNotEmpty(name)) { String type = getTypePresentation(((GoReceiver)element).getGoType(GoPsiImplUtil.createContextOnElement(context)), presentationFunction); return "var " + name + (!type.isEmpty() ? " " + type : ""); } } return element instanceof GoSignatureOwner ? getSignatureOwnerTypePresentation((GoSignatureOwner)element, presentationFunction) : ""; } @NotNull private static String getSignatureOwnerTypePresentation(@NotNull GoSignatureOwner signatureOwner, @NotNull Function<PsiElement, String> presentationFunction) { PsiElement identifier = null; if (signatureOwner instanceof GoNamedSignatureOwner) { identifier = ((GoNamedSignatureOwner)signatureOwner).getIdentifier(); } GoSignature signature = signatureOwner.getSignature(); if (identifier == null && signature == null) { return ""; } StringBuilder builder = new StringBuilder("func ").append(identifier != null ? identifier.getText() : "").append('('); if (signature != null) { builder.append(getParametersAsString(signature.getParameters())); } builder.append(')'); GoResult result = signature != null ? signature.getResult() : null; GoParameters parameters = result != null ? result.getParameters() : null; GoType type = result != null ? result.getType() : null; if (parameters != null) { String signatureParameters = getParametersAsString(parameters); if (!signatureParameters.isEmpty()) { builder.append(" (").append(signatureParameters).append(')'); } } else if (type != null) { builder.append(' ').append(getTypePresentation(type, presentationFunction)); } return builder.toString(); } @NotNull private static String getParametersAsString(@NotNull GoParameters parameters) { String contextImportPath = getImportPathForElement(parameters); return StringUtil.join(GoParameterInfoHandler.getParameterPresentations(parameters, element -> getTypePresentation(element, new GoDocumentationPresentationFunction(contextImportPath))), ", "); } @Nullable private static String getImportPathForElement(@Nullable PsiElement element) { PsiFile file = element != null ? element.getContainingFile() : null; return file instanceof GoFile ? ((GoFile)file).getImportPath(false) : null; } @NotNull public static String getTypePresentation(@Nullable PsiElement element, @NotNull Function<PsiElement, String> presentationFunction) { if (element instanceof GoType) { GoType type = (GoType)element; if (type instanceof GoMapType) { GoType keyType = ((GoMapType)type).getKeyType(); GoType valueType = ((GoMapType)type).getValueType(); return "map[" + getTypePresentation(keyType, presentationFunction) + "]" + getTypePresentation(valueType, presentationFunction); } if (type instanceof GoChannelType) { ASTNode typeNode = type.getNode(); GoType innerType = ((GoChannelType)type).getType(); ASTNode innerTypeNode = innerType != null ? innerType.getNode() : null; if (typeNode != null && innerTypeNode != null) { StringBuilder result = new StringBuilder(); for (ASTNode node : typeNode.getChildren(null)) { if (node.equals(innerTypeNode)) { break; } if (node.getElementType() != TokenType.WHITE_SPACE) { result.append(XmlStringUtil.escapeString(node.getText())); } } result.append(" ").append(getTypePresentation(innerType, presentationFunction)); return result.toString(); } } if (type instanceof GoParType) { return "(" + getTypePresentation(((GoParType)type).getActualType(), presentationFunction) + ")"; } if (type instanceof GoArrayOrSliceType) { return "[]" + getTypePresentation(((GoArrayOrSliceType)type).getType(), presentationFunction); } if (type instanceof GoPointerType) { GoType inner = ((GoPointerType)type).getType(); return inner instanceof GoSpecType ? getTypePresentation(inner, presentationFunction) : "*" + getTypePresentation(inner, presentationFunction); } if (type instanceof GoTypeList) { return "(" + StringUtil.join(((GoTypeList)type).getTypeList(), element1 -> getTypePresentation(element1, presentationFunction), ", ") + ")"; } if (type instanceof GoFunctionType) { return getSignatureOwnerTypePresentation((GoFunctionType)type, presentationFunction); } if (type instanceof GoSpecType) { return getTypePresentation(GoPsiImplUtil.getTypeSpecSafe(type), presentationFunction); } if (type instanceof GoCType) { return "C"; } if (type instanceof GoLightType) { LOG.error("Cannot build presentable text for type: " + type.getClass().getSimpleName() + " - " + type.getText()); return ""; } if (type instanceof GoStructType) { StringBuilder result = new StringBuilder("struct {"); result.append(StringUtil.join(((GoStructType)type).getFieldDeclarationList(), declaration -> { GoAnonymousFieldDefinition anon = declaration.getAnonymousFieldDefinition(); String fieldString = anon != null ? getTypePresentation(anon.getGoTypeInner(null), presentationFunction) : StringUtil.join(declaration.getFieldDefinitionList(), PsiElement::getText, ", ") + " " + getTypePresentation(declaration.getType(), presentationFunction); GoTag tag = declaration.getTag(); return fieldString + (tag != null ? tag.getText() : ""); }, "; ")); return result.append("}").toString(); } GoTypeReferenceExpression typeRef = GoPsiImplUtil.getTypeReference(type); if (typeRef != null) { PsiElement resolve = typeRef.resolve(); if (resolve != null) { return getTypePresentation(resolve, presentationFunction); } } } return presentationFunction.fun(element); } @Nullable public static String getLocalUrlToElement(@NotNull PsiElement element) { if (element instanceof GoTypeSpec || element instanceof PsiDirectory) { return getReferenceText(element, true); } return null; } @Nullable private static String getReferenceText(@Nullable PsiElement element, boolean includePackageName) { if (element instanceof GoNamedElement) { PsiFile file = element.getContainingFile(); if (file instanceof GoFile) { String importPath = ((GoFile)file).getImportPath(false); if (element instanceof GoFunctionDeclaration || element instanceof GoTypeSpec) { String name = ((GoNamedElement)element).getName(); if (StringUtil.isNotEmpty(name)) { String fqn = getElementFqn((GoNamedElement)element, name, includePackageName); return String.format("%s#%s", StringUtil.notNullize(importPath), fqn); } } else if (element instanceof GoMethodDeclaration) { GoType receiverType = ((GoMethodDeclaration)element).getReceiverType(); String receiver = getReceiverTypeText(receiverType); String name = ((GoMethodDeclaration)element).getName(); if (StringUtil.isNotEmpty(receiver) && StringUtil.isNotEmpty(name)) { String fqn = getElementFqn((GoNamedElement)element, name, includePackageName); return String.format("%s#%s.%s", StringUtil.notNullize(importPath), receiver, fqn); } } } } else if (element instanceof PsiDirectory && findDocFileForDirectory((PsiDirectory)element) != null) { return GoSdkUtil.getImportPath((PsiDirectory)element, false); } return null; } private static String getReceiverTypeText(@Nullable GoType type) { if (type == null) return null; if (type instanceof GoPointerType) { GoType inner = ((GoPointerType)type).getType(); if (inner != null) return inner.getText(); } return type.getText(); } @NotNull private static String getElementFqn(@NotNull GoNamedElement element, @NotNull String name, boolean includePackageName) { if (includePackageName) { String packageName = element.getContainingFile().getPackageName(); if (!StringUtil.isEmpty(packageName)) { return packageName + "." + name; } } return name; } @Override public String generateDoc(PsiElement element, PsiElement originalElement) { element = adjustDocElement(element); if (element instanceof GoNamedElement) { String signature = getSignature(element, originalElement); signature = StringUtil.isNotEmpty(signature) ? "<b>" + signature + "</b>\n" : signature; return StringUtil.nullize(signature + getCommentText(getCommentsForElement(element), true)); } if (element instanceof PsiDirectory) { return getPackageComment(findDocFileForDirectory((PsiDirectory)element)); } return null; } @Override public List<String> getUrlFor(PsiElement element, PsiElement originalElement) { String referenceText = getReferenceText(adjustDocElement(element), false); if (StringUtil.isNotEmpty(referenceText)) { return Collections.singletonList("https://godoc.org/" + referenceText); } return super.getUrlFor(element, originalElement); } @Nullable @Override public String getQuickNavigateInfo(PsiElement element, PsiElement originalElement) { if (element instanceof GoNamedElement) { String result = getSignature(element, originalElement); if (StringUtil.isNotEmpty(result)) return result; } return super.getQuickNavigateInfo(element, originalElement); } @Override public PsiElement getDocumentationElementForLink(PsiManager psiManager, String link, PsiElement context) { if (context != null && !DumbService.isDumb(psiManager.getProject())) { // it's important to ask module on file, otherwise module won't be found for elements in libraries files [zolotov] Module module = ModuleUtilCore.findModuleForPsiElement(context.getContainingFile()); int hash = link.indexOf('#'); String importPath = hash >= 0 ? link.substring(0, hash) : link; Project project = psiManager.getProject(); VirtualFile directory = GoPackageUtil.findByImportPath(importPath, project, module); PsiDirectory psiDirectory = directory != null ? psiManager.findDirectory(directory) : null; String anchor = hash >= 0 ? link.substring(Math.min(hash + 1, link.length())) : null; if (StringUtil.isNotEmpty(anchor)) { GlobalSearchScope scope = psiDirectory != null ? GoPackageUtil.packageScope(psiDirectory, null) : GlobalSearchScope.projectScope(project); IdFilter idFilter = GoIdFilter.getFilesFilter(scope); GoNamedElement element = ContainerUtil.getFirstItem(StubIndex.getElements(GoAllPublicNamesIndex.ALL_PUBLIC_NAMES, anchor, project, scope, idFilter, GoNamedElement.class)); if (element != null) { return element; } return ContainerUtil.getFirstItem(StubIndex.getElements(GoAllPrivateNamesIndex.ALL_PRIVATE_NAMES, anchor, project, scope, idFilter, GoNamedElement.class)); } else { return psiDirectory; } } return super.getDocumentationElementForLink(psiManager, link, context); } private static class GoDocumentationPresentationFunction implements Function<PsiElement, String> { private final String myContextImportPath; public GoDocumentationPresentationFunction(@Nullable String contextImportPath) { myContextImportPath = contextImportPath; } @Override public String fun(PsiElement element) { if (element instanceof GoTypeSpec) { String localUrl = getLocalUrlToElement(element); String name = ((GoTypeSpec)element).getName(); if (StringUtil.isNotEmpty(name)) { String importPath = getImportPathForElement(element); GoFile file = ((GoTypeSpec)element).getContainingFile(); String packageName = file.getPackageName(); if (StringUtil.isNotEmpty(packageName) && !GoPsiImplUtil.isBuiltinFile(file) && !Comparing.equal(importPath, myContextImportPath)) { return String.format("<a href=\"%s%s\">%s</a>.<a href=\"%s%s\">%s</a>", DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL, StringUtil.notNullize(importPath), packageName, DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL, localUrl, name); } return String.format("<a href=\"%s%s\">%s</a>", DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL, localUrl, name); } } return element != null ? element.getText() : ""; } } }