From fdb8d23960378ad496ad175f371657fb54707ccb Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Wed, 15 Jul 2015 18:45:47 +0300 Subject: [PATCH] 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. --- .../documentation/PyDocstringGenerator.java | 118 +++++++++++------- ...TypeInDocStringDifferentIndentationSize.py | 6 + ...TypeInDocStringDifferentIndentationSize.py | 6 + ...TypeInDocStringDifferentIndentationSize.py | 2 + ...TypeInDocStringDifferentIndentationSize.py | 2 + .../python/intentions/PyIntentionTest.java | 10 ++ 6 files changed, 101 insertions(+), 43 deletions(-) create mode 100644 python/testData/intentions/afterReturnTypeInDocStringDifferentIndentationSize.py create mode 100644 python/testData/intentions/afterTypeInDocStringDifferentIndentationSize.py create mode 100644 python/testData/intentions/beforeReturnTypeInDocStringDifferentIndentationSize.py create mode 100644 python/testData/intentions/beforeTypeInDocStringDifferentIndentationSize.py diff --git a/python/src/com/jetbrains/python/documentation/PyDocstringGenerator.java b/python/src/com/jetbrains/python/documentation/PyDocstringGenerator.java index cdfbcdbcea8f..a3332350319a 100644 --- a/python/src/com/jetbrains/python/documentation/PyDocstringGenerator.java +++ b/python/src/com/jetbrains/python/documentation/PyDocstringGenerator.java @@ -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> 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> 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 unindentedLines = new ArrayList(); 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 offsets = getOffsets(); - return offsets != null ? offsets.first : -1; + final Couple range = getOffsets(); + return range == null ? -1 : visualPositionToOffset(range.getFirst()); } - private Pair getOffsets() { - DocstringParam paramToEdit = getParamToEdit(); - String paramName = paramToEdit.getName(); - return myParamTypesOffset.get(paramName); + public int getEndOffset() { + final Couple 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 getOffsets() { + return myParamTypesOffset.get(getParamToEdit().getName()); } private DocstringParam getParamToEdit() { @@ -351,11 +388,6 @@ public class PyDocstringGenerator { return myParams.get(0); } - public int getEndOffset() { - Pair 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() { diff --git a/python/testData/intentions/afterReturnTypeInDocStringDifferentIndentationSize.py b/python/testData/intentions/afterReturnTypeInDocStringDifferentIndentationSize.py new file mode 100644 index 000000000000..52900357d59c --- /dev/null +++ b/python/testData/intentions/afterReturnTypeInDocStringDifferentIndentationSize.py @@ -0,0 +1,6 @@ +def func(x): + """ + + :rtype : object + """ + return 1 \ No newline at end of file diff --git a/python/testData/intentions/afterTypeInDocStringDifferentIndentationSize.py b/python/testData/intentions/afterTypeInDocStringDifferentIndentationSize.py new file mode 100644 index 000000000000..de5fc24db1ae --- /dev/null +++ b/python/testData/intentions/afterTypeInDocStringDifferentIndentationSize.py @@ -0,0 +1,6 @@ +def func(x): + """ + + :type x: object + """ + return 1 \ No newline at end of file diff --git a/python/testData/intentions/beforeReturnTypeInDocStringDifferentIndentationSize.py b/python/testData/intentions/beforeReturnTypeInDocStringDifferentIndentationSize.py new file mode 100644 index 000000000000..d89af49bb95f --- /dev/null +++ b/python/testData/intentions/beforeReturnTypeInDocStringDifferentIndentationSize.py @@ -0,0 +1,2 @@ +def func(x): + return 1 \ No newline at end of file diff --git a/python/testData/intentions/beforeTypeInDocStringDifferentIndentationSize.py b/python/testData/intentions/beforeTypeInDocStringDifferentIndentationSize.py new file mode 100644 index 000000000000..eb6ed54d624c --- /dev/null +++ b/python/testData/intentions/beforeTypeInDocStringDifferentIndentationSize.py @@ -0,0 +1,2 @@ +def func(x): + return 1 \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java b/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java index 93e7333ae831..bb71bfad5f87 100644 --- a/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java +++ b/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java @@ -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(); }