Proper offsets for type placeholders inside generated docstrings even if additional indentation was added

Formatter can insert additional indentation inside multiline string
literal to align it with surrounding statement list. It happens before
live template to edit type annotations is inserted in document, thus
offsets saved previously in PyDoctstringGenerator become invalid. From
now on PyDocstringGenerator stores line and column after first
non-whitespace character for these annotations instead and converts them
to normal offsets considering any additional indentation inserted.
This commit is contained in:
Mikhail Golubev
2015-07-15 19:28:25 +03:00
parent 150a577535
commit fdb8d23960
6 changed files with 101 additions and 43 deletions
@@ -23,12 +23,13 @@ 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.editor.VisualPosition;
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.Couple;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.LineTokenizer;
import com.intellij.openapi.util.text.StringUtil;
@@ -50,6 +51,7 @@ import com.jetbrains.python.toolbox.Substring;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -69,7 +71,9 @@ public class PyDocstringGenerator {
private final Project myProject;
private PyStringLiteralExpression myDocStringExpression;
private final Map<String, Pair<Integer, Integer>> myParamTypesOffset = Maps.newHashMap();
// Formatter can increase indentation inside multiline docstring, that's why we don't keep plain
// text offsets and instead save pair of line and *unindented* column of that offset.
private final Map<String, Couple<VisualPosition>> myParamTypesOffset = Maps.newHashMap();
private PsiFile myFile;
private boolean myGenerateReturn;
@@ -249,26 +253,26 @@ public class PyDocstringGenerator {
if (documentationSettings.isPlain(getFile())) return replacementText.length() - 1;
}
int i = 0;
int line = StringUtil.getLineBreakCount(replacementText);
final List<String> unindentedLines = new ArrayList<String>();
for (DocstringParam param : paramsToAdd) {
replacementText.append(getPrefix());
replacementText.append(param.getKind());
replacementText.append(" ");
replacementText.append(param.getName());
replacementText.append(": ");
int startOffset = replacementText.length();
final StringBuilder lineBuilder = new StringBuilder();
lineBuilder.append(getPrefix());
lineBuilder.append(param.getKind());
lineBuilder.append(" ");
lineBuilder.append(param.getName());
lineBuilder.append(": ");
final int startOffset = lineBuilder.length();
int endOffset = startOffset;
if (param.getType() != null) {
replacementText.append(param.getType());
lineBuilder.append(param.getType());
endOffset += param.getType().length();
}
myParamTypesOffset.put(param.getName(), Pair.create(startOffset, endOffset));
i++;
if (i < paramsToAdd.size()) {
replacementText.append(ws);
}
myParamTypesOffset.put(param.getName(), Couple.of(new VisualPosition(line, startOffset), new VisualPosition(line, endOffset)));
unindentedLines.add(lineBuilder.toString());
line++;
}
StringUtil.join(unindentedLines, ws, replacementText);
if (myGenerateReturn && myDocStringOwner instanceof PyFunction) {
PyFunction function = (PyFunction)myDocStringOwner;
@@ -299,13 +303,14 @@ public class PyDocstringGenerator {
}
String ws = "\n";
if (whitespace != null) {
String[] spaces = whitespace.getText().split("\n");
if (spaces.length > 0) {
ws += spaces[spaces.length - 1];
final String whitespaceText = whitespace.getText();
final int index = whitespaceText.lastIndexOf('\n');
if (index >= 0) {
ws += whitespaceText.substring(index + 1);
}
}
else {
ws += StringUtil.repeat(" ", getIndentSize(myDocStringOwner));
ws += StringUtil.repeat(" ", calcExpectedIndentSize(myDocStringOwner));
}
return ws;
}
@@ -334,14 +339,46 @@ public class PyDocstringGenerator {
}
public int getStartOffset() {
Pair<Integer, Integer> offsets = getOffsets();
return offsets != null ? offsets.first : -1;
final Couple<VisualPosition> range = getOffsets();
return range == null ? -1 : visualPositionToOffset(range.getFirst());
}
private Pair<Integer, Integer> getOffsets() {
DocstringParam paramToEdit = getParamToEdit();
String paramName = paramToEdit.getName();
return myParamTypesOffset.get(paramName);
public int getEndOffset() {
final Couple<VisualPosition> range = getOffsets();
return range == null ? -1 : visualPositionToOffset(range.getSecond());
}
private int visualPositionToOffset(@NotNull VisualPosition pos) {
final String text = myDocStringExpression.getText();
int offset = offsetOfLineFeed(text, pos.getLine());
if (offset < 0) {
return -1;
}
offset++;
// Indentation consists solely of whitespaces
while (offset < text.length() && text.charAt(offset) == ' ') {
offset++;
}
if (offset == text.length()) {
return -1;
}
return offset + pos.getColumn();
}
private static int offsetOfLineFeed(@NotNull String s, int count) {
int result = -1;
for (int i = 0; i < count; i++) {
result = s.indexOf('\n', result + 1);
if (result < 0) {
break;
}
}
return result;
}
@Nullable
private Couple<VisualPosition> getOffsets() {
return myParamTypesOffset.get(getParamToEdit().getName());
}
private DocstringParam getParamToEdit() {
@@ -351,11 +388,6 @@ public class PyDocstringGenerator {
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;
@@ -410,10 +442,9 @@ public class PyDocstringGenerator {
PyFunction func = elementGenerator.createFromText(LanguageLevel.forElement(myFunction),
PyFunction.class,
"def " + myFunction.getName() + myFunction.getParameterList().getText()
+ ":\n" + StringUtil.repeat(" ", getIndentSize(myFunction))
+ ":\n" + StringUtil.repeat(" ", calcExpectedIndentSize(myFunction))
+ replacement + "\n" +
StringUtil.repeat(" ", getIndentSize(myFunction)) + list.getText()
);
StringUtil.repeat(" ", calcExpectedIndentSize(myFunction)) + list.getText());
myFunction = (PyFunction)myFunction.replace(func);
}
@@ -423,24 +454,25 @@ public class PyDocstringGenerator {
}
}
myFunction = CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(myFunction);
if (myFunction != null) {
myDocStringExpression = myFunction.getDocStringExpression();
}
CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(myFunction);
myDocStringExpression = myFunction.getDocStringExpression();
}
}
private static int getIndentSize(PyDocStringOwner function) {
CodeStyleSettings.IndentOptions indentOptions = CodeStyleSettingsManager.
getInstance(function.getProject()).getCurrentSettings().getIndentOptions(PythonFileType.INSTANCE);
private int calcExpectedIndentSize(@NotNull PyDocStringOwner function) {
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;
return indent * getIndentSizeFromSettings();
}
private int getIndentSizeFromSettings() {
final CodeStyleSettings codeStyleSettings = CodeStyleSettingsManager.getInstance(myProject).getCurrentSettings();
final CodeStyleSettings.IndentOptions indentOptions = codeStyleSettings.getIndentOptions(PythonFileType.INSTANCE);
return indentOptions.INDENT_SIZE;
}
private String getPrefix() {
@@ -0,0 +1,6 @@
def func(x):
"""
:rtype : object
"""
return 1
@@ -0,0 +1,6 @@
def func(x):
"""
:type x: object
"""
return 1
@@ -0,0 +1,2 @@
def fu<caret>nc(x):
return 1
@@ -0,0 +1,2 @@
def func(<caret>x):
return 1
@@ -318,6 +318,16 @@ public class PyIntentionTest extends PyTestCase {
doDocReferenceTest();
}
// PY-16456
public void testTypeInDocStringDifferentIndentationSize() {
doDocReferenceTest();
}
// PY-16456
public void testReturnTypeInDocStringDifferentIndentationSize() {
doDocReturnTypeTest();
}
public void testReturnTypeInDocstring() {
doDocReturnTypeTest();
}