blob: 0e2161fb01876cc48d02ab7c8f45ec6f373768ee [file] [log] [blame]
/*
* 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);
}
}
}