summaryrefslogtreecommitdiff
path: root/java/java-impl/src/com/intellij/psi/impl/source/codeStyle/TabPostFormatProcessor.java
diff options
context:
space:
mode:
authorJean-Baptiste Queru <jbq@google.com>2013-01-08 11:11:20 -0800
committerJean-Baptiste Queru <jbq@google.com>2013-01-08 11:11:20 -0800
commitb56ea2a18f232d79481e778085fd64e8ae486fc3 (patch)
tree44e1f6eb4864a45033f865b74fe783e3d784dd6a /java/java-impl/src/com/intellij/psi/impl/source/codeStyle/TabPostFormatProcessor.java
downloadidea-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.java534
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);
+ }
+ }
+}