diff options
author | Jean-Baptiste Queru <jbq@google.com> | 2013-01-08 11:11:20 -0800 |
---|---|---|
committer | Jean-Baptiste Queru <jbq@google.com> | 2013-01-08 11:11:20 -0800 |
commit | b56ea2a18f232d79481e778085fd64e8ae486fc3 (patch) | |
tree | 44e1f6eb4864a45033f865b74fe783e3d784dd6a /java/java-impl/src/com/intellij/psi/impl/source/codeStyle/TabPostFormatProcessor.java | |
download | idea-b56ea2a18f232d79481e778085fd64e8ae486fc3.tar.gz |
Snapshot of commit d5ec1d5018ed24f1b4f32b1d09df6dbd7e2fc425
from branch master of git://git.jetbrains.org/idea/community.git
Diffstat (limited to 'java/java-impl/src/com/intellij/psi/impl/source/codeStyle/TabPostFormatProcessor.java')
-rw-r--r-- | java/java-impl/src/com/intellij/psi/impl/source/codeStyle/TabPostFormatProcessor.java | 534 |
1 files changed, 534 insertions, 0 deletions
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..0e2161fb0187 --- /dev/null +++ b/java/java-impl/src/com/intellij/psi/impl/source/codeStyle/TabPostFormatProcessor.java @@ -0,0 +1,534 @@ +/* + * 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. + * <p/> + * 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. + * <p/> + * 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: + * <pre> + * <ul> + * <li>document-based processing;</li> + * <li>PSI-based processing;</li> + * </ul> + * </pre> + * That's why we hide implementation-specific processing behind the current interface and use it at the generic 'engine'. + * <p/> + * 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; <code>null</code> 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++) { + char c = text.charAt(i); + if (c == '\n' || (c != ' ' && c != '\t')) { + 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.replaceInnerWhiteSpace(newText, leaf, range); + } + } +} |