| /* |
| * Copyright 2000-2013 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.formatter; |
| |
| import com.intellij.codeInsight.actions.ReformatCodeProcessor; |
| import com.intellij.lang.ASTFactory; |
| import com.intellij.lang.ASTNode; |
| import com.intellij.lang.Language; |
| import com.intellij.openapi.command.CommandProcessor; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.openapi.vcs.checkin.ReformatBeforeCheckinHandler; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.TokenType; |
| import com.intellij.psi.impl.source.SourceTreeToPsiMap; |
| import com.intellij.psi.impl.source.tree.*; |
| import com.intellij.psi.tree.IElementType; |
| import com.intellij.psi.tree.TokenSet; |
| import com.intellij.util.CharTable; |
| import com.intellij.util.containers.ContainerUtilRt; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.Collection; |
| import java.util.Collections; |
| |
| public class FormatterUtil { |
| |
| public static final Collection<String> FORMATTER_ACTION_NAMES = Collections.unmodifiableCollection(ContainerUtilRt.newHashSet( |
| ReformatCodeProcessor.COMMAND_NAME, ReformatBeforeCheckinHandler.COMMAND_NAME |
| )); |
| |
| private FormatterUtil() { |
| } |
| |
| public static boolean isWhitespaceOrEmpty(@Nullable ASTNode node) { |
| if (node == null) return false; |
| IElementType type = node.getElementType(); |
| return type == TokenType.WHITE_SPACE || (type != TokenType.ERROR_ELEMENT && node.getTextLength() == 0); |
| } |
| |
| public static boolean isOneOf(@Nullable ASTNode node, @NotNull IElementType... types) { |
| if (node == null) return false; |
| IElementType elementType = node.getElementType(); |
| for (IElementType each : types) { |
| if (elementType == each) return true; |
| } |
| return false; |
| } |
| |
| @Nullable |
| public static ASTNode getPrevious(@Nullable ASTNode node, @NotNull IElementType... typesToIgnore) { |
| return getNextOrPrevious(node, false, typesToIgnore); |
| } |
| |
| @Nullable |
| public static ASTNode getNext(@Nullable ASTNode node, @NotNull IElementType... typesToIgnore) { |
| return getNextOrPrevious(node, true, typesToIgnore); |
| } |
| |
| @Nullable |
| private static ASTNode getNextOrPrevious(@Nullable ASTNode node, boolean isNext, @NotNull IElementType... typesToIgnore) { |
| if (node == null) return null; |
| |
| ASTNode each = isNext ? node.getTreeNext() : node.getTreePrev(); |
| ASTNode parent = node.getTreeParent(); |
| while (each == null && parent != null) { |
| each = isNext ? parent.getTreeNext() : parent.getTreePrev(); |
| parent = parent.getTreeParent(); |
| } |
| |
| if (each == null) { |
| return null; |
| } |
| |
| for (IElementType type : typesToIgnore) { |
| if (each.getElementType() == type) { |
| return getNextOrPrevious(each, isNext, typesToIgnore); |
| } |
| } |
| |
| return each; |
| } |
| |
| @Nullable |
| public static ASTNode getPreviousLeaf(@Nullable ASTNode node, @NotNull IElementType... typesToIgnore) { |
| ASTNode prev = getPrevious(node, typesToIgnore); |
| if (prev == null) { |
| return null; |
| } |
| |
| ASTNode result = prev; |
| ASTNode lastChild = prev.getLastChildNode(); |
| while (lastChild != null) { |
| result = lastChild; |
| lastChild = lastChild.getLastChildNode(); |
| } |
| |
| for (IElementType type : typesToIgnore) { |
| if (result.getElementType() == type) { |
| return getPreviousLeaf(result, typesToIgnore); |
| } |
| } |
| return result; |
| } |
| |
| @Nullable |
| public static ASTNode getPreviousNonWhitespaceLeaf(@Nullable ASTNode node) { |
| if (node == null) return null; |
| ASTNode treePrev = node.getTreePrev(); |
| if (treePrev != null) { |
| ASTNode candidate = TreeUtil.getLastChild(treePrev); |
| if (candidate != null && !isWhitespaceOrEmpty(candidate)) { |
| return candidate; |
| } |
| else { |
| return getPreviousNonWhitespaceLeaf(candidate); |
| } |
| } |
| final ASTNode treeParent = node.getTreeParent(); |
| |
| if (treeParent == null || treeParent.getTreeParent() == null) { |
| return null; |
| } |
| else { |
| return getPreviousNonWhitespaceLeaf(treeParent); |
| } |
| } |
| |
| @Nullable |
| public static ASTNode getPreviousNonWhitespaceSibling(@Nullable ASTNode node) { |
| ASTNode prevNode = node == null ? null : node.getTreePrev(); |
| while (prevNode != null && isWhitespaceOrEmpty(prevNode)) { |
| prevNode = prevNode.getTreePrev(); |
| } |
| return prevNode; |
| } |
| |
| @Nullable |
| public static ASTNode getNextNonWhitespaceSibling(@Nullable ASTNode node) { |
| ASTNode next = node == null ? null : node.getTreeNext(); |
| while (next != null && isWhitespaceOrEmpty(next)) { |
| next = next.getTreeNext(); |
| } |
| return next; |
| } |
| |
| public static boolean isPrecededBy(@Nullable ASTNode node, IElementType expectedType) { |
| return isPrecededBy(node, expectedType, IElementType.EMPTY_ARRAY); |
| } |
| |
| public static boolean isPrecededBy(@Nullable ASTNode node, IElementType expectedType, TokenSet skipTypes) { |
| return isPrecededBy(node, expectedType, skipTypes.getTypes()); |
| } |
| |
| public static boolean isPrecededBy(@Nullable ASTNode node, IElementType expectedType, IElementType... skipTypes) { |
| ASTNode prevNode = node == null ? null : node.getTreePrev(); |
| while (prevNode != null && (isWhitespaceOrEmpty(prevNode) || isOneOf(prevNode, skipTypes))) { |
| prevNode = prevNode.getTreePrev(); |
| } |
| if (prevNode == null) return false; |
| return prevNode.getElementType() == expectedType; |
| } |
| |
| public static boolean isPrecededBy(@Nullable ASTNode node, TokenSet expectedTypes) { |
| return isPrecededBy(node, expectedTypes, IElementType.EMPTY_ARRAY); |
| } |
| |
| public static boolean isPrecededBy(@Nullable ASTNode node, TokenSet expectedTypes, TokenSet skipTypes) { |
| return isPrecededBy(node, expectedTypes, skipTypes.getTypes()); |
| } |
| |
| public static boolean isPrecededBy(@Nullable ASTNode node, TokenSet expectedTypes, IElementType... skipTypes) { |
| ASTNode prevNode = node == null ? null : node.getTreePrev(); |
| while (prevNode != null && (isWhitespaceOrEmpty(prevNode) || isOneOf(prevNode, skipTypes))) { |
| prevNode = prevNode.getTreePrev(); |
| } |
| if (prevNode == null) return false; |
| return expectedTypes.contains(prevNode.getElementType()); |
| } |
| |
| public static boolean hasPrecedingSiblingOfType(@Nullable ASTNode node, IElementType expectedSiblingType, IElementType... skipTypes) { |
| for (ASTNode prevNode = node == null ? null : node.getTreePrev(); prevNode != null; prevNode = prevNode.getTreePrev()) { |
| if (isWhitespaceOrEmpty(prevNode) || isOneOf(prevNode, skipTypes)) continue; |
| if (prevNode.getElementType() == expectedSiblingType) return true; |
| } |
| return false; |
| } |
| |
| public static boolean isFollowedBy(@Nullable ASTNode node, IElementType expectedType) { |
| return isFollowedBy(node, expectedType, IElementType.EMPTY_ARRAY); |
| } |
| |
| public static boolean isFollowedBy(@Nullable ASTNode node, IElementType expectedType, TokenSet skipTypes) { |
| return isFollowedBy(node, expectedType, skipTypes.getTypes()); |
| } |
| |
| public static boolean isFollowedBy(@Nullable ASTNode node, IElementType expectedType, IElementType... skipTypes) { |
| ASTNode nextNode = node == null ? null : node.getTreeNext(); |
| while (nextNode != null && (isWhitespaceOrEmpty(nextNode) || isOneOf(nextNode, skipTypes))) { |
| nextNode = nextNode.getTreeNext(); |
| } |
| if (nextNode == null) return false; |
| return nextNode.getElementType() == expectedType; |
| } |
| |
| public static boolean isIncomplete(@Nullable ASTNode node) { |
| ASTNode lastChild = node == null ? null : node.getLastChildNode(); |
| while (lastChild != null && lastChild.getElementType() == TokenType.WHITE_SPACE) { |
| lastChild = lastChild.getTreePrev(); |
| } |
| if (lastChild == null) return false; |
| if (lastChild.getElementType() == TokenType.ERROR_ELEMENT) return true; |
| return isIncomplete(lastChild); |
| } |
| |
| public static boolean containsWhiteSpacesOnly(@Nullable ASTNode node) { |
| if (node == null) return false; |
| |
| final boolean[] spacesOnly = {true}; |
| ((TreeElement)node).acceptTree(new RecursiveTreeElementWalkingVisitor() { |
| @Override |
| public void visitComposite(CompositeElement composite) { |
| if (!spacesOnly(composite)) { |
| super.visitComposite(composite); |
| } |
| } |
| |
| @Override |
| public void visitLeaf(LeafElement leaf) { |
| if (!spacesOnly(leaf)) { |
| spacesOnly[0] = false; |
| stopWalking(); |
| } |
| } |
| }); |
| |
| return spacesOnly[0]; |
| } |
| |
| private static boolean spacesOnly(@Nullable TreeElement node) { |
| if (node == null) return false; |
| |
| if (isWhitespaceOrEmpty(node)) return true; |
| PsiElement psi = node.getPsi(); |
| if (psi == null) { |
| return false; |
| } |
| Language language = psi.getLanguage(); |
| return WhiteSpaceFormattingStrategyFactory.getStrategy(language).containsWhitespacesOnly(node); |
| } |
| |
| /** |
| * There is a possible case that we want to adjust white space which is not represented at the AST/PSI tree, e.g. |
| * we might have a multiline comment which uses tabs for inner lines indents and want to replace them by spaces. |
| * There is no white space element then, the only leaf is the comment itself. |
| * <p/> |
| * This method allows such 'inner element modifications', i.e. it receives information on what new text should be used |
| * at the target inner element range and performs corresponding replacement by generating new leaf with adjusted text |
| * and replacing the old one by it. |
| * |
| * @param newWhiteSpaceText new text to use at the target inner element range |
| * @param holder target range holder |
| * @param whiteSpaceRange target range which text should be replaced by the given one |
| */ |
| public static void replaceInnerWhiteSpace(@NotNull final String newWhiteSpaceText, |
| @NotNull final ASTNode holder, |
| @NotNull final TextRange whiteSpaceRange) |
| { |
| final CharTable charTable = SharedImplUtil.findCharTableByTree(holder); |
| StringBuilder newText = createNewLeafChars(holder, whiteSpaceRange, newWhiteSpaceText); |
| LeafElement newElement = |
| Factory.createSingleLeafElement(holder.getElementType(), newText, charTable, holder.getPsi().getManager()); |
| |
| holder.getTreeParent().replaceChild(holder, newElement); |
| } |
| |
| public static void replaceWhiteSpace(final String whiteSpace, |
| final ASTNode leafElement, |
| final IElementType whiteSpaceToken, |
| @Nullable final TextRange textRange) { |
| final CharTable charTable = SharedImplUtil.findCharTableByTree(leafElement); |
| |
| ASTNode treePrev = findPreviousWhiteSpace(leafElement, whiteSpaceToken); |
| if (treePrev == null) { |
| treePrev = getWsCandidate(leafElement); |
| } |
| |
| if (treePrev != null && |
| treePrev.getText().trim().isEmpty() && |
| treePrev.getElementType() != whiteSpaceToken && |
| treePrev.getTextLength() > 0 && |
| !whiteSpace.isEmpty()) { |
| LeafElement whiteSpaceElement = |
| Factory.createSingleLeafElement(treePrev.getElementType(), whiteSpace, charTable, SharedImplUtil.getManagerByTree(leafElement)); |
| |
| ASTNode treeParent = treePrev.getTreeParent(); |
| treeParent.replaceChild(treePrev, whiteSpaceElement); |
| } |
| else { |
| LeafElement whiteSpaceElement = |
| Factory.createSingleLeafElement(whiteSpaceToken, whiteSpace, charTable, SharedImplUtil.getManagerByTree(leafElement)); |
| |
| if (treePrev == null) { |
| if (!whiteSpace.isEmpty()) { |
| addWhiteSpace(leafElement, whiteSpaceElement); |
| } |
| } |
| else { |
| if (!(treePrev.getElementType() == whiteSpaceToken)) { |
| if (!whiteSpace.isEmpty()) { |
| addWhiteSpace(treePrev, whiteSpaceElement); |
| } |
| } |
| else { |
| if (treePrev.getElementType() == whiteSpaceToken) { |
| final CompositeElement treeParent = (CompositeElement)treePrev.getTreeParent(); |
| if (!whiteSpace.isEmpty()) { |
| // LOG.assertTrue(textRange == null || treeParent.getTextRange().equals(textRange)); |
| treeParent.replaceChild(treePrev, whiteSpaceElement); |
| } |
| else { |
| treeParent.removeChild(treePrev); |
| } |
| |
| // There is a possible case that more than one PSI element is matched by the target text range. |
| // That is the case, for example, for Python's multi-line expression. It may looks like below: |
| // import contextlib,\ |
| // math, decimal |
| // Here single range contains two blocks: '\' & '\n '. So, we may want to replace that range to another text, hence, |
| // we replace last element located there with it ('\n ') and want to remove any remaining elements ('\'). |
| ASTNode removeCandidate = findPreviousWhiteSpace(whiteSpaceElement, whiteSpaceToken); |
| while (textRange != null && removeCandidate != null && removeCandidate.getStartOffset() >= textRange.getStartOffset()) { |
| treePrev = findPreviousWhiteSpace(removeCandidate, whiteSpaceToken); |
| removeCandidate.getTreeParent().removeChild(removeCandidate); |
| removeCandidate = treePrev; |
| } |
| //treeParent.subtreeChanged(); |
| } |
| } |
| } |
| } |
| } |
| |
| @Nullable |
| private static ASTNode findPreviousWhiteSpace(final ASTNode leafElement, final IElementType whiteSpaceTokenType) { |
| final int offset = leafElement.getTextRange().getStartOffset() - 1; |
| if (offset < 0) return null; |
| final PsiElement psiElement = SourceTreeToPsiMap.treeElementToPsi(leafElement); |
| if (psiElement == null) { |
| return null; |
| } |
| final PsiElement found = psiElement.getContainingFile().findElementAt(offset); |
| if (found == null) return null; |
| final ASTNode treeElement = found.getNode(); |
| if (treeElement != null && treeElement.getElementType() == whiteSpaceTokenType) return treeElement; |
| return null; |
| } |
| |
| @Nullable |
| private static ASTNode getWsCandidate(@Nullable ASTNode node) { |
| if (node == null) return null; |
| ASTNode treePrev = node.getTreePrev(); |
| if (treePrev != null) { |
| if (treePrev.getElementType() == TokenType.WHITE_SPACE) { |
| return treePrev; |
| } |
| else if (treePrev.getTextLength() == 0) { |
| return getWsCandidate(treePrev); |
| } |
| else { |
| return node; |
| } |
| } |
| final ASTNode treeParent = node.getTreeParent(); |
| |
| if (treeParent == null || treeParent.getTreeParent() == null) { |
| return node; |
| } |
| else { |
| return getWsCandidate(treeParent); |
| } |
| } |
| |
| private static StringBuilder createNewLeafChars(final ASTNode leafElement, final TextRange textRange, final String whiteSpace) { |
| final TextRange elementRange = leafElement.getTextRange(); |
| final String elementText = leafElement.getText(); |
| |
| final StringBuilder result = new StringBuilder(); |
| |
| if (elementRange.getStartOffset() < textRange.getStartOffset()) { |
| result.append(elementText.substring(0, textRange.getStartOffset() - elementRange.getStartOffset())); |
| } |
| |
| result.append(whiteSpace); |
| |
| if (elementRange.getEndOffset() > textRange.getEndOffset()) { |
| result.append(elementText.substring(textRange.getEndOffset() - elementRange.getStartOffset())); |
| } |
| |
| return result; |
| } |
| |
| private static void addWhiteSpace(final ASTNode treePrev, final LeafElement whiteSpaceElement) { |
| for (WhiteSpaceFormattingStrategy strategy : WhiteSpaceFormattingStrategyFactory.getAllStrategies()) { |
| if (strategy.addWhitespace(treePrev, whiteSpaceElement)) { |
| return; |
| } |
| } |
| |
| final ASTNode treeParent = treePrev.getTreeParent(); |
| treeParent.addChild(whiteSpaceElement, treePrev); |
| } |
| |
| |
| public static void replaceLastWhiteSpace(final ASTNode astNode, final String whiteSpace, final TextRange textRange) { |
| ASTNode lastWS = TreeUtil.findLastLeaf(astNode); |
| if (lastWS == null) { |
| return; |
| } |
| if (lastWS.getElementType() != TokenType.WHITE_SPACE) { |
| lastWS = null; |
| } |
| if (lastWS != null && !lastWS.getTextRange().equals(textRange)) { |
| return; |
| } |
| if (whiteSpace.isEmpty() && lastWS == null) { |
| return; |
| } |
| if (lastWS != null && whiteSpace.isEmpty()) { |
| lastWS.getTreeParent().removeRange(lastWS, null); |
| return; |
| } |
| |
| LeafElement whiteSpaceElement = ASTFactory.whitespace(whiteSpace); |
| |
| if (lastWS == null) { |
| astNode.addChild(whiteSpaceElement, null); |
| } |
| else { |
| ASTNode treeParent = lastWS.getTreeParent(); |
| treeParent.replaceChild(lastWS, whiteSpaceElement); |
| } |
| } |
| |
| /** |
| * @return <code>true</code> explicitly called 'reformat' is in progress at the moment; <code>false</code> otherwise |
| */ |
| public static boolean isFormatterCalledExplicitly() { |
| return FORMATTER_ACTION_NAMES.contains(CommandProcessor.getInstance().getCurrentCommandName()); |
| } |
| } |