diff --git a/python/src/META-INF/python-core.xml b/python/src/META-INF/python-core.xml index 6c4665d91e20..8a9fe46241eb 100644 --- a/python/src/META-INF/python-core.xml +++ b/python/src/META-INF/python-core.xml @@ -18,6 +18,7 @@ + diff --git a/python/src/com/jetbrains/python/formatter/PyBlock.java b/python/src/com/jetbrains/python/formatter/PyBlock.java index d160749fbda8..1a867237eab3 100644 --- a/python/src/com/jetbrains/python/formatter/PyBlock.java +++ b/python/src/com/jetbrains/python/formatter/PyBlock.java @@ -516,7 +516,7 @@ public class PyBlock implements ASTBlock { } } - if (psi2 instanceof PsiComment && !hasLineBreaksBefore(psi2.getNode(), 1)) { + if (psi2 instanceof PsiComment && !hasLineBreaksBefore(psi2.getNode(), 1) && myContext.getPySettings().SPACE_BEFORE_NUMBER_SIGN) { return Spacing.createSpacing(2, 0, 0, false, 0); } } diff --git a/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java b/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java index 961ca725506a..88ddd414152e 100644 --- a/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java +++ b/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java @@ -38,6 +38,9 @@ public class PyCodeStyleSettings extends CustomCodeStyleSettings { public boolean NEW_LINE_AFTER_COLON = false; public boolean NEW_LINE_AFTER_COLON_MULTI_CLAUSE = true; + public boolean SPACE_AFTER_NUMBER_SIGN = true; + public boolean SPACE_BEFORE_NUMBER_SIGN = true; + public PyCodeStyleSettings(CodeStyleSettings container) { super("Python", container); } diff --git a/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java b/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java index c26a1a1742bd..9e7f97db5f8e 100644 --- a/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java +++ b/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java @@ -72,9 +72,12 @@ public class PyLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSettin consumer.showCustomOption(PyCodeStyleSettings.class, "SPACE_AROUND_EQ_IN_KEYWORD_ARGUMENT", "Around = in keyword argument", SPACES_AROUND_OPERATORS); consumer.showCustomOption(PyCodeStyleSettings.class, "SPACE_WITHIN_BRACES", "Braces", SPACES_WITHIN); - consumer.showCustomOption(PyCodeStyleSettings.class, "SPACE_BEFORE_PY_COLON", ApplicationBundle.message("checkbox.spaces.before.colon"), SPACES_OTHER); + consumer.showCustomOption(PyCodeStyleSettings.class, "SPACE_BEFORE_PY_COLON", + ApplicationBundle.message("checkbox.spaces.before.colon"), SPACES_OTHER); consumer.showCustomOption(PyCodeStyleSettings.class, "SPACE_AFTER_PY_COLON", ApplicationBundle.message("checkbox.spaces.after.colon"), SPACES_OTHER); consumer.showCustomOption(PyCodeStyleSettings.class, "SPACE_BEFORE_BACKSLASH", "Before '\\'", SPACES_OTHER); + consumer.showCustomOption(PyCodeStyleSettings.class, "SPACE_BEFORE_NUMBER_SIGN", "Before '#'", SPACES_OTHER); + consumer.showCustomOption(PyCodeStyleSettings.class, "SPACE_AFTER_NUMBER_SIGN", "After '#'", SPACES_OTHER); } else if (settingsType == SettingsType.BLANK_LINES_SETTINGS) { consumer.showStandardOptions("BLANK_LINES_AROUND_CLASS", @@ -112,7 +115,7 @@ public class PyLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSettin CommonCodeStyleSettings.IndentOptions indentOptions = defaultSettings.initIndentOptions(); indentOptions.INDENT_SIZE = 4; defaultSettings.ALIGN_MULTILINE_PARAMETERS_IN_CALLS = true; - return defaultSettings; + return defaultSettings; } @Override diff --git a/python/src/com/jetbrains/python/formatter/PyLineWrapPositionStrategy.java b/python/src/com/jetbrains/python/formatter/PyLineWrapPositionStrategy.java index d7fa5f0f5e7b..419d97c9e5d4 100644 --- a/python/src/com/jetbrains/python/formatter/PyLineWrapPositionStrategy.java +++ b/python/src/com/jetbrains/python/formatter/PyLineWrapPositionStrategy.java @@ -24,6 +24,7 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.text.CharArrayUtil; +import com.jetbrains.python.PyTokenTypes; import com.jetbrains.python.psi.StringLiteralExpression; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -75,11 +76,20 @@ public class PyLineWrapPositionStrategy extends GenericLineWrapPositionStrategy int maxPreferredOffset, boolean allowToBeyondMaxPreferredOffset, boolean virtual) { + int wrapPosition = super.calculateWrapPosition(document, project, startOffset, endOffset, maxPreferredOffset, allowToBeyondMaxPreferredOffset, virtual); if (wrapPosition < 0) return wrapPosition; final CharSequence text = document.getCharsSequence(); + if (wrapPosition > 0) { + char charBefore = text.charAt(wrapPosition - 1); + if (charBefore == '\'' || charBefore == '"') { + //don't wrap the first char of string literal + return wrapPosition + 1; + } + } + char c = text.charAt(wrapPosition); if (!StringUtil.isWhiteSpace(c) || project == null) { return wrapPosition; diff --git a/python/src/com/jetbrains/python/formatter/PyPreFormatProcessor.java b/python/src/com/jetbrains/python/formatter/PyPreFormatProcessor.java new file mode 100644 index 000000000000..ee56016d1df9 --- /dev/null +++ b/python/src/com/jetbrains/python/formatter/PyPreFormatProcessor.java @@ -0,0 +1,98 @@ +/* + * Copyright 2000-2014 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.formatter; + +import com.intellij.lang.ASTNode; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiComment; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.codeStyle.CodeStyleSettings; +import com.intellij.psi.codeStyle.CodeStyleSettingsManager; +import com.intellij.psi.impl.source.codeStyle.PreFormatProcessor; +import com.jetbrains.python.PythonLanguage; +import com.jetbrains.python.psi.LanguageLevel; +import com.jetbrains.python.psi.PyElementGenerator; +import com.jetbrains.python.psi.PyRecursiveElementVisitor; +import org.jetbrains.annotations.NotNull; + +/** + * @author traff + */ +public class PyPreFormatProcessor implements PreFormatProcessor { + @NotNull + @Override + public TextRange process(@NotNull ASTNode element, @NotNull TextRange range) { + PsiElement psiElement = element.getPsi(); + if (psiElement == null) return range; + + if (!psiElement.getLanguage().is(PythonLanguage.getInstance())) return range; + + PsiFile file = psiElement.isValid() ? psiElement.getContainingFile() : null; + if (file == null) return range; + + Project project = psiElement.getProject(); + + return new PyCommentFormatter(project).process(psiElement, range); + } + + /** + * @author traff + */ + public static class PyCommentFormatter extends PyRecursiveElementVisitor { + private final Project myProject; + private final CodeStyleSettings mySettings; + private final PyCodeStyleSettings myPyCodeStyleSettings; + private TextRange myRange; + private int myDelta = 0; + + public PyCommentFormatter(Project project) { + myProject = project; + mySettings = CodeStyleSettingsManager.getSettings(project); + myPyCodeStyleSettings = mySettings.getCustomSettings(PyCodeStyleSettings.class); + } + + public TextRange process(PsiElement element, TextRange range) { + if (!myPyCodeStyleSettings.SPACE_AFTER_NUMBER_SIGN) { + return range; + } + myRange = range; + element.accept(this); + return TextRange.create(range.getStartOffset(), range.getEndOffset() + myDelta); + } + + @Override + public void visitComment(PsiComment element) { + if (!myRange.contains(element.getTextRange())) { + return; + } + String text = element.getText(); + int commentStart = text.indexOf('#'); + if (commentStart != -1 && (commentStart + 1) < text.length()) { + String commentText = StringUtil.trimLeading(text.substring(commentStart + 1)); + + String newText = "# " + commentText; + if (!newText.equals(text)) { + myDelta += newText.length() - text.length(); + element.replace( + PyElementGenerator.getInstance(myProject).createFromText(LanguageLevel.getDefault(), PsiComment.class, newText)); + } + } + } + } +} diff --git a/python/testData/formatter/twoLinesBetweenTopLevelDeclarationsWithComment.py b/python/testData/formatter/twoLinesBetweenTopLevelDeclarationsWithComment.py index a0f52904114e..c9082539ab46 100644 --- a/python/testData/formatter/twoLinesBetweenTopLevelDeclarationsWithComment.py +++ b/python/testData/formatter/twoLinesBetweenTopLevelDeclarationsWithComment.py @@ -1,7 +1,7 @@ class A(object): pass -#comment +# comment def one(): pass @@ -9,10 +9,10 @@ def one(): def two(): pass -#comment +# comment class B(object): pass -#comment +# comment class C(object): pass \ No newline at end of file diff --git a/python/testData/formatter/twoLinesBetweenTopLevelDeclarationsWithComment_after.py b/python/testData/formatter/twoLinesBetweenTopLevelDeclarationsWithComment_after.py index d582036d8654..ba0e69717738 100644 --- a/python/testData/formatter/twoLinesBetweenTopLevelDeclarationsWithComment_after.py +++ b/python/testData/formatter/twoLinesBetweenTopLevelDeclarationsWithComment_after.py @@ -2,7 +2,7 @@ class A(object): pass -#comment +# comment def one(): pass @@ -12,11 +12,11 @@ def two(): pass -#comment +# comment class B(object): pass -#comment +# comment class C(object): pass \ No newline at end of file diff --git a/python/testData/refactoring/extractmethod/Comment2.after.py b/python/testData/refactoring/extractmethod/Comment2.after.py index 5e5fdc3c6a8c..c5cc3a7ff224 100644 --- a/python/testData/refactoring/extractmethod/Comment2.after.py +++ b/python/testData/refactoring/extractmethod/Comment2.after.py @@ -1,6 +1,6 @@ class Foo(): def baz(): - tmp = "!" #try to extract this assignmet, either with or without this comment + tmp = "!" # try to extract this assignment, either with or without this comment baz() diff --git a/python/testData/refactoring/extractmethod/Comment2.before.py b/python/testData/refactoring/extractmethod/Comment2.before.py index d43386aff93c..23ba13de61f7 100644 --- a/python/testData/refactoring/extractmethod/Comment2.before.py +++ b/python/testData/refactoring/extractmethod/Comment2.before.py @@ -1,5 +1,5 @@ class Foo(): - tmp = "!" #try to extract this assignmet, either with or without this comment + tmp = "!" #try to extract this assignment, either with or without this comment def bar(self): pass \ No newline at end of file diff --git a/python/testData/refactoring/extractmethod/ElseBody.after.py b/python/testData/refactoring/extractmethod/ElseBody.after.py index 57b5fa011715..24e387dd3633 100644 --- a/python/testData/refactoring/extractmethod/ElseBody.after.py +++ b/python/testData/refactoring/extractmethod/ElseBody.after.py @@ -1,5 +1,5 @@ def baz(f_new): - length = len(f_new.readlines()) #<---extract something from here + length = len(f_new.readlines()) # <---extract something from here print("hi from else") diff --git a/python/testData/wrap/DontWrapStartOfString.after.py b/python/testData/wrap/DontWrapStartOfString.after.py new file mode 100644 index 000000000000..5fb7919f6566 --- /dev/null +++ b/python/testData/wrap/DontWrapStartOfString.after.py @@ -0,0 +1,2 @@ +call_command('.' + 'regressiontests_some_long_text_here_to_test_right_margin_some_long_text_here_to_test_right_margin_some_long_text_here_to_test_right_margin') \ No newline at end of file diff --git a/python/testData/wrap/DontWrapStartOfString.py b/python/testData/wrap/DontWrapStartOfString.py new file mode 100644 index 000000000000..06487362ca6a --- /dev/null +++ b/python/testData/wrap/DontWrapStartOfString.py @@ -0,0 +1 @@ +call_command('.regressiontests') \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyFormatterTest.java b/python/testSrc/com/jetbrains/python/PyFormatterTest.java index 815f61bd311f..f59fd49b2d03 100644 --- a/python/testSrc/com/jetbrains/python/PyFormatterTest.java +++ b/python/testSrc/com/jetbrains/python/PyFormatterTest.java @@ -251,7 +251,7 @@ public class PyFormatterTest extends PyTestCase { " desired_response_parameters,\n" + " inverse_filter_length, \n" + " observed_impulse_response):\n" + - " # Extract from here to ...\n" + + " # Extract from here to ...\n" + " desired_impulse_response = {'dirac, 'gaussian', logistic_derivative'}\n" + "return desired, o"; @@ -264,7 +264,7 @@ public class PyFormatterTest extends PyTestCase { " desired_response_parameters,\n" + " inverse_filter_length,\n" + " observed_impulse_response):\n" + - " # Extract from here to ...\n" + + " # Extract from here to ...\n" + " desired_impulse_response = {'dirac, '\n" + " gaussian\n" + " ', logistic_derivative'}\n" + diff --git a/python/testSrc/com/jetbrains/python/PyWrapTest.java b/python/testSrc/com/jetbrains/python/PyWrapTest.java index 39606c3b5ca2..05c21b8af21a 100644 --- a/python/testSrc/com/jetbrains/python/PyWrapTest.java +++ b/python/testSrc/com/jetbrains/python/PyWrapTest.java @@ -64,6 +64,10 @@ public class PyWrapTest extends PyTestCase { doTest(" AND field"); } + public void testDontWrapStartOfString() { // PY-9436 + doTest("_some_long_text_here_to_test_right_margin_some_long_text_here_to_test_right_margin_some_long_text_here_to_test_right_margin"); + } + public void testWrapRightMargin() { final CodeStyleSettings settings = CodeStyleSettingsManager.getInstance(myFixture.getProject()).getCurrentSettings(); @@ -83,7 +87,6 @@ public class PyWrapTest extends PyTestCase { settings.RIGHT_MARGIN = oldValue; settings.WRAP_WHEN_TYPING_REACHES_RIGHT_MARGIN = oldMarginValue; } - } private void doTest(final String textToType) {