From 747b033995618ccf8eeffb0baa4ab326e3e43c78 Mon Sep 17 00:00:00 2001 From: "Denis.Zhdanov" Date: Fri, 3 Aug 2012 16:54:43 +0400 Subject: [PATCH] IDEA-87087 Code formatting corrupts the code Corrected tab <-> spaces indents processing algorithm --- .../codeStyle/BracePostFormatProcessor.java | 7 +- .../codeStyle/ImportPostFormatProcessor.java | 7 +- .../codeStyle/TabPostFormatProcessor.java | 533 ++++++++++++++++++ .../codeStyle/TabPostFormatProcessorTest.java | 403 +++++++++++++ .../intellij/openapi/editor/impl/LineSet.java | 6 +- .../src/com/intellij/psi/impl/DebugUtil.java | 9 +- .../codeStyle/CodeStyleSettingsManager.java | 4 +- .../codeStyle/CodeStyleManagerImpl.java | 5 +- .../source/codeStyle/PostFormatProcessor.java | 7 +- .../GroovyBracePostFormatProcessor.java | 5 +- resources/src/META-INF/IdeaPlugin.xml | 1 + 11 files changed, 968 insertions(+), 19 deletions(-) create mode 100644 java/java-impl/src/com/intellij/psi/impl/source/codeStyle/TabPostFormatProcessor.java create mode 100644 java/java-tests/testSrc/com/intellij/psi/impl/source/codeStyle/TabPostFormatProcessorTest.java diff --git a/java/java-impl/src/com/intellij/psi/impl/source/codeStyle/BracePostFormatProcessor.java b/java/java-impl/src/com/intellij/psi/impl/source/codeStyle/BracePostFormatProcessor.java index 2db288d3e59c..4b798ab40414 100644 --- a/java/java-impl/src/com/intellij/psi/impl/source/codeStyle/BracePostFormatProcessor.java +++ b/java/java-impl/src/com/intellij/psi/impl/source/codeStyle/BracePostFormatProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2009 JetBrains s.r.o. + * Copyright 2000-2012 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. @@ -19,15 +19,16 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.codeStyle.CodeStyleSettings; import com.intellij.openapi.util.TextRange; +import org.jetbrains.annotations.NotNull; public class BracePostFormatProcessor implements PostFormatProcessor { @Override - public PsiElement processElement(PsiElement source, CodeStyleSettings settings) { + public PsiElement processElement(@NotNull PsiElement source, @NotNull CodeStyleSettings settings) { return new BraceEnforcer(settings).process(source); } @Override - public TextRange processText(PsiFile source, TextRange rangeToReformat, CodeStyleSettings settings) { + public TextRange processText(@NotNull PsiFile source, @NotNull TextRange rangeToReformat, @NotNull CodeStyleSettings settings) { return new BraceEnforcer(settings).processText(source, rangeToReformat); } } diff --git a/java/java-impl/src/com/intellij/psi/impl/source/codeStyle/ImportPostFormatProcessor.java b/java/java-impl/src/com/intellij/psi/impl/source/codeStyle/ImportPostFormatProcessor.java index bcb06d0f0898..8a15ed61afcb 100644 --- a/java/java-impl/src/com/intellij/psi/impl/source/codeStyle/ImportPostFormatProcessor.java +++ b/java/java-impl/src/com/intellij/psi/impl/source/codeStyle/ImportPostFormatProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2009 JetBrains s.r.o. + * Copyright 2000-2012 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. @@ -19,15 +19,16 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.codeStyle.CodeStyleSettings; import com.intellij.openapi.util.TextRange; +import org.jetbrains.annotations.NotNull; public class ImportPostFormatProcessor implements PostFormatProcessor { @Override - public PsiElement processElement(PsiElement source, CodeStyleSettings settings) { + public PsiElement processElement(@NotNull PsiElement source, @NotNull CodeStyleSettings settings) { return new ImportsFormatter(settings, source.getContainingFile()).process(source); } @Override - public TextRange processText(PsiFile source, TextRange rangeToReformat, CodeStyleSettings settings) { + public TextRange processText(@NotNull PsiFile source, @NotNull TextRange rangeToReformat, @NotNull CodeStyleSettings settings) { return new ImportsFormatter(settings, source.getContainingFile()).processText(source, rangeToReformat); } } diff --git a/java/java-impl/src/com/intellij/psi/impl/source/codeStyle/TabPostFormatProcessor.java b/java/java-impl/src/com/intellij/psi/impl/source/codeStyle/TabPostFormatProcessor.java new file mode 100644 index 000000000000..a06a63facb47 --- /dev/null +++ b/java/java-impl/src/com/intellij/psi/impl/source/codeStyle/TabPostFormatProcessor.java @@ -0,0 +1,533 @@ +/* + * Copyright 2000-2012 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.intellij.psi.impl.source.codeStyle; + +import com.intellij.lang.ASTNode; +import com.intellij.lang.Language; +import com.intellij.lang.java.JavaLanguage; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.codeStyle.CodeStyleSettings; +import com.intellij.psi.formatter.FormatterUtil; +import com.intellij.psi.impl.source.tree.TreeUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * This class handles a use-case when reformatted text conflicts with 'use tab' code style setting. E.g. target text uses + * tabs for indentation but our code style is configured to use spaces. + *

+ * We already have corresponding support at the block level but it's possible that multiline text is treated as a single block, + * i.e. all its internal indents are not visible to the formatter. That's why current class is introduced. + *

+ * Thread-safe. + * + * @author Denis Zhdanov + * @since 8/1/12 2:38 PM + */ +public class TabPostFormatProcessor implements PostFormatProcessor { + + @Override + public PsiElement processElement(@NotNull PsiElement source, @NotNull CodeStyleSettings settings) { + doProcess(source, TextRange.from(source.getTextRange().getStartOffset(), source.getTextLength()), settings); + return source; + } + + @Override + public TextRange processText(@NotNull PsiFile source, @NotNull TextRange rangeToReformat, @NotNull CodeStyleSettings settings) { + return doProcess(source, rangeToReformat, settings); + } + + @NotNull + private static TextRange doProcess(@NotNull PsiElement source, @NotNull TextRange range, @NotNull CodeStyleSettings settings) { + ASTNode node = source.getNode(); + if (node == null) { + return range; + } + + Language language = source.getLanguage(); + if (language != JavaLanguage.INSTANCE) { + // We had the only complaint for tabs not being converted to spaces for now. It was for the java code which has + // a single block for the multi-line comment. This check should be removed if it is decided to generalize + // this logic to other languages as well. + return range; + } + + LanguageFileType fileType = language.getAssociatedFileType(); + if (fileType == null) { + return range; + } + + boolean useTabs = settings.useTabCharacter(fileType); + boolean smartTabs = settings.isSmartTabs(fileType); + int tabWidth = settings.getTabSize(fileType); + return processViaPsi(node, range, new TreeHelperImpl(), useTabs, smartTabs, tabWidth); + } + + @NotNull + static TextRange processViaPsi(@NotNull ASTNode node, + @NotNull TextRange range, + @NotNull TreeHelper treeHelper, + boolean useTabs, + boolean smartTabs, + int tabWidth) + { + AstHelper helper = new AstHelper(node, treeHelper); + do { + if (useTabs) { + if (smartTabs) { + range = processSmartTabs(helper, range, tabWidth); + } + else { + range = processTabs(helper, range, tabWidth); + } + } + else { + range = processSpaces(helper, range, tabWidth); + } + } + while (helper.nextLine()); + return range; + } + + @NotNull + static TextRange processViaDocument(@NotNull Document document, + @NotNull TextRange range, + boolean useTabs, + boolean useSmartTabs, + int tabWidth) + { + TextRange result = range; + int startLine = document.getLineNumber(Math.min(document.getTextLength(), range.getStartOffset())); + int endLine = document.getLineNumber(Math.max(0, Math.min(document.getTextLength(), range.getEndOffset()) - 1)); + DocumentHelper helper = new DocumentHelper(document, startLine); + for (int line = startLine; line <= endLine; line++) { + helper.setLine(line); + if (useTabs) { + if (useSmartTabs) { + result = processSmartTabs(helper, result, tabWidth); + } + else { + result = processTabs(helper, result, tabWidth); + } + } + else { + result = processSpaces(helper, result, tabWidth); + } + } + return result; + } + + /** + * Converts tabulations to white spaces at the target line's indent space. + * + * @param helper data facade + * @param range target range allowed for modification + * @param tabWidth tab width in columns to use during conversion (each tab symbol is replaced by white spaces which number is + * equal to tab width) + * @return given text range if no modification to the target line's indent space has been performed: + * adjusted range that points to semantically the same region otherwise + */ + @NotNull + private static TextRange processSpaces(@NotNull Helper helper, @NotNull TextRange range, int tabWidth) { + CharSequence indent = helper.getCurrentLineIndent(); + int start = Math.max(0, range.getStartOffset() - helper.getCurrentLineStartOffset()); + int end = Math.min(indent.length(), range.getEndOffset() - helper.getCurrentLineStartOffset()); + int tabsNumber = 0; + int indentOffset = end; + for (int i = start; i < end; i++) { + char c = indent.charAt(i); + if (c == '\t') { + tabsNumber++; + } + else if (c != ' ') { + indentOffset = i; + break; + } + } + if (tabsNumber > 0) { + helper.replace(start, indentOffset, StringUtil.repeat(" ", indentOffset - start - tabsNumber + tabsNumber * tabWidth)); + return TextRange.create(range.getStartOffset(), range.getEndOffset() - tabsNumber + tabsNumber * tabWidth); + } + else { + return range; + } + } + + /** + * Converts white spaces to tabulations at the target line's indent space. + * + * @param helper data facade + * @param range target range allowed for modification + * @param tabWidth tab width in columns to use during conversion (each tab symbol is replaced by white spaces which number is + * equal to tab width) + * @return given text range if no modification to the target line's indent space has been performed: + * adjusted range that points to semantically the same region otherwise + */ + @NotNull + private static TextRange processTabs(@NotNull Helper helper, @NotNull TextRange range, int tabWidth) { + CharSequence indent = helper.getCurrentLineIndent(); + int start = Math.max(0, range.getStartOffset() - helper.getCurrentLineStartOffset()); + int end = Math.min(indent.length(), range.getEndOffset() - helper.getCurrentLineStartOffset()); + int replacementsNumber = 0; + int consecutiveSpaces = 0; + for (int i = start; i < end; i++) { + char c = indent.charAt(i); + if (c == ' ') { + ++consecutiveSpaces; + } + else { + int tabsNumber = consecutiveSpaces / tabWidth; + if (tabsNumber > 0) { + helper.replace(i - consecutiveSpaces, i - consecutiveSpaces + tabsNumber * tabWidth, StringUtil.repeat("\t", tabsNumber)); + replacementsNumber++; + consecutiveSpaces = 0; + } + if (c != '\t') { + break; + } + } + } + + int tabsNumber = consecutiveSpaces / tabWidth; + if (tabsNumber > 0) { + helper.replace(end - consecutiveSpaces, end - consecutiveSpaces + tabsNumber * tabWidth, StringUtil.repeat("\t", tabsNumber)); + } + + if (replacementsNumber > 0) { + return TextRange.create(range.getStartOffset(), range.getEndOffset() - replacementsNumber * (tabWidth - 1)); + } + else { + return range; + } + } + + /** + * Converts tabulations to white spaces at the target line's indent space. + * + * @param helper data facade + * @param range target range allowed for modification + * @param tabWidth tab width in columns to use during conversion (every group of 'tab width' white spaces from the indent space might + * be replaced by a tab symbol) + * @return given text range if no modification to the target line's indent space has been performed: + * adjusted range that points to semantically the same region otherwise + */ + @SuppressWarnings("AssignmentToForLoopParameter") + @NotNull + private static TextRange processSmartTabs(@NotNull Helper helper, @NotNull TextRange range, int tabWidth) { + // Adjust current line indent. The general idea is to replace white spaces by tab symbols if that maps to the previous line indent. + CharSequence prevLineIndent = helper.getPrevLineIndent(); + if (prevLineIndent == null) { + return processTabs(helper, range, tabWidth); + } + + CharSequence currentLineIndent = helper.getCurrentLineIndent(); + int lineStart = 0; + int start = Math.max(0, range.getStartOffset() - helper.getCurrentLineStartOffset()); + int end = Math.min(currentLineIndent.length(), range.getEndOffset() - helper.getCurrentLineStartOffset()); + int indentOffset = 0; + int tabsReplaced = 0; + for (int i = lineStart; i < end && indentOffset < prevLineIndent.length(); i++, indentOffset++) { + char c = currentLineIndent.charAt(i); + if (prevLineIndent.charAt(indentOffset) == ' ') { + if (c == ' ') { + continue; + } + else { + break; + } + } + + // Assuming that target prevLineIndent symbol is tab then. + if (c == '\t') { + continue; + } + + if (end - i < tabWidth) { + break; + } + + boolean canReplace = true; + for (int j = i + 1, max = Math.min(end, i + tabWidth); j < max; j++) { + if (currentLineIndent.charAt(j) != ' ') { + canReplace = false; + break; + } + } + + if (!canReplace) { + break; + } + + if (i < start) { + // Continue processing if target range doesn't cover the whole white spaces which are intended to replace tab symbol. + i += tabWidth - 1; // -1 because of 'for' loop increment + continue; + } + + helper.replace(i, i + tabWidth, "\t"); + tabsReplaced++; + end -= tabWidth - 1; + } + + return tabsReplaced > 0 ? TextRange.create(range.getStartOffset(), range.getEndOffset() - tabsReplaced * (tabWidth - 1)) : range; + } + + /** + * There are two possible processing use-cases: + *

+   * 
+   * 
+ * That's why we hide implementation-specific processing behind the current interface and use it at the generic 'engine'. + *

+ * The general idea is to process indent spaces line-by-line from top to bottom. + */ + interface Helper { + + /** + * @return previous line indent space if current line is not the first one; null otherwise + */ + @Nullable CharSequence getPrevLineIndent(); + + int getCurrentLineStartOffset(); + + /** @return current line's indent space */ + @NotNull CharSequence getCurrentLineIndent(); + + /** + * Asks current helper to modify target line's indent space. + * + * @param start start offset of the indent range to modify (counts from the line start, i.e. doesn't take into + * consideration line start offset at the document) + * @param end end offset of the indent range to modify (counts from the line start, i.e. doesn't take into + * consideration line start offset at the document) + * @param newText replacement text + */ + void replace(int start, int end, @NotNull String newText); + } + + private static class DocumentHelper implements Helper { + + @NotNull private final Document myDocument; + private int myLine; + private int myLineStartOffset; + + DocumentHelper(@NotNull Document document, int line) { + myDocument = document; + setLine(line); + } + + @Nullable + @Override + public CharSequence getPrevLineIndent() { + if (myLine <= 0) { + return null; + } + int prevLineStart = myDocument.getLineStartOffset(myLine - 1); + int prevLineIndentEnd = prevLineStart; + int prevLineEnd = myDocument.getLineEndOffset(myLine - 1); + CharSequence text = myDocument.getCharsSequence(); + for (; prevLineIndentEnd < prevLineEnd; prevLineIndentEnd++) { + char c = text.charAt(prevLineIndentEnd); + if (c != '\t' && c != ' ') { + break; + } + } + return text.subSequence(prevLineStart, prevLineIndentEnd); + } + + @Override + public int getCurrentLineStartOffset() { + return myLineStartOffset; + } + + @NotNull + @Override + public CharSequence getCurrentLineIndent() { + int end = myDocument.getLineEndOffset(myLine); + CharSequence text = myDocument.getCharsSequence(); + for (int i = myLineStartOffset; i < end; i++) { + char c = text.charAt(i); + if (c != ' ' && c != '\t') { + return text.subSequence(myLineStartOffset, i); + } + } + return text.subSequence(myLineStartOffset, end); + } + + @Override + public void replace(int start, int end, @NotNull String newText) { + myDocument.replaceString(myLineStartOffset + start, myLineStartOffset + end, newText); + } + + public void setLine(int line) { + myLine = line; + myLineStartOffset = myDocument.getLineStartOffset(line); + } + } + + private static class AstHelper implements Helper { + + @NotNull private final TreeHelper myHelper; + @Nullable private ASTNode myCurrentIndentHolder; + + private int myLineStartOffset; + + AstHelper(@NotNull ASTNode startNode, @NotNull TreeHelper helper) { + myHelper = helper; + myCurrentIndentHolder = myHelper.firstLeaf(startNode); + if (startNode.getStartOffset() <= 0) { + return; + } + nextLine(); + } + + @SuppressWarnings("LoopStatementThatDoesntLoop") + @Override + public CharSequence getPrevLineIndent() { + if (myCurrentIndentHolder == null) { + return null; + } + + // Check if current white space is multiline. + int end = myLineStartOffset - 1; + CharSequence text = myCurrentIndentHolder.getChars(); + for (int i = end - 1; i >= 0; i--) { + if (text.charAt(i) == '\n') { + return text.subSequence(i + 1, end); + } + } + for (ASTNode prev = prevIndentNode(myCurrentIndentHolder); prev != null; prev = prevIndentNode(prev)) { + CharSequence chars = prev.getChars(); + for (int i = chars.length() - 1; i >= 0; i--) { + if (chars.charAt(i) == '\n') { + return chars.subSequence(i + 1, chars.length()); + } + } + return chars; + } + return null; + } + + @Override + public int getCurrentLineStartOffset() { + ASTNode whiteSpace = myCurrentIndentHolder; + return whiteSpace == null ? 0 : whiteSpace.getStartOffset() + myLineStartOffset; + } + + @SuppressWarnings("UnusedAssignment") + @NotNull + @Override + public CharSequence getCurrentLineIndent() { + if (myCurrentIndentHolder == null || myLineStartOffset < 0) { + return ""; + } + + CharSequence text = myCurrentIndentHolder.getChars(); + for (int i = myLineStartOffset; i < text.length(); i++) { + if (text.charAt(i) == '\n') { + return text.subSequence(myLineStartOffset, i); + } + } + return text.subSequence(myLineStartOffset, text.length()); + } + + @Override + public void replace(int start, int end, @NotNull String newText) { + if (myCurrentIndentHolder != null) { + myHelper.replace(newText, TextRange.create(start, end).shiftRight(getCurrentLineStartOffset()), myCurrentIndentHolder); + } + } + + public boolean nextLine() { + if (myCurrentIndentHolder == null) { + return false; + } + for (ASTNode node = myHelper.nextLeaf(myCurrentIndentHolder); node != null; node = myHelper.nextLeaf(node)) { + if (myCurrentIndentHolder.getTextLength() <= 0) { + continue; + } + CharSequence text = node.getChars(); + for (myLineStartOffset = 0; myLineStartOffset < text.length(); myLineStartOffset++) { + char c = text.charAt(myLineStartOffset); + if (c == '\n' && myLineStartOffset < text.length() - 1) { + myCurrentIndentHolder = node; + myLineStartOffset++; + return true; + } + } + } + + myCurrentIndentHolder = null; + return false; + } + + @Nullable + private ASTNode prevIndentNode(@NotNull ASTNode current) { + for (ASTNode candidate = myHelper.prevLeaf(current); candidate != null; candidate = myHelper.prevLeaf(candidate)) { + if (candidate.getStartOffset() <= 0 || StringUtil.contains(candidate.getChars(), 0, candidate.getTextLength(), '\n')) { + return candidate; + } + } + return null; + } + } + + interface TreeHelper { + @Nullable + ASTNode prevLeaf(@NotNull ASTNode current); + + @Nullable + ASTNode nextLeaf(@NotNull ASTNode current); + + @Nullable + ASTNode firstLeaf(@NotNull ASTNode startNode); + + void replace(@NotNull String newText, @NotNull TextRange range, @NotNull ASTNode leaf); + } + + private static class TreeHelperImpl implements TreeHelper { + + @Override + public ASTNode prevLeaf(@NotNull ASTNode current) { + return TreeUtil.prevLeaf(current); + } + + @Nullable + @Override + public ASTNode nextLeaf(@NotNull ASTNode current) { + return TreeUtil.nextLeaf(current); + } + + @Nullable + @Override + public ASTNode firstLeaf(@NotNull ASTNode startNode) { + return TreeUtil.findFirstLeaf(startNode); + } + + @Override + public void replace(@NotNull String newText, @NotNull TextRange range, @NotNull ASTNode leaf) { + FormatterUtil.replaceWhiteSpace(newText, leaf, leaf.getElementType(), range); + } + } +} diff --git a/java/java-tests/testSrc/com/intellij/psi/impl/source/codeStyle/TabPostFormatProcessorTest.java b/java/java-tests/testSrc/com/intellij/psi/impl/source/codeStyle/TabPostFormatProcessorTest.java new file mode 100644 index 000000000000..0d132473cbe8 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/psi/impl/source/codeStyle/TabPostFormatProcessorTest.java @@ -0,0 +1,403 @@ +/* + * Copyright 2000-2012 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.intellij.psi.impl.source.codeStyle; + +import com.intellij.lang.ASTNode; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.impl.LineSet; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.TokenType; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.jmock.api.Invocation; +import org.jmock.integration.junit4.JMock; +import org.jmock.integration.junit4.JUnit4Mockery; +import org.jmock.lib.action.CustomAction; +import org.jmock.lib.legacy.ClassImposteriser; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Denis Zhdanov + * @since 08/01/2012 + */ +@RunWith(JMock.class) +public class TabPostFormatProcessorTest { + + private static final String START_RANGE_MARKER = ""; + private static final String END_RANGE_MARKER = ""; + + private Mockery myMockery; + private Document myDocument; + + @Before + public void setUp() { + myMockery = new JUnit4Mockery() {{ + setImposteriser(ClassImposteriser.INSTANCE); + }}; + myDocument = myMockery.mock(Document.class); + } + + @After + public void checkExpectations() { + myMockery.assertIsSatisfied(); + } + + @Test + public void spacesAndWholeLineInsideRange() { + doTestSpaces( + "line 1\n" + + " \t \tline2\n" + + "line 3", + 4, + "line 1\n" + + " line2\n" + + "line 3" + ); + } + @Test + public void spacesAndExactRange() { + doTestSpaces( + "line 1\n" + + " \t \tline2\n" + + "line 3", + 4, + "line 1\n" + + " line2\n" + + "line 3" + ); + } + + @Test + public void spacesAndHeadIntersection() { + doTestSpaces( + "line 1\n" + + " \t \tline2\n" + + "line 3", + 4, + "line 1\n" + + " \tline2\n" + + "line 3" + ); + } + + @Test + public void spacesAndTailIntersection() { + doTestSpaces( + "line 1\n" + + " \t \tline2\n" + + "line 3", + 4, + "line 1\n" + + " \t line2\n" + + "line 3" + ); + } + + @Test + public void spacesAndPartialIndentInsideRange() { + doTestSpaces( + "line 1\n" + + " \t \t \t line2\n" + + "line 3", + 4, + "line 1\n" + + " \t \t line2\n" + + "line 3" + ); + } + + @Test + public void tabsAndWholeLineInsideRange() { + doTestTabs( + "line 1\n" + + " \t line2\n" + + "line 3", + 4, + "line 1\n" + + "\t \t line2\n" + + "line 3" + ); + } + + @Test + public void tabsAndHeadIntersection() { + doTestTabs( + "line 1\n" + + " \t line2\n" + + "line 3", + 4, + "line 1\n" + + "\t\t line2\n" + + "line 3" + ); + } + + @Test + public void tabsAndTailIntersection() { + doTestTabs( + "line 1\n" + + " line2\n" + + "line 3", + 4, + "line 1\n" + + " \t line2\n" + + "line 3" + ); + } + + @Test + public void tabsAndPartialIndentInsideRange() { + doTestTabs( + "line 1\n" + + " line2\n" + + "line 3", + 4, + "line 1\n" + + " \t line2\n" + + "line 3" + ); + } + + @Test + public void smartTabsForTheFirstLine() { + doTestSmartTabs( + " line 1\n" + + "\t line 2", + 4, + "\t\t line 1\n" + + "\t line 2" + ); + } + + @Test + public void smartTabsFromUpperLine() { + doTestSmartTabs( + "\t\t line 1\n" + + " 2", + 4, + "\t\t line 1\n" + + "\t\t 2" + ); + } + + @Test + public void smartTabsExactReplacement() { + doTestSmartTabs( + "\tline 1\n" + + " line 2", + 4, + "\tline 1\n" + + "\tline 2" + ); + } + + @Test + public void smartTabsMismatchedIndent() { + doTestSmartTabs( + " \tline 1\n" + + "\t line 2", + 4, + " \tline 1\n" + + "\t line 2" + ); + } + + @Test + public void smartTabsPartialMatchedIndent() { + doTestSmartTabs( + "\t\tline 1\n" + + " line 2", + 4, + "\t\tline 1\n" + + " \tline 2" + ); + } + + @Test + public void smartTabsPartialMisMatchedIndent() { + doTestSmartTabs( + "\t\tline 1\n" + + " line 2", + 4, + "\t\tline 1\n" + + " line 2" + ); + } + + private void doTestSpaces(@NotNull String initial, final int tabWidth, @NotNull String expected) { + doTest(initial, expected, false, false, tabWidth); + } + + private void doTestTabs(@NotNull String initial, final int tabWidth, @NotNull String expected) { + doTest(initial, expected, true, false, tabWidth); + } + + private void doTestSmartTabs(@NotNull String initial, final int tabWidth, @NotNull String expected) { + doTest(initial, expected, true, true, tabWidth); + } + + private void doTest(@NotNull String initial, @NotNull String expected, boolean useTabs, boolean smartTabs, int tabWidth) { + doDocumentTest(initial, expected, useTabs, smartTabs, tabWidth); + doPsiTest(initial, expected, useTabs, smartTabs, tabWidth); + } + + private void doDocumentTest(@NotNull String initial, @NotNull String expected, boolean useTabs, boolean smartTabs, int tabWidth) { + Pair pair = parse(initial); + final StringBuilder text = new StringBuilder(pair.first); + final TextRange range = pair.second; + + myMockery.checking(new Expectations() {{ + allowing(myDocument).getCharsSequence(); will(returnValue(text.toString())); + allowing(myDocument).getTextLength(); will(returnValue(text.length())); + }}); + + final LineSet lines = new LineSet(); + lines.documentCreated(myDocument); + myMockery.checking(new Expectations() {{ + allowing(myDocument).getLineNumber(with(any(int.class))); will(new CustomAction("getLineNumber()") { + @Override + public Object invoke(Invocation invocation) throws Throwable { + return lines.findLineIndex((Integer)invocation.getParameter(0)); + } + }); + allowing(myDocument).getLineStartOffset(with(any(int.class))); will(new CustomAction("getLineStartOffset()") { + @Override + public Object invoke(Invocation invocation) throws Throwable { + return lines.getLineStart((Integer)invocation.getParameter(0)); + } + }); + allowing(myDocument).getLineEndOffset(with(any(int.class))); will(new CustomAction("getLineEndOffset()") { + @Override + public Object invoke(Invocation invocation) throws Throwable { + return lines.getLineEnd((Integer)invocation.getParameter(0)); + } + }); + allowing(myDocument).replaceString(with(any(int.class)), with(any(int.class)), with(any(String.class))); + will(new CustomAction("replaceString") { + @Nullable + @Override + public Object invoke(Invocation invocation) throws Throwable { + int start = (Integer)invocation.getParameter(0); + int end = (Integer)invocation.getParameter(1); + String newText = (String)invocation.getParameter(2); + text.replace(start, end, newText); + return null; + } + }); + }}); + + TabPostFormatProcessor.processViaDocument(myDocument, range, useTabs, smartTabs, tabWidth); + assertEquals(expected, text.toString()); + } + + private static Pair parse(@NotNull String text) { + int rangeMarkerStart = text.indexOf(START_RANGE_MARKER); + int rangeMarkerEnd = text.indexOf(END_RANGE_MARKER); + final StringBuilder buffer = new StringBuilder(); + final TextRange range; + if (rangeMarkerStart >= 0 && rangeMarkerEnd >= 0) { + range = TextRange.create(rangeMarkerStart, rangeMarkerEnd - START_RANGE_MARKER.length()); + buffer.append(text.substring(0, rangeMarkerStart)) + .append(text.substring(rangeMarkerStart + START_RANGE_MARKER.length(), rangeMarkerEnd)) + .append(text.substring(rangeMarkerEnd + END_RANGE_MARKER.length())); + } + else { + range = TextRange.create(0, text.length()); + buffer.append(text); + } + return Pair.create(buffer.toString(), range); + } + + private void doPsiTest(@NotNull String initial, @NotNull String expected, boolean useTabs, boolean smartTabs, int tabWidth) { + final List children = new ArrayList(); + final List childrenText = new ArrayList(); + Pair pair = parse(initial); + final String text = pair.first; + int start = 0; + boolean inWhiteSpace = initial.charAt(0) == ' ' || initial.charAt(0) == '\t'; + for (int i = 1; i <= text.length(); i++) { + if (i == text.length() || ((StringUtil.isWhiteSpace(text.charAt(i))) ^ inWhiteSpace)) { + final int childIndex = children.size(); + final int startOffset = start; + childrenText.add(new StringBuilder(text.substring(start, i))); + final ASTNode child = myMockery.mock(ASTNode.class, "child" + childIndex); + children.add(child); + final IElementType type = inWhiteSpace ? TokenType.WHITE_SPACE : TokenType.CODE_FRAGMENT; + myMockery.checking(new Expectations() {{ + allowing(child).getElementType(); will(returnValue(type)); + allowing(child).getChars(); will(returnValue(childrenText.get(childIndex))); + allowing(child).getTextLength(); will(returnValue(childrenText.get(childIndex).length())); + allowing(child).getStartOffset(); will(returnValue(startOffset)); + }}); + inWhiteSpace = !inWhiteSpace; + start = i; + } + } + + final ASTNode root = myMockery.mock(ASTNode.class); + myMockery.checking(new Expectations() {{ + allowing(root).getFirstChildNode(); will(returnValue(children.get(0))); + allowing(root).getTextLength(); will(returnValue(text.length())); + allowing(root).getStartOffset(); will(returnValue(0)); + }}); + + TabPostFormatProcessor.TreeHelper helper = new TabPostFormatProcessor.TreeHelper() { + + @Override + public ASTNode prevLeaf(@NotNull ASTNode current) { + int i = children.indexOf(current); + return i > 0 ? children.get(i - 1) : null; + } + + @Override + public ASTNode nextLeaf(@NotNull ASTNode current) { + int i = children.indexOf(current); + return i < children.size() - 1 ? children.get(i + 1) : null; + } + + @Override + public ASTNode firstLeaf(@NotNull ASTNode startNode) { + return root == startNode ? children.get(0) : null; + } + + @Override + public void replace(@NotNull String newText, @NotNull TextRange range, @NotNull ASTNode leaf) { + int i = children.indexOf(leaf); + childrenText.get(i).replace(range.getStartOffset() - leaf.getStartOffset(), range.getEndOffset() - leaf.getStartOffset(), newText); + } + }; + + TabPostFormatProcessor.processViaPsi(root, pair.second, helper, useTabs, smartTabs, tabWidth); + StringBuilder actual = new StringBuilder(); + for (ASTNode child : children) { + actual.append(child.getChars()); + } + assertEquals(expected, actual.toString()); + } +} diff --git a/platform/core-impl/src/com/intellij/openapi/editor/impl/LineSet.java b/platform/core-impl/src/com/intellij/openapi/editor/impl/LineSet.java index f92cb7cfe669..d7574d262e5d 100644 --- a/platform/core-impl/src/com/intellij/openapi/editor/impl/LineSet.java +++ b/platform/core-impl/src/com/intellij/openapi/editor/impl/LineSet.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2009 JetBrains s.r.o. + * Copyright 2000-2012 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. @@ -46,13 +46,13 @@ public class LineSet{ return new LineIteratorImpl(this); } - final int getLineStart(int index) { + public final int getLineStart(int index) { int lineStart = mySegments.getSegmentStart(index); assert lineStart >= 0; return lineStart; } - final int getLineEnd(int index) { + public final int getLineEnd(int index) { return mySegments.getSegmentEnd(index); } diff --git a/platform/core-impl/src/com/intellij/psi/impl/DebugUtil.java b/platform/core-impl/src/com/intellij/psi/impl/DebugUtil.java index 36eb4d7feaa9..a44cb72fdda1 100644 --- a/platform/core-impl/src/com/intellij/psi/impl/DebugUtil.java +++ b/platform/core-impl/src/com/intellij/psi/impl/DebugUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2009 JetBrains s.r.o. + * Copyright 2000-2012 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. @@ -19,6 +19,7 @@ import com.intellij.lang.ASTNode; import com.intellij.lang.LighterASTNode; import com.intellij.lang.LighterASTTokenNode; import com.intellij.lang.impl.PsiBuilderImpl; +import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Key; @@ -76,7 +77,11 @@ public class DebugUtil { } public static /*final*/ boolean CHECK = false; - public static final boolean DO_EXPENSIVE_CHECKS = ApplicationManager.getApplication().isUnitTestMode(); + public static final boolean DO_EXPENSIVE_CHECKS; + static { + Application application = ApplicationManager.getApplication(); + DO_EXPENSIVE_CHECKS = application != null && application.isUnitTestMode(); + } public static final boolean CHECK_INSIDE_ATOMIC_ACTION_ENABLED = DO_EXPENSIVE_CHECKS; public static String psiTreeToString(@NotNull final PsiElement element, final boolean skipWhitespaces) { diff --git a/platform/lang-api/src/com/intellij/psi/codeStyle/CodeStyleSettingsManager.java b/platform/lang-api/src/com/intellij/psi/codeStyle/CodeStyleSettingsManager.java index 619e641ca421..d6fe531c685f 100644 --- a/platform/lang-api/src/com/intellij/psi/codeStyle/CodeStyleSettingsManager.java +++ b/platform/lang-api/src/com/intellij/psi/codeStyle/CodeStyleSettingsManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2009 JetBrains s.r.o. + * Copyright 2000-2012 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. @@ -57,11 +57,13 @@ public class CodeStyleSettingsManager implements PersistentStateComponent EP_NAME = ExtensionPointName.create("com.intellij.postFormatProcessor"); - PsiElement processElement(PsiElement source, CodeStyleSettings settings); - TextRange processText(PsiFile source, TextRange rangeToReformat,CodeStyleSettings settings); + PsiElement processElement(@NotNull PsiElement source, @NotNull CodeStyleSettings settings); + TextRange processText(@NotNull PsiFile source, @NotNull TextRange rangeToReformat, @NotNull CodeStyleSettings settings); } diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/formatter/GroovyBracePostFormatProcessor.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/formatter/GroovyBracePostFormatProcessor.java index 93f1622faf61..6cc03c17e0f1 100644 --- a/plugins/groovy/src/org/jetbrains/plugins/groovy/formatter/GroovyBracePostFormatProcessor.java +++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/formatter/GroovyBracePostFormatProcessor.java @@ -20,6 +20,7 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.codeStyle.CodeStyleSettings; import com.intellij.psi.impl.source.codeStyle.PostFormatProcessor; +import org.jetbrains.annotations.NotNull; import org.jetbrains.plugins.groovy.lang.psi.GroovyFile; import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElement; @@ -28,7 +29,7 @@ import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElement; */ public class GroovyBracePostFormatProcessor implements PostFormatProcessor { @Override - public PsiElement processElement(PsiElement source, CodeStyleSettings settings) { + public PsiElement processElement(@NotNull PsiElement source, @NotNull CodeStyleSettings settings) { if (source instanceof GroovyPsiElement) { return new GroovyBraceEnforcer(settings).process(((GroovyPsiElement)source)); } @@ -38,7 +39,7 @@ public class GroovyBracePostFormatProcessor implements PostFormatProcessor { } @Override - public TextRange processText(PsiFile source, TextRange rangeToReformat, CodeStyleSettings settings) { + public TextRange processText(@NotNull PsiFile source, @NotNull TextRange rangeToReformat, @NotNull CodeStyleSettings settings) { if (source instanceof GroovyFile) { return new GroovyBraceEnforcer(settings).processText(((GroovyFile)source), rangeToReformat); } diff --git a/resources/src/META-INF/IdeaPlugin.xml b/resources/src/META-INF/IdeaPlugin.xml index a274074033d6..4cbf8f2708c5 100644 --- a/resources/src/META-INF/IdeaPlugin.xml +++ b/resources/src/META-INF/IdeaPlugin.xml @@ -944,6 +944,7 @@ +