/* * Copyright 2000-2013 JetBrains s.r.o. * * 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.jetbrains.python.documentation; import com.intellij.codeInsight.TargetElementUtilBase; import com.intellij.ide.actions.ShowSettingsUtilImpl; import com.intellij.lang.documentation.AbstractDocumentationProvider; import com.intellij.lang.documentation.ExternalDocumentationProvider; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.QualifiedName; import com.intellij.util.Function; import com.jetbrains.python.PyNames; import com.jetbrains.python.codeInsight.PyCodeInsightSettings; import com.jetbrains.python.console.PydevConsoleRunner; import com.jetbrains.python.console.PydevDocumentationProvider; import com.jetbrains.python.debugger.PySignature; import com.jetbrains.python.debugger.PySignatureCacheManager; import com.jetbrains.python.debugger.PySignatureUtil; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyBuiltinCache; import com.jetbrains.python.psi.resolve.QualifiedNameFinder; import com.jetbrains.python.psi.types.PyClassType; import com.jetbrains.python.psi.types.PyType; import com.jetbrains.python.psi.types.PyTypeParser; import com.jetbrains.python.psi.types.TypeEvalContext; import com.jetbrains.python.toolbox.ChainIterable; import com.jetbrains.python.toolbox.FP; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.methods.HeadMethod; import org.apache.commons.httpclient.params.HttpConnectionManagerParams; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; import java.util.Collections; import java.util.List; import static com.jetbrains.python.documentation.DocumentationBuilderKit.*; /** * Provides quick docs for classes, methods, and functions. * Generates documentation stub */ public class PythonDocumentationProvider extends AbstractDocumentationProvider implements ExternalDocumentationProvider { @NonNls static final String LINK_TYPE_CLASS = "#class#"; @NonNls static final String LINK_TYPE_PARENT = "#parent#"; @NonNls static final String LINK_TYPE_PARAM = "#param#"; @NonNls static final String LINK_TYPE_TYPENAME = "#typename#"; @NonNls private static final String RST_PREFIX = ":"; @NonNls private static final String EPYDOC_PREFIX = "@"; // provides ctrl+hover info public String getQuickNavigateInfo(final PsiElement element, PsiElement originalElement) { if (element instanceof PyFunction) { PyFunction func = (PyFunction)element; StringBuilder cat = new StringBuilder(); PyClass cls = func.getContainingClass(); if (cls != null) { String cls_name = cls.getName(); cat.append("class ").append(cls_name).append("\n"); // It would be nice to have class import info here, but we don't know the ctrl+hovered reference and context } String summary = ""; final PyStringLiteralExpression docStringExpression = func.getDocStringExpression(); if (docStringExpression != null) { final StructuredDocString docString = DocStringUtil.parse(docStringExpression.getStringValue()); if (docString != null) { summary = docString.getSummary(); } } return $(cat.toString()).add(describeDecorators(func, LSame2, ", ", LSame1)).add(describeFunction(func, LSame2, LSame1)) .toString() + "\n" + summary; } else if (element instanceof PyClass) { PyClass cls = (PyClass)element; String summary = ""; PyStringLiteralExpression docStringExpression = cls.getDocStringExpression(); if (docStringExpression == null) { final PyFunction initOrNew = cls.findInitOrNew(false); if (initOrNew != null) { docStringExpression = initOrNew.getDocStringExpression(); } } if (docStringExpression != null) { final StructuredDocString docString = DocStringUtil.parse(docStringExpression.getStringValue()); if (docString != null) { summary = docString.getSummary(); } } return describeDecorators(cls, LSame2, ", ", LSame1).add(describeClass(cls, LSame2, false, false)).toString() + "\n" + summary; } else if (element instanceof PyExpression) { return describeExpression((PyExpression)element, originalElement); } return null; } /** * Creates a HTML description of function definition. * * @param fun the function * @param func_name_wrapper puts a tag around the function name * @param escaper sanitizes values that come directly from doc string or code * @return chain of strings for further chaining */ static ChainIterable describeFunction( PyFunction fun, FP.Lambda1, Iterable> func_name_wrapper, FP.Lambda1 escaper ) { ChainIterable cat = new ChainIterable(); final String name = fun.getName(); cat.addItem("def ").addWith(func_name_wrapper, $(name)); final TypeEvalContext context = TypeEvalContext.userInitiated(fun.getContainingFile()); final List parameters = PyUtil.getParameters(fun, context); final String paramStr = "(" + StringUtil.join(parameters, new Function() { @Override public String fun(PyParameter parameter) { return PyUtil.getReadableRepr(parameter, false); } }, ", ") + ")"; cat.addItem(escaper.apply(paramStr)); if (!PyNames.INIT.equals(name)) { cat.addItem(escaper.apply("\nInferred type: ")); getTypeDescription(fun, cat); cat.addItem(BR); } return cat; } @Nullable private static String describeExpression(@NotNull PyExpression expr, @NotNull PsiElement originalElement) { final String name = expr.getName(); if (name != null) { StringBuilder result = new StringBuilder((expr instanceof PyNamedParameter) ? "parameter" : "variable"); result.append(String.format(" \"%s\"", name)); if (expr instanceof PyNamedParameter) { final PyFunction function = PsiTreeUtil.getParentOfType(expr, PyFunction.class); if (function != null) { result.append(" of ").append(function.getContainingClass() == null ? "function" : "method"); result.append(String.format(" \"%s\"", function.getName())); } } if (originalElement instanceof PyTypedElement) { result.append("\n").append(describeType((PyTypedElement)originalElement)); } return result.toString(); } return null; } static String describeType(@NotNull PyTypedElement element) { final TypeEvalContext context = TypeEvalContext.userInitiated(element.getContainingFile()); return String.format("Inferred type: %s", getTypeName(context.getType(element), context)); } public static void getTypeDescription(@NotNull PyFunction fun, ChainIterable body) { final TypeEvalContext context = TypeEvalContext.userInitiated(fun.getContainingFile()); PyTypeModelBuilder builder = new PyTypeModelBuilder(context); builder.build(context.getType(fun), true).toBodyWithLinks(body, fun); } public static String getTypeName(@Nullable PyType type, @NotNull final TypeEvalContext context) { PyTypeModelBuilder.TypeModel typeModel = buildTypeModel(type, context); return typeModel.asString(); } private static PyTypeModelBuilder.TypeModel buildTypeModel(PyType type, TypeEvalContext context) { PyTypeModelBuilder builder = new PyTypeModelBuilder(context); return builder.build(type, true); } public static void describeExpressionTypeWithLinks(ChainIterable body, PyReferenceExpression expression, @NotNull TypeEvalContext context) { PyType type = context.getType(expression); describeTypeWithLinks(body, expression, type, context); } public static void describeTypeWithLinks(ChainIterable body, PsiElement anchor, PyType type, TypeEvalContext context) { PyTypeModelBuilder builder = new PyTypeModelBuilder(context); builder.build(type, true).toBodyWithLinks(body, anchor); } static ChainIterable describeDecorators(PyDecoratable what, FP.Lambda1, Iterable> deco_name_wrapper, String deco_separator, FP.Lambda1 escaper) { ChainIterable cat = new ChainIterable(); PyDecoratorList deco_list = what.getDecoratorList(); if (deco_list != null) { for (PyDecorator deco : deco_list.getDecorators()) { cat.add(describeDeco(deco, deco_name_wrapper, escaper)).addItem(deco_separator); // can't easily pass describeDeco to map() %) } } return cat; } /** * Creates a HTML description of function definition. * * @param cls the class * @param name_wrapper wrapper to render the name with * @param allow_html * @param link_own_name if true, add link to class's own name @return cat for easy chaining */ static ChainIterable describeClass(PyClass cls, FP.Lambda1, Iterable> name_wrapper, boolean allow_html, boolean link_own_name) { ChainIterable cat = new ChainIterable(); final String name = cls.getName(); cat.addItem("class "); if (allow_html && link_own_name) { cat.addWith(LinkMyClass, $(name)); } else { cat.addWith(name_wrapper, $(name)); } final PyExpression[] ancestors = cls.getSuperClassExpressions(); if (ancestors.length > 0) { cat.addItem("("); boolean is_not_first = false; for (PyExpression parent : ancestors) { final String parentName = parent.getName(); if (parentName == null) { continue; } if (is_not_first) { cat.addItem(", "); } else { is_not_first = true; } if (allow_html) { cat.addWith(new LinkWrapper(LINK_TYPE_PARENT + parentName), $(parentName)); } else { cat.addItem(parentName); } } cat.addItem(")"); } return cat; } // private static Iterable describeDeco(PyDecorator deco, final FP.Lambda1, Iterable> name_wrapper, // addWith in tags, if need be final FP.Lambda1 arg_wrapper // add escaping, if need be ) { ChainIterable cat = new ChainIterable(); cat.addItem("@").addWith(name_wrapper, $(PyUtil.getReadableRepr(deco.getCallee(), true))); if (deco.hasArgumentList()) { PyArgumentList arglist = deco.getArgumentList(); if (arglist != null) { cat .addItem("(") .add(interleave(FP.map(FP.combine(LReadableRepr, arg_wrapper), arglist.getArguments()), ", ")) .addItem(")") ; } } return cat; } // provides ctrl+Q doc public String generateDoc(PsiElement element, @Nullable PsiElement originalElement) { if (element != null && PydevConsoleRunner.isInPydevConsole(element) || originalElement != null && PydevConsoleRunner.isInPydevConsole(originalElement)) { return PydevDocumentationProvider.createDoc(element, originalElement); } originalElement = findRealOriginalElement(originalElement); //original element can be whitespace or bracket, // but we need identifier that resolves to element return new PyDocumentationBuilder(element, originalElement).build(); } private static PsiElement findRealOriginalElement(@Nullable PsiElement element) { if (element == null) { return null; } PsiFile file = element.getContainingFile(); if (file == null) { return element; } Document document = PsiDocumentManager.getInstance(element.getProject()).getDocument(file); if (document == null) { return element; } int newOffset = TargetElementUtilBase.adjustOffset(file, document, element.getTextOffset()); PsiElement newElement = file.findElementAt(newOffset); return newElement != null ? newElement : element; } @Override public PsiElement getDocumentationElementForLink(PsiManager psiManager, String link, PsiElement context) { if (link.equals(LINK_TYPE_CLASS)) { return inferContainingClassOf(context); } else if (link.equals(LINK_TYPE_PARAM)) { return inferClassOfParameter(context); } else if (link.startsWith(LINK_TYPE_PARENT)) { PyClass cls = inferContainingClassOf(context); if (cls != null) { String desired_name = link.substring(LINK_TYPE_PARENT.length()); for (PyClass parent : cls.getAncestorClasses()) { final String parent_name = parent.getName(); if (parent_name != null && parent_name.equals(desired_name)) return parent; } } } else if (link.startsWith(LINK_TYPE_TYPENAME)) { String typeName = link.substring(LINK_TYPE_TYPENAME.length()); PyType type = PyTypeParser.getTypeByName(context, typeName); if (type instanceof PyClassType) { return ((PyClassType)type).getPyClass(); } } return null; } @Override public List getUrlFor(final PsiElement element, PsiElement originalElement) { final String url = getUrlFor(element, originalElement, true); return url == null ? null : Collections.singletonList(url); } @Nullable private static String getUrlFor(PsiElement element, PsiElement originalElement, boolean checkExistence) { PsiFileSystemItem file = element instanceof PsiFileSystemItem ? (PsiFileSystemItem)element : element.getContainingFile(); if (file == null) return null; if (PyNames.INIT_DOT_PY.equals(file.getName())) { file = file.getParent(); assert file != null; } Sdk sdk = PyBuiltinCache.findSdkForFile(file); if (sdk == null) { return null; } QualifiedName qName = QualifiedNameFinder.findCanonicalImportPath(element, originalElement); if (qName == null) { return null; } PythonDocumentationMap map = PythonDocumentationMap.getInstance(); String pyVersion = pyVersion(sdk.getVersionString()); PsiNamedElement namedElement = (element instanceof PsiNamedElement && !(element instanceof PsiFileSystemItem)) ? (PsiNamedElement)element : null; if (namedElement instanceof PyFunction && PyNames.INIT.equals(namedElement.getName())) { final PyClass containingClass = ((PyFunction)namedElement).getContainingClass(); if (containingClass != null) { namedElement = containingClass; } } String url = map.urlFor(qName, namedElement, pyVersion); if (url != null) { if (checkExistence && !pageExists(url)) { return map.rootUrlFor(qName); } return url; } for (PythonDocumentationLinkProvider provider : Extensions.getExtensions(PythonDocumentationLinkProvider.EP_NAME)) { final String providerUrl = provider.getExternalDocumentationUrl(element, originalElement); if (providerUrl != null) { if (checkExistence && !pageExists(providerUrl)) { return provider.getExternalDocumentationRoot(sdk); } return providerUrl; } } return null; } private static boolean pageExists(String url) { if (new File(url).exists()) { return true; } HttpClient client = new HttpClient(); HttpConnectionManagerParams params = client.getHttpConnectionManager().getParams(); params.setSoTimeout(5 * 1000); params.setConnectionTimeout(5 * 1000); try { HeadMethod method = new HeadMethod(url); int rc = client.executeMethod(method); if (rc == 404) { return false; } } catch (IllegalArgumentException e) { return false; } catch (IOException ignored) { } return true; } @Nullable public static String pyVersion(@Nullable String versionString) { String prefix = "Python "; if (versionString != null && versionString.startsWith(prefix)) { String version = versionString.substring(prefix.length()); int dot = version.indexOf('.'); if (dot > 0) { dot = version.indexOf('.', dot + 1); if (dot > 0) { return version.substring(0, dot); } return version; } } return null; } @Override public String fetchExternalDocumentation(Project project, PsiElement element, List docUrls) { return null; } @Override public boolean hasDocumentationFor(PsiElement element, PsiElement originalElement) { return getUrlFor(element, originalElement, false) != null; } @Override public boolean canPromptToConfigureDocumentation(PsiElement element) { final PsiFile containingFile = element.getContainingFile(); if (containingFile instanceof PyFile) { final Project project = element.getProject(); final VirtualFile vFile = containingFile.getVirtualFile(); if (vFile != null && ProjectRootManager.getInstance(project).getFileIndex().isInLibraryClasses(vFile)) { final QualifiedName qName = QualifiedNameFinder.findCanonicalImportPath(element, element); if (qName != null && qName.getComponentCount() > 0) { return true; } } } return false; } @Override public void promptToConfigureDocumentation(PsiElement element) { final Project project = element.getProject(); final QualifiedName qName = QualifiedNameFinder.findCanonicalImportPath(element, element); if (qName != null && qName.getComponentCount() > 0) { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { int rc = Messages.showOkCancelDialog(project, "No external documentation URL configured for module " + qName.getComponents().get(0) + ".\nWould you like to configure it now?", "Python External Documentation", Messages.getQuestionIcon()); if (rc == Messages.OK) { ShowSettingsUtilImpl.showSettingsDialog(project, PythonDocumentationConfigurable.ID, ""); } } }, ModalityState.NON_MODAL); } } @Nullable private static PyClass inferContainingClassOf(PsiElement context) { if (context instanceof PyClass) return (PyClass)context; if (context instanceof PyFunction) { return ((PyFunction)context).getContainingClass(); } else { return PsiTreeUtil.getParentOfType(context, PyClass.class); } } @Nullable private static PyClass inferClassOfParameter(PsiElement context) { if (context instanceof PyNamedParameter) { final PyType type = TypeEvalContext.userInitiated(context.getContainingFile()).getType((PyNamedParameter)context); if (type instanceof PyClassType) { return ((PyClassType)type).getPyClass(); } } return null; } public static final LinkWrapper LinkMyClass = new LinkWrapper(LINK_TYPE_CLASS); // link item to containing class public static String generateDocumentationContentStub(PyFunction element, String offset, boolean checkReturn) { final Module module = ModuleUtilCore.findModuleForPsiElement(element); if (module == null) return ""; PyDocumentationSettings documentationSettings = PyDocumentationSettings.getInstance(module); String result = ""; if (documentationSettings.isEpydocFormat(element.getContainingFile())) { result += generateContent(element, offset, EPYDOC_PREFIX, checkReturn); } else if (documentationSettings.isReSTFormat(element.getContainingFile())) { result += generateContent(element, offset, RST_PREFIX, checkReturn); } else { result += offset; } return result; } public static void insertDocStub(PyFunction function, PyStatementList insertPlace, Project project, Editor editor) { PyElementGenerator elementGenerator = PyElementGenerator.getInstance(project); PsiWhiteSpace whitespace = PsiTreeUtil.getPrevSiblingOfType(insertPlace, PsiWhiteSpace.class); String ws = "\n"; if (whitespace != null) { String[] spaces = whitespace.getText().split("\n"); if (spaces.length > 1) { ws += spaces[spaces.length - 1]; } } String docContent = ws + generateDocumentationContentStub(function, ws, true); PyExpressionStatement string = elementGenerator.createDocstring("\"\"\"" + docContent + "\"\"\""); if (insertPlace != null) { final PyStatement[] statements = insertPlace.getStatements(); if (statements.length != 0) { insertPlace.addBefore(string, statements[0]); } } PyStringLiteralExpression docstring = function.getDocStringExpression(); if (editor != null && docstring != null) { int offset = docstring.getTextOffset(); editor.getCaretModel().moveToOffset(offset); editor.getCaretModel().moveCaretRelatively(0, 1, false, false, false); } } public String generateDocumentationContentStub(PyFunction element, boolean checkReturn) { PsiWhiteSpace whitespace = PsiTreeUtil.getPrevSiblingOfType(element.getStatementList(), PsiWhiteSpace.class); String ws = "\n"; if (whitespace != null) { String[] spaces = whitespace.getText().split("\n"); if (spaces.length > 1) { ws += whitespace.getText().split("\n")[1]; } } return generateDocumentationContentStub(element, ws, checkReturn); } private static String generateContent(PyFunction function, String offset, String prefix, boolean checkReturn) { //TODO: this code duplicates PyDocstringGenerator in some parts final StringBuilder builder = new StringBuilder(offset); final TypeEvalContext context = TypeEvalContext.userInitiated(function.getContainingFile()); PySignature signature = PySignatureCacheManager.getInstance(function.getProject()).findSignature(function); final PyDecoratorList decoratorList = function.getDecoratorList(); final PyDecorator classMethod = decoratorList == null ? null : decoratorList.findDecorator(PyNames.CLASSMETHOD); for (PyParameter p : PyUtil.getParameters(function, context)) { final String parameterName = p.getName(); if (p.getText().equals(PyNames.CANONICAL_SELF) || parameterName == null) { continue; } if (classMethod != null && parameterName.equals(PyNames.CANONICAL_CLS)) continue; String argType = signature == null ? null : signature.getArgTypeQualifiedName(parameterName); if (argType == null) { builder.append(prefix); builder.append("param "); builder.append(parameterName); builder.append(": "); builder.append(offset); } if (PyCodeInsightSettings.getInstance().INSERT_TYPE_DOCSTUB || argType != null) { builder.append(prefix); builder.append("type "); builder.append(parameterName); builder.append(": "); if (signature != null && argType != null) { builder.append(PySignatureUtil.getShortestImportableName(function, argType)); } builder.append(offset); } } builder.append(generateRaiseOrReturn(function, offset, prefix, checkReturn)); return builder.toString(); } public static String generateRaiseOrReturn(PyFunction element, String offset, String prefix, boolean checkReturn) { StringBuilder builder = new StringBuilder(); if (checkReturn) { RaiseVisitor visitor = new RaiseVisitor(); PyStatementList statementList = element.getStatementList(); statementList.accept(visitor); if (visitor.myHasReturn) { builder.append(prefix).append("return:").append(offset); if (PyCodeInsightSettings.getInstance().INSERT_TYPE_DOCSTUB) { builder.append(prefix).append("rtype:").append(offset); } } if (visitor.myHasRaise) { builder.append(prefix).append("raise"); if (visitor.myRaiseTarget != null) { String raiseTarget = visitor.myRaiseTarget.getText(); if (visitor.myRaiseTarget instanceof PyCallExpression) { final PyExpression callee = ((PyCallExpression)visitor.myRaiseTarget).getCallee(); if (callee != null) raiseTarget = callee.getText(); } builder.append(" ").append(raiseTarget); } builder.append(":").append(offset); } } else { builder.append(prefix).append("return:").append(offset); if (PyCodeInsightSettings.getInstance().INSERT_TYPE_DOCSTUB) { builder.append(prefix).append("rtype:").append(offset); } } return builder.toString(); } private static class RaiseVisitor extends PyRecursiveElementVisitor { private boolean myHasRaise = false; private boolean myHasReturn = false; private PyExpression myRaiseTarget = null; @Override public void visitPyRaiseStatement(PyRaiseStatement node) { myHasRaise = true; final PyExpression[] expressions = node.getExpressions(); if (expressions.length > 0) myRaiseTarget = expressions[0]; } @Override public void visitPyReturnStatement(PyReturnStatement node) { myHasReturn = true; } } }