diff --git a/python/src/META-INF/python-core.xml b/python/src/META-INF/python-core.xml index c533c0ec1b0d..72222837670b 100644 --- a/python/src/META-INF/python-core.xml +++ b/python/src/META-INF/python-core.xml @@ -576,6 +576,7 @@ + diff --git a/python/src/com/jetbrains/python/codeInsight/editorActions/PyTripleQuoteBackspaceDelegate.java b/python/src/com/jetbrains/python/codeInsight/editorActions/PyTripleQuoteBackspaceDelegate.java new file mode 100644 index 000000000000..6b63ff91b2bb --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/editorActions/PyTripleQuoteBackspaceDelegate.java @@ -0,0 +1,58 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.codeInsight.editorActions; + +import com.intellij.codeInsight.CodeInsightSettings; +import com.intellij.codeInsight.editorActions.BackspaceHandlerDelegate; +import com.intellij.codeInsight.editorActions.QuoteHandler; +import com.intellij.codeInsight.editorActions.TypedHandler; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.ex.EditorEx; +import com.intellij.openapi.editor.highlighter.HighlighterIterator; +import com.intellij.psi.PsiFile; +import com.jetbrains.python.editor.BaseQuoteHandler; + +public class PyTripleQuoteBackspaceDelegate extends BackspaceHandlerDelegate { + private boolean isTripleQuote; + + @Override + public void beforeCharDeleted(char c, PsiFile file, Editor editor) { + isTripleQuote = false; + if (c == '"' || c == '\'' && CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE) { + final QuoteHandler quoteHandler = TypedHandler.getQuoteHandler(file, editor); + if (quoteHandler == null || !(quoteHandler instanceof BaseQuoteHandler)) return; + + final int offset = editor.getCaretModel().getCurrentCaret().getOffset(); + String text = editor.getDocument().getText(); + boolean mayBeTripleQuote = offset >= 3 && offset + 2 < text.length(); + if (mayBeTripleQuote) { + HighlighterIterator iterator = ((EditorEx)editor).getHighlighter().createIterator(offset); + boolean hasTripleQuoteAfter = text.charAt(offset - 1) == c && text.charAt(offset - 2) == c && text.charAt(offset - 3) == c; + isTripleQuote = quoteHandler.isOpeningQuote(iterator, offset - 1) && hasTripleQuoteAfter; + } + } + } + + @Override + public boolean charDeleted(char c, PsiFile file, Editor editor) { + if (isTripleQuote) { + int offset = editor.getCaretModel().getCurrentCaret().getOffset(); + editor.getDocument().deleteString(offset - 2, offset + 3); + return true; + } + return false; + } +} diff --git a/python/src/com/jetbrains/python/editor/BaseQuoteHandler.java b/python/src/com/jetbrains/python/editor/BaseQuoteHandler.java index dac383da363b..0f0859ed300c 100644 --- a/python/src/com/jetbrains/python/editor/BaseQuoteHandler.java +++ b/python/src/com/jetbrains/python/editor/BaseQuoteHandler.java @@ -15,18 +15,21 @@ */ package com.jetbrains.python.editor; +import com.intellij.codeInsight.editorActions.MultiCharQuoteHandler; import com.intellij.codeInsight.editorActions.SimpleTokenSetQuoteHandler; import com.intellij.openapi.editor.Document; 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 org.jetbrains.annotations.Nullable; import java.util.Arrays; /** * @author yole */ -public class BaseQuoteHandler extends SimpleTokenSetQuoteHandler { +public class BaseQuoteHandler extends SimpleTokenSetQuoteHandler implements MultiCharQuoteHandler { private final char[] ourAutoClosingChars; // we add auto-close quotes before these @@ -44,17 +47,21 @@ public class BaseQuoteHandler extends SimpleTokenSetQuoteHandler { return false; } CharSequence text = document.getCharsSequence(); - if (offset + 1 >= text.length() || Arrays.binarySearch(ourAutoClosingChars, text.charAt(offset + 1)) >= 0) { + boolean mayBeSingleQuote = offset + 1 >= text.length() || Arrays.binarySearch(ourAutoClosingChars, text.charAt(offset + 1)) >= 0; + boolean mayBeTripleQuote = offset + 4 >= text.length() || Arrays.binarySearch(ourAutoClosingChars, text.charAt(offset + 4)) >= 0; + + if (mayBeTripleQuote) { char the_quote = text.charAt(offset); - // if we're next to two same quotes, don't auto-close, the user may want a triple quote + // if we're next to two same quotes, auto-close triple quote if ( offset >= 2 && text.charAt(offset - 1) == the_quote && text.charAt(offset - 2) == the_quote && (offset < 3 || text.charAt(offset - 3) != the_quote) ) { - return false; + return true; } + }if (mayBeSingleQuote ) { // handle string literal context if (super.isOpeningQuote(iterator, offset)) { return true; @@ -83,8 +90,18 @@ public class BaseQuoteHandler extends SimpleTokenSetQuoteHandler { @Override protected boolean isNonClosedLiteral(HighlighterIterator iterator, CharSequence chars) { - if (getLiteralStartOffset(chars, iterator.getStart()) >= iterator.getEnd() - 1) return true; - if (chars.charAt(iterator.getEnd() - 1) != '\"' && chars.charAt(iterator.getEnd() - 1) != '\'') return true; + int end = iterator.getEnd(); + if (getLiteralStartOffset(chars, iterator.getStart()) >= end - 1) return true; + char end_symbol = chars.charAt(end - 1); + if (end_symbol != '"' && end_symbol != '\'') return true; + + //for triple quoted string + if (end >= 3 && + (end_symbol == chars.charAt(end - 2)) && (chars.charAt(end - 2) == chars.charAt(end - 3)) && + (end < 4 || chars.charAt(end - 4) != end_symbol)) { + return true; + } + return false; } @@ -109,4 +126,23 @@ public class BaseQuoteHandler extends SimpleTokenSetQuoteHandler { return false; } + + @Nullable + @Override + public CharSequence getClosingQuote(HighlighterIterator iterator, int offset) { + Document document = iterator.getDocument(); + String text = document.getText(); + char the_quote = text.charAt(offset - 1); + if ( + offset >= 2 && + text.charAt(offset - 2) == the_quote && + text.charAt(offset - 3) == the_quote && + (offset < 4 || text.charAt(offset - 4) != the_quote)) { + return StringUtil.repeat(String.valueOf(the_quote), 3); + } + else if (super.isOpeningQuote(iterator, offset - 1)) { + return String.valueOf(the_quote); + } + return null; + } } diff --git a/python/testData/editing/closedTripleQuoteBackspace.after.py b/python/testData/editing/closedTripleQuoteBackspace.after.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/editing/closedTripleQuoteBackspace.before.py b/python/testData/editing/closedTripleQuoteBackspace.before.py new file mode 100644 index 000000000000..c1ce811b0188 --- /dev/null +++ b/python/testData/editing/closedTripleQuoteBackspace.before.py @@ -0,0 +1 @@ +'''''' \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyEditingTest.java b/python/testSrc/com/jetbrains/python/PyEditingTest.java index 88aad1427f61..22dc95ee7cbd 100644 --- a/python/testSrc/com/jetbrains/python/PyEditingTest.java +++ b/python/testSrc/com/jetbrains/python/PyEditingTest.java @@ -75,8 +75,12 @@ public class PyEditingTest extends PyTestCase { assertEquals("'' ", doTestTyping(" ", 0, '\'')); } - public void testNoClosingTriple() { - assertEquals("'''", doTestTyping("''", 2, '\'')); + public void testAutoCloseTriple() { + assertEquals("''''''", doTestTyping("''", 2, '\'')); + } + + public void testAutoRemoveTriple() { + doTestBackspace("closedTripleQuoteBackspace", new LogicalPosition(1, 3)); } public void testOvertypeFromInside() {