blob: c05069098ea69bc1ad9402718e3896b27b0bf7f8 [file] [log] [blame]
/*
* 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.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.intellij.codeInsight.CodeInsightUtilCore;
import com.intellij.codeInsight.template.*;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.LineTokenizer;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PythonFileType;
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.toolbox.Substring;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* @author traff
*/
public class PyDocstringGenerator {
@NotNull
private PyDocStringOwner myDocStringOwner;
@Nullable
private PyFunction myFunction;
private List<DocstringParam> myParams = Lists.newArrayList();
private final Project myProject;
private PyStringLiteralExpression myDocStringExpression;
private final Map<String, Pair<Integer, Integer>> myParamTypesOffset = Maps.newHashMap();
private PsiFile myFile;
private boolean myGenerateReturn;
public PyDocstringGenerator(@NotNull PyDocStringOwner docStringOwner) {
myDocStringOwner = docStringOwner;
if (docStringOwner instanceof PyFunction) {
myFunction = (PyFunction)docStringOwner;
}
myProject = myDocStringOwner.getProject();
myFile = myDocStringOwner.getContainingFile();
}
public void addFunctionArguments(@NotNull PyFunction function,
@Nullable PySignature signature) {
for (PyParameter functionParam : function.getParameterList().getParameters()) {
String paramName = functionParam.getName();
if (!functionParam.isSelf() && !StringUtil.isEmpty(paramName)) {
String type = signature != null ? signature.getArgTypeQualifiedName(paramName) : null;
if (type != null) {
withParamTypedByQualifiedName("type", paramName, type, function);
}
else {
withParam("param", paramName);
}
}
}
}
public PyDocstringGenerator withParam(@NotNull String kind, @NotNull String name) {
return withParamTypedByName(kind, name, null);
}
public PyDocstringGenerator withParamTypedByQualifiedName(String kind, String name, @Nullable String type, @NotNull PsiElement anchor) {
String typeName = type != null ? PySignatureUtil.getShortestImportableName(anchor, type) : null;
return withParamTypedByName(kind, name, typeName);
}
public PyDocstringGenerator withParamTypedByName(String kind, String name, String type) {
myParams.add(new DocstringParam(kind, name, type));
return this;
}
public void withReturn() {
myGenerateReturn = true;
}
private PsiFile getFile() {
return myFile;
}
public void startTemplate() {
assert myDocStringExpression != null;
final TemplateBuilder builder = TemplateBuilderFactory.getInstance().createTemplateBuilder(myDocStringExpression);
if (myParams.size() > 1) {
throw new IllegalArgumentException("TemplateBuilder can be created only for one parameter");
}
int offset = getStartOffset();
if (offset > 0) {
builder.replaceRange(TextRange.create(offset, getEndOffset()), getDefaultType());
Template template = ((TemplateBuilderImpl)builder).buildInlineTemplate();
final VirtualFile virtualFile = myFile.getVirtualFile();
if (virtualFile == null) return;
OpenFileDescriptor descriptor = new OpenFileDescriptor(myProject, virtualFile, myDocStringExpression.getTextOffset());
Editor targetEditor = FileEditorManager.getInstance(myProject).openTextEditor(descriptor, true);
if (targetEditor != null) {
TemplateManager.getInstance(myProject).startTemplate(targetEditor, template);
}
}
}
private String getDefaultType() {
DocstringParam param = getParamToEdit();
if (StringUtil.isEmpty(param.getType())) {
return PyNames.OBJECT;
}
else {
return param.getType();
}
}
@NotNull
public String addParamToDocstring() {
String text = getDocstringText();
StructuredDocString structuredDocString = DocStringUtil.parse(text);
Collection<DocstringParam> paramsToAdd = getParamsToAdd(structuredDocString, myParams);
String[] lines = LineTokenizer.tokenize(text, true);
if (lines.length == 1) {
return createSingleLineReplacement(paramsToAdd);
}
else {
return createMultiLineReplacement(lines, paramsToAdd);
}
}
public boolean haveParametersToAdd() {
Collection<DocstringParam> paramsToAdd = collectParametersToAdd();
return paramsToAdd.size() > 0;
}
private Collection<DocstringParam> collectParametersToAdd() {
String text = getDocstringText();
StructuredDocString structuredDocString = DocStringUtil.parse(text); //TODO: do we need to cache it?
return getParamsToAdd(structuredDocString, myParams);
}
@NotNull
private String getDocstringText() {
final PyStringLiteralExpression docstring = myDocStringOwner.getDocStringExpression();
return docstring != null ? docstring.getText() : "\"\"\"\"\"\"";
}
public static Collection<DocstringParam> getParamsToAdd(final StructuredDocString structuredDocString,
List<DocstringParam> params) {
return Collections2.filter(params, new Predicate<DocstringParam>() {
@Override
public boolean apply(DocstringParam input) {
Substring s = structuredDocString != null ? structuredDocString.getParamByNameAndKind(input.getName(), input.getKind()) : null;
return s == null;
}
});
}
@NotNull
private String createMultiLineReplacement(String[] lines, Collection<DocstringParam> paramsToAdd) {
StringBuilder replacementText = new StringBuilder();
int ind = lines.length - 1;
for (int i = 0; i != lines.length - 1; ++i) {
String line = lines[i];
if (isPlaceToInsertParameter(line)) {
ind = i;
break;
}
replacementText.append(line);
}
if (replacementText.length() > 0) {
replacementText.deleteCharAt(replacementText.length() - 1);
}
addParams(replacementText, false, paramsToAdd);
for (int i = ind; i != lines.length; ++i) {
String line = lines[i];
replacementText.append(line);
}
return replacementText.toString();
}
private boolean isPlaceToInsertParameter(String line) {
return line.contains(getPrefix()); //TODO: use regexp here?
}
private int addParams(StringBuilder replacementText,
boolean addWS, Collection<DocstringParam> paramsToAdd) {
final String ws = getWhitespace();
// if creating a new docstring, leave blank line where text will be entered
if (!StringUtil.containsAlphaCharacters(replacementText.toString())) {
replacementText.append("\n");
}
replacementText.append(ws);
final Module module = ModuleUtilCore.findModuleForPsiElement(myDocStringOwner);
if (module != null) {
PyDocumentationSettings documentationSettings = PyDocumentationSettings.getInstance(module);
if (documentationSettings.isPlain(getFile())) return replacementText.length() - 1;
}
int i = 0;
for (DocstringParam param : paramsToAdd) {
replacementText.append(getPrefix());
replacementText.append(param.getKind());
replacementText.append(" ");
replacementText.append(param.getName());
replacementText.append(": ");
int startOffset = replacementText.length();
int endOffset = startOffset;
if (param.getType() != null) {
replacementText.append(param.getType());
endOffset += param.getType().length();
}
myParamTypesOffset.put(param.getName(), Pair.create(startOffset, endOffset));
i++;
if (i < paramsToAdd.size()) {
replacementText.append(ws);
}
}
if (myGenerateReturn && myDocStringOwner instanceof PyFunction) {
PyFunction function = (PyFunction)myDocStringOwner;
String returnType = PythonDocumentationProvider.generateRaiseOrReturn(function, " ", getPrefix(), true);
if (!returnType.isEmpty()) {
replacementText.append(ws).append(returnType);
}
}
int offset = replacementText.length();
if (addWS) {
replacementText.append(ws);
}
else {
replacementText.append("\n");
}
return offset;
}
private String getWhitespace() {
PsiWhiteSpace whitespace = null;
if (myDocStringOwner instanceof PyFunction) {
final PyStatementList statementList = ((PyFunction)myDocStringOwner).getStatementList();
final Document document = PsiDocumentManager.getInstance(myProject).getDocument(getFile());
if (document != null && myFunction != null && statementList.getStatements().length != 0
&& document.getLineNumber(statementList.getTextOffset()) != document.getLineNumber(myFunction.getTextOffset())) {
whitespace = PsiTreeUtil.getPrevSiblingOfType(statementList, PsiWhiteSpace.class);
}
}
String ws = "\n";
if (whitespace != null) {
String[] spaces = whitespace.getText().split("\n");
if (spaces.length > 0) {
ws += spaces[spaces.length - 1];
}
}
else {
ws += StringUtil.repeat(" ", getIndentSize(myDocStringOwner));
}
return ws;
}
@NotNull
private String createSingleLineReplacement(Collection<DocstringParam> paramsToAdd) {
String text = getDocstringText();
StringBuilder replacementText = new StringBuilder();
String closingQuotes;
if (text.endsWith("'''") || text.endsWith("\"\"\"")) {
replacementText.append(text.substring(0, text.length() - 3));
closingQuotes = text.substring(text.length() - 3);
}
else {
replacementText.append(text.substring(0, text.length()));
closingQuotes = text.substring(text.length() - 1);
}
addParams(replacementText, true, paramsToAdd);
replacementText.append(closingQuotes);
return replacementText.toString();
}
public String docStringAsText() {
return addParamToDocstring();
}
public int getStartOffset() {
Pair<Integer, Integer> offsets = getOffsets();
return offsets != null ? offsets.first : -1;
}
private Pair<Integer, Integer> getOffsets() {
DocstringParam paramToEdit = getParamToEdit();
String paramName = paramToEdit.getName();
return myParamTypesOffset.get(paramName);
}
private DocstringParam getParamToEdit() {
if (myParams.size() == 0) {
throw new IllegalStateException("We should have at least one param to edit");
}
return myParams.get(0);
}
public int getEndOffset() {
Pair<Integer, Integer> offsets = getOffsets();
return offsets != null ? offsets.second : -1;
}
public static class DocstringParam {
private String myKind;
private String myName;
private String myType;
private DocstringParam(@NotNull String kind, @NotNull String name, @Nullable String type) {
myKind = kind;
myName = name;
myType = type;
}
public String getKind() {
return myKind;
}
public String getName() {
return myName;
}
public String getType() {
return myType;
}
}
public void build() {
myDocStringExpression = myDocStringOwner.getDocStringExpression();
final String replacement = addParamToDocstring();
PyElementGenerator elementGenerator = PyElementGenerator.getInstance(myProject);
if (myDocStringExpression != null) {
PyExpression str = elementGenerator.createDocstring(replacement).getExpression();
myDocStringExpression.replace(str);
if (myFunction != null) {
myFunction = CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(myFunction);
}
PyDocStringOwner owner = CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(myDocStringOwner);
if (owner != null) {
myDocStringOwner = owner;
}
myDocStringExpression = myDocStringOwner.getDocStringExpression();
}
else {
if (myFunction == null) {
throw new IllegalStateException("Should be a function");
}
final PyStatementList list = myFunction.getStatementList();
final Document document = PsiDocumentManager.getInstance(myProject).getDocument(getFile());
if (document != null) {
if (document.getLineNumber(list.getTextOffset()) == document.getLineNumber(myFunction.getTextOffset()) ||
list.getStatements().length == 0) {
PyFunction func = elementGenerator.createFromText(LanguageLevel.forElement(myFunction),
PyFunction.class,
"def " + myFunction.getName() + myFunction.getParameterList().getText()
+ ":\n" + StringUtil.repeat(" ", getIndentSize(myFunction))
+ replacement + "\n" +
StringUtil.repeat(" ", getIndentSize(myFunction)) + list.getText()
);
myFunction = (PyFunction)myFunction.replace(func);
}
else {
PyExpressionStatement str = elementGenerator.createDocstring(replacement);
list.addBefore(str, list.getStatements()[0]);
}
}
myFunction = CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(myFunction);
if (myFunction != null) {
myDocStringExpression = myFunction.getDocStringExpression();
}
}
}
private static int getIndentSize(PyDocStringOwner function) {
CodeStyleSettings.IndentOptions indentOptions = CodeStyleSettingsManager.
getInstance(function.getProject()).getCurrentSettings().getIndentOptions(PythonFileType.INSTANCE);
PyStatementList statementList = PsiTreeUtil.getParentOfType(function, PyStatementList.class);
int indent = 1;
while (statementList != null) {
statementList = PsiTreeUtil.getParentOfType(statementList, PyStatementList.class);
indent += 1;
}
return indent * indentOptions.TAB_SIZE;
}
private String getPrefix() {
String prefix = ":";
final Module module = ModuleUtilCore.findModuleForPsiElement(myDocStringOwner);
if (module == null) return prefix;
PyDocumentationSettings documentationSettings = PyDocumentationSettings.getInstance(module);
if (documentationSettings.isEpydocFormat(getFile())) {
prefix = "@";
}
return prefix;
}
public PyDocstringGenerator withSignatures() {
if (myFunction != null) {
PySignature signature = PySignatureCacheManager.getInstance(myProject).findSignature(myFunction);
addFunctionArguments(myFunction, signature);
}
return this;
}
}