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: + *+ *
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 = "