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(); }