PY-18486 Better solution: on enter generate content for empty docstrings too
authorMikhail Golubev <mikhail.golubev@jetbrains.com>
Wed, 30 Mar 2016 12:38:10 +0000 (15:38 +0300)
committerMikhail Golubev <mikhail.golubev@jetbrains.com>
Wed, 30 Mar 2016 12:47:28 +0000 (15:47 +0300)
I reverted previously modified test data in PyEditingTest, since
this fix doesn't affect top-level string literals treated as module
docstrings. They are processed in BaseQuoteHandler as earlier.

python/src/com/jetbrains/python/editor/BaseQuoteHandler.java
python/src/com/jetbrains/python/editor/PythonEnterHandler.java
python/src/com/jetbrains/python/editor/PythonSpaceHandler.java
python/testSrc/com/jetbrains/python/PyEditingTest.java

index 79ededc37f700a9e4aa5f980c8c8178dafd488d3..c5d14dfa8b53e5dcf5f680fdabca93980b13773f 100644 (file)
@@ -22,7 +22,6 @@ import com.intellij.openapi.editor.highlighter.HighlighterIterator;
 import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.psi.tree.IElementType;
 import com.intellij.psi.tree.TokenSet;
-import com.jetbrains.python.PyTokenTypes;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.Arrays;
@@ -35,7 +34,7 @@ public class BaseQuoteHandler extends SimpleTokenSetQuoteHandler implements Mult
   private final char[] ourAutoClosingChars; // we add auto-close quotes before these
 
   public BaseQuoteHandler(TokenSet tokenSet, char[] autoClosingChars) {
-    super(TokenSet.andNot(tokenSet, TokenSet.create(PyTokenTypes.DOCSTRING)));
+    super(tokenSet);
     ourAutoClosingChars = autoClosingChars;
     Arrays.sort(ourAutoClosingChars);
   }
index 209af09f6b408fabec856b803bf1aa8b0f306ab6..1ad667c4fe39224c590661cc5fb8329ea11af7e2 100644 (file)
@@ -107,9 +107,12 @@ public class PythonEnterHandler extends EnterHandlerDelegateAdapter {
         comment = file.findElementAt(offset - 1);
       }
       int expectedStringStart = editor.getCaretModel().getOffset() - 3; // """ or '''
-      if (comment != null && atDocCommentStart(comment, expectedStringStart, doc)) {
-        insertDocStringStub(editor, comment);
-        return Result.Continue;
+      if (comment != null) {
+        final DocstringState state = canGenerateDocstring(comment, expectedStringStart, doc);
+        if (state != DocstringState.NONE) {
+          insertDocStringStub(editor, comment, state);
+          return Result.Continue;
+        }
       }
     }
 
@@ -259,17 +262,23 @@ public class PythonEnterHandler extends EnterHandlerDelegateAdapter {
     return wrappableBefore != wrappableAfter;
   }
 
-  private static void insertDocStringStub(Editor editor, PsiElement element) {
+  private static void insertDocStringStub(Editor editor, PsiElement element, DocstringState state) {
     PyDocStringOwner docOwner = PsiTreeUtil.getParentOfType(element, PyDocStringOwner.class);
     if (docOwner != null) {
       final int caretOffset = editor.getCaretModel().getOffset();
-      final String quotes = editor.getDocument().getText(TextRange.from(caretOffset - 3, 3));
+      final Document document = editor.getDocument();
+      final String quotes = document.getText(TextRange.from(caretOffset - 3, 3));
       final String docString = PyDocstringGenerator.forDocStringOwner(docOwner)
         .withInferredParameters(true)
         .withQuotes(quotes)
         .forceNewMode()
         .buildDocString();
-      editor.getDocument().replaceString(caretOffset - 3, caretOffset, docString);
+      if (state == DocstringState.INCOMPLETE) {
+        document.insertString(caretOffset, docString.substring(3));
+      }
+      else if (state == DocstringState.EMPTY) {
+        document.replaceString(caretOffset, caretOffset + 3, docString.substring(3));
+      }
     }
   }
 
@@ -379,34 +388,46 @@ public class PythonEnterHandler extends EnterHandlerDelegateAdapter {
       }
     }
   }
+  
+  enum DocstringState {
+    NONE,
+    INCOMPLETE,
+    EMPTY
+  }
 
-  public static boolean atDocCommentStart(@NotNull PsiElement element, int firstQuoteOffset, @NotNull Document document) {
+  @NotNull
+  public static DocstringState canGenerateDocstring(@NotNull PsiElement element, int firstQuoteOffset, @NotNull Document document) {
     if (firstQuoteOffset < 0 || firstQuoteOffset > document.getTextLength() - 3) {
-      return false;
+      return DocstringState.NONE;
     }
     final String quotes = document.getText(TextRange.from(firstQuoteOffset, 3));
     if (!quotes.equals("\"\"\"") && !quotes.equals("'''")) {
-      return false;
+      return DocstringState.NONE;
     }
     final PyStringLiteralExpression pyString = DocStringUtil.getParentDefinitionDocString(element);
     if (pyString != null) {
+
       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;
+          if (error == null) {
+            error = PsiTreeUtil.getNextSiblingOfType(pyString.getParent(), PsiErrorElement.class);
           }
-          error = PsiTreeUtil.getNextSiblingOfType(pyString.getParent(), PsiErrorElement.class);
           if (error != null) {
-            return true;
+            return DocstringState.INCOMPLETE;
           }
-
+  
+          if (nodeText.equals(quotes + quotes)) {
+            return DocstringState.EMPTY;
+          }
+  
           if (nodeText.length() < 6 || !nodeText.endsWith(quotes)) {
-            return true;
+            return DocstringState.INCOMPLETE;
           }
           // 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.
@@ -416,12 +437,12 @@ public class PythonEnterHandler extends EnterHandlerDelegateAdapter {
             final String lineContent = line.substring(lineIndent.length());
             if ((lineContent.startsWith("def ") || lineContent.startsWith("class ")) &&
                 docstringIndent.length() > lineIndent.length() && docstringIndent.startsWith(lineIndent)) {
-              return true;
+              return DocstringState.INCOMPLETE;
             }
           }
         }
       }
     }
-    return false;
+    return DocstringState.NONE;
   }
 }
index c2bb1d39a5c9629868ebafdb5c658bd38e06fa0d..ce01d2500c3dc038e7d0940bd80755f61ce4ba20 100644 (file)
@@ -26,6 +26,7 @@ import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiFile;
 import com.intellij.psi.util.PsiTreeUtil;
 import com.jetbrains.python.documentation.docstrings.PyDocstringGenerator;
+import com.jetbrains.python.editor.PythonEnterHandler.DocstringState;
 import com.jetbrains.python.psi.PyDocStringOwner;
 import org.jetbrains.annotations.NotNull;
 
@@ -45,7 +46,7 @@ public class PythonSpaceHandler extends TypedHandlerDelegate {
       if (element == null) return Result.CONTINUE;
       int expectedStringStart = offset - 4;        // """ or ''' plus space char
       final Document document = editor.getDocument();
-      if (PythonEnterHandler.atDocCommentStart(element, expectedStringStart, document)) {
+      if (PythonEnterHandler.canGenerateDocstring(element, expectedStringStart, document) == DocstringState.INCOMPLETE) {
         final PyDocStringOwner docOwner = PsiTreeUtil.getParentOfType(element, PyDocStringOwner.class);
         if (docOwner != null) {
           final String quotes = document.getText(TextRange.from(expectedStringStart, 3));
index 2c80434e11be0655d747226125bfd4e322608dc1..a48309cd5479dcbb4b91a588818f48f4296fb232 100644 (file)
@@ -44,7 +44,7 @@ public class PyEditingTest extends PyTestCase {
   }
 
   public void testPairedQuotesInRawString() {   // PY-263
-    assertEquals("x = r''", doTestTyping("x = r", 5, '\''));
+    assertEquals("r''", doTestTyping("r", 1, '\''));
   }
 
   public void testQuotesInString() {   // PY-5041
@@ -60,23 +60,23 @@ public class PyEditingTest extends PyTestCase {
   }
 
   public void testAutoClosingQuoteAtRBracket() {
-    assertEquals("x = '']", doTestTyping("x = ]", 4, '\''));
+    assertEquals("'']", doTestTyping("]", 0, '\''));
   }
 
   public void testAutoClosingQuoteAtRParen() {
-    assertEquals("x = '')", doTestTyping("x = )", 4, '\''));
+    assertEquals("'')", doTestTyping(")", 0, '\''));
   }
 
   public void testAutoClosingQuoteAtComma() {
-    assertEquals("x = '',", doTestTyping("x = ,", 4, '\''));
+    assertEquals("'',", doTestTyping(",", 0, '\''));
   }
 
   public void testAutoClosingQuoteAtSpace() {
-    assertEquals("x = '' ", doTestTyping("x =  ", 4, '\''));
+    assertEquals("'' ", doTestTyping(" ", 0, '\''));
   }
 
   public void testAutoCloseTriple() {
-    assertEquals("x = ''''''", doTestTyping("x = ''", 6, '\''));
+    assertEquals("''''''", doTestTyping("''", 2, '\''));
   }
 
   public void testAutoRemoveTriple() {
@@ -84,7 +84,7 @@ public class PyEditingTest extends PyTestCase {
   }
 
   public void testOvertypeFromInside() {
-    assertEquals("x = ''", doTestTyping("x = ''", 5, '\''));
+    assertEquals("''", doTestTyping("''", 1, '\''));
   }
 
   public void testGreedyBackspace() {  // PY-254