diff --git a/python/src/com/jetbrains/python/editor/PythonEnterHandler.java b/python/src/com/jetbrains/python/editor/PythonEnterHandler.java index f4184c6ae65c..209af09f6b40 100644 --- a/python/src/com/jetbrains/python/editor/PythonEnterHandler.java +++ b/python/src/com/jetbrains/python/editor/PythonEnterHandler.java @@ -29,6 +29,7 @@ import com.intellij.openapi.editor.actionSystem.EditorActionHandler; import com.intellij.openapi.editor.actions.SplitLineAction; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.LineTokenizer; import com.intellij.psi.*; import com.intellij.psi.impl.source.tree.TreeUtil; import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil; @@ -106,7 +107,7 @@ public class PythonEnterHandler extends EnterHandlerDelegateAdapter { comment = file.findElementAt(offset - 1); } int expectedStringStart = editor.getCaretModel().getOffset() - 3; // """ or ''' - if (comment != null && atDocCommentStart(comment, expectedStringStart)) { + if (comment != null && atDocCommentStart(comment, expectedStringStart, doc)) { insertDocStringStub(editor, comment); return Result.Continue; } @@ -379,14 +380,22 @@ public class PythonEnterHandler extends EnterHandlerDelegateAdapter { } } - public static boolean atDocCommentStart(@NotNull PsiElement element, int offset) { + public static boolean atDocCommentStart(@NotNull PsiElement element, int firstQuoteOffset, @NotNull Document document) { + if (firstQuoteOffset < 0 || firstQuoteOffset > document.getTextLength() - 3) { + return false; + } + final String quotes = document.getText(TextRange.from(firstQuoteOffset, 3)); + if (!quotes.equals("\"\"\"") && !quotes.equals("'''")) { + return false; + } final PyStringLiteralExpression pyString = DocStringUtil.getParentDefinitionDocString(element); if (pyString != null) { - String text = element.getText(); - final int prefixLength = PyStringLiteralExpressionImpl.getPrefixLength(text); - text = text.substring(prefixLength); - if (pyString.getText().endsWith(text) && (text.startsWith("\"\"\"") || text.startsWith("'''"))) { - if (offset == pyString.getTextOffset() + prefixLength) { + String nodeText = element.getText(); + final int prefixLength = PyStringLiteralExpressionImpl.getPrefixLength(nodeText); + nodeText = nodeText.substring(prefixLength); + final String literalText = pyString.getText(); + if (literalText.endsWith(nodeText) && nodeText.startsWith(quotes)) { + if (firstQuoteOffset == pyString.getTextOffset() + prefixLength) { PsiErrorElement error = PsiTreeUtil.getNextSiblingOfType(pyString, PsiErrorElement.class); if (error != null) { return true; @@ -396,9 +405,20 @@ public class PythonEnterHandler extends EnterHandlerDelegateAdapter { return true; } - if (text.length() < 6 || (!text.endsWith("\"\"\"") && !text.endsWith("'''"))) { + if (nodeText.length() < 6 || !nodeText.endsWith(quotes)) { return true; } + // Sometimes if incomplete docstring is followed by another declaration with a docstring, it might be treated + // as complete docstring, because we can't understand that closing quotes actually belong to another docstring. + final String docstringIndent = PyIndentUtil.getLineIndent(document, document.getLineNumber(firstQuoteOffset)); + for (String line : LineTokenizer.tokenizeIntoList(nodeText, false)) { + final String lineIndent = (String)PyIndentUtil.getLineIndent(line); + final String lineContent = line.substring(lineIndent.length()); + if ((lineContent.startsWith("def ") || lineContent.startsWith("class ")) && + docstringIndent.length() > lineIndent.length() && docstringIndent.startsWith(lineIndent)) { + return true; + } + } } } } diff --git a/python/src/com/jetbrains/python/editor/PythonSpaceHandler.java b/python/src/com/jetbrains/python/editor/PythonSpaceHandler.java index e1c36da2f456..c2bb1d39a5c9 100644 --- a/python/src/com/jetbrains/python/editor/PythonSpaceHandler.java +++ b/python/src/com/jetbrains/python/editor/PythonSpaceHandler.java @@ -44,10 +44,10 @@ public class PythonSpaceHandler extends TypedHandlerDelegate { } if (element == null) return Result.CONTINUE; int expectedStringStart = offset - 4; // """ or ''' plus space char - if (PythonEnterHandler.atDocCommentStart(element, expectedStringStart)) { + final Document document = editor.getDocument(); + if (PythonEnterHandler.atDocCommentStart(element, expectedStringStart, document)) { final PyDocStringOwner docOwner = PsiTreeUtil.getParentOfType(element, PyDocStringOwner.class); if (docOwner != null) { - final Document document = editor.getDocument(); final String quotes = document.getText(TextRange.from(expectedStringStart, 3)); final String docString = PyDocstringGenerator.forDocStringOwner(docOwner) .forceNewMode() diff --git a/python/src/com/jetbrains/python/psi/PyIndentUtil.java b/python/src/com/jetbrains/python/psi/PyIndentUtil.java index c168e3773194..1f4cce9e94db 100644 --- a/python/src/com/jetbrains/python/psi/PyIndentUtil.java +++ b/python/src/com/jetbrains/python/psi/PyIndentUtil.java @@ -16,7 +16,9 @@ package com.jetbrains.python.psi; import com.google.common.collect.Iterables; +import com.intellij.openapi.editor.Document; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.LineTokenizer; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; @@ -195,4 +197,11 @@ public class PyIndentUtil { } return StringUtil.notNullize(minIndent); } + + @NotNull + public static String getLineIndent(@NotNull Document document, int lineNumber) { + final TextRange lineRange = TextRange.create(document.getLineStartOffset(lineNumber), document.getLineEndOffset(lineNumber)); + final String line = document.getText(lineRange); + return (String)getLineIndent(line); + } } diff --git a/python/testData/editing/enterDocstringStubWhenClassDocstringBelow.after.py b/python/testData/editing/enterDocstringStubWhenClassDocstringBelow.after.py new file mode 100644 index 000000000000..3d3792855d89 --- /dev/null +++ b/python/testData/editing/enterDocstringStubWhenClassDocstringBelow.after.py @@ -0,0 +1,12 @@ +def f(): + """ + + Returns: + + """ + + +class Class: + """ + bar + """ \ No newline at end of file diff --git a/python/testData/editing/enterDocstringStubWhenClassDocstringBelow.py b/python/testData/editing/enterDocstringStubWhenClassDocstringBelow.py new file mode 100644 index 000000000000..1bb30a27dd55 --- /dev/null +++ b/python/testData/editing/enterDocstringStubWhenClassDocstringBelow.py @@ -0,0 +1,8 @@ +def f(): + """ + + +class Class: + """ + bar + """ \ No newline at end of file diff --git a/python/testData/editing/enterDocstringStubWhenFunctionDocstringBelow.after.py b/python/testData/editing/enterDocstringStubWhenFunctionDocstringBelow.after.py new file mode 100644 index 000000000000..4623c4aaaf44 --- /dev/null +++ b/python/testData/editing/enterDocstringStubWhenFunctionDocstringBelow.after.py @@ -0,0 +1,12 @@ +def f(): + """ + + Returns: + + """ + + +def g(): + """ + bar + """ \ No newline at end of file diff --git a/python/testData/editing/enterDocstringStubWhenFunctionDocstringBelow.py b/python/testData/editing/enterDocstringStubWhenFunctionDocstringBelow.py new file mode 100644 index 000000000000..96da1df1ee69 --- /dev/null +++ b/python/testData/editing/enterDocstringStubWhenFunctionDocstringBelow.py @@ -0,0 +1,8 @@ +def f(): + """ + + +def g(): + """ + bar + """ \ No newline at end of file diff --git a/python/testData/editing/enterNoDocstringStubWhenCodeExampleInDocstring.after.py b/python/testData/editing/enterNoDocstringStubWhenCodeExampleInDocstring.after.py new file mode 100644 index 000000000000..df3ca8813b8b --- /dev/null +++ b/python/testData/editing/enterNoDocstringStubWhenCodeExampleInDocstring.after.py @@ -0,0 +1,15 @@ +def f(): + """ + + Monospaced ``func`` and func + + Example: + + :: + + def func(): + pass + + class Class(): + pass + """ diff --git a/python/testData/editing/enterNoDocstringStubWhenCodeExampleInDocstring.py b/python/testData/editing/enterNoDocstringStubWhenCodeExampleInDocstring.py new file mode 100644 index 000000000000..be7787cf0cea --- /dev/null +++ b/python/testData/editing/enterNoDocstringStubWhenCodeExampleInDocstring.py @@ -0,0 +1,14 @@ +def f(): + """ + Monospaced ``func`` and func + + Example: + + :: + + def func(): + pass + + class Class(): + pass + """ diff --git a/python/testSrc/com/jetbrains/python/PyEditingTest.java b/python/testSrc/com/jetbrains/python/PyEditingTest.java index 0c3c3d796254..c5b0d8ec55de 100644 --- a/python/testSrc/com/jetbrains/python/PyEditingTest.java +++ b/python/testSrc/com/jetbrains/python/PyEditingTest.java @@ -257,6 +257,21 @@ public class PyEditingTest extends PyTestCase { doDocStringTypingTest("\nparam", DocStringFormat.GOOGLE); } + // PY-17183 + public void testEnterDocstringStubWhenFunctionDocstringBelow() { + doDocStringTypingTest("\n", DocStringFormat.GOOGLE); + } + + // PY-17183 + public void testEnterDocstringStubWhenClassDocstringBelow() { + doDocStringTypingTest("\n", DocStringFormat.GOOGLE); + } + + // PY-17183 + public void testEnterNoDocstringStubWhenCodeExampleInDocstring() { + doDocStringTypingTest("\n", DocStringFormat.GOOGLE); + } + public void testEnterInString() { // PY-1738 doTestEnter("a = \"some string\"", "a = \"some \" \\\n" + " \"string\"");