/* * 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.impl.source.codeStyle; import com.intellij.lang.*; import com.intellij.openapi.command.AbnormalCommandTerminationException; import com.intellij.openapi.editor.Document; import com.intellij.openapi.util.Key; import com.intellij.psi.*; import com.intellij.psi.impl.source.tree.Factory; import com.intellij.psi.impl.source.tree.LeafElement; import com.intellij.psi.impl.source.tree.TreeElement; import com.intellij.psi.impl.source.tree.TreeUtil; import com.intellij.psi.templateLanguages.OuterLanguageElement; import com.intellij.psi.tree.IElementType; import com.intellij.psi.util.PsiUtilCore; import com.intellij.util.NotNullFunction; import com.intellij.util.text.CharArrayUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public class CodeEditUtil { private static final Key<Boolean> GENERATED_FLAG = new Key<Boolean>("GENERATED_FLAG"); private static final Key<Integer> INDENT_INFO = new Key<Integer>("INDENT_INFO"); private static final Key<Boolean> REFORMAT_BEFORE_KEY = new Key<Boolean>("REFORMAT_BEFORE_KEY"); private static final Key<Boolean> REFORMAT_KEY = new Key<Boolean>("REFORMAT_KEY"); private static final ThreadLocal<Boolean> ALLOW_TO_MARK_NODES_TO_REFORMAT = new ThreadLocal<Boolean>() { @Override protected Boolean initialValue() { return Boolean.TRUE; } }; private static final ThreadLocal<Boolean> ALLOW_NODES_REFORMATTING = new ThreadLocal<Boolean>() { @Override protected Boolean initialValue() { return Boolean.TRUE; } }; private static final ThreadLocal<NotNullFunction<ASTNode, Boolean>> NODE_REFORMAT_STRATEGY = new ThreadLocal<NotNullFunction<ASTNode, Boolean>>(); public static final Key<Boolean> OUTER_OK = new Key<Boolean>("OUTER_OK"); private CodeEditUtil() { } public static void addChild(ASTNode parent, ASTNode child, ASTNode anchorBefore) { addChildren(parent, child, child, anchorBefore); } public static void removeChild(ASTNode parent, @NotNull ASTNode child) { removeChildren(parent, child, child); } public static ASTNode addChildren(ASTNode parent, @NotNull ASTNode first, @NotNull ASTNode last, ASTNode anchorBefore) { ASTNode lastChild = last.getTreeNext(); ASTNode current = first; while (current != lastChild) { saveWhitespacesInfo(current); checkForOuters(current); current = current.getTreeNext(); } if (anchorBefore != null && isComment(anchorBefore.getElementType())) { final ASTNode anchorPrev = anchorBefore.getTreePrev(); if (anchorPrev != null && anchorPrev.getElementType() == TokenType.WHITE_SPACE) { anchorBefore = anchorPrev; } } parent.addChildren(first, lastChild, anchorBefore); ASTNode firstAddedLeaf = findFirstLeaf(first, last); ASTNode prevLeaf = TreeUtil.prevLeaf(first); ASTNode result = first; if (firstAddedLeaf != null) { ASTNode placeHolderEnd = makePlaceHolderBetweenTokens(prevLeaf, firstAddedLeaf, isFormattingRequired(prevLeaf, first), false); if (placeHolderEnd != prevLeaf && first == firstAddedLeaf) { result = placeHolderEnd; } ASTNode lastAddedLeaf = findLastLeaf(first, last); placeHolderEnd = makePlaceHolderBetweenTokens(lastAddedLeaf, TreeUtil.nextLeaf(last), true, false); if (placeHolderEnd != lastAddedLeaf && lastAddedLeaf == first) { result = placeHolderEnd; } } else { makePlaceHolderBetweenTokens(prevLeaf, TreeUtil.nextLeaf(last), isFormattingRequired(prevLeaf, first), false); } return result; } private static boolean isComment(IElementType type) { final ParserDefinition def = LanguageParserDefinitions.INSTANCE.forLanguage(type.getLanguage()); return def != null && def.getCommentTokens().contains(type); } private static boolean isFormattingRequired(ASTNode prevLeaf, ASTNode first) { while (first != null) { ASTNode current = prevLeaf; while (current != null) { if (current.getTreeNext() == first) return true; current = current.getTreeParent(); } final ASTNode parent = first.getTreeParent(); if (parent != null && parent.getTextRange().equals(first.getTextRange())) { first = parent; } else { break; } } return false; } public static void checkForOuters(ASTNode element) { if (element instanceof OuterLanguageElement && element.getCopyableUserData(OUTER_OK) == null) { throw new AbnormalCommandTerminationException(); } ASTNode child = element.getFirstChildNode(); while (child != null) { checkForOuters(child); child = child.getTreeNext(); } } public static void saveWhitespacesInfo(ASTNode first) { if (first == null || isNodeGenerated(first) || getOldIndentation(first) >= 0) { return; } PsiElement psiElement = first.getPsi(); if (psiElement == null) { return; } PsiFile file = psiElement.getContainingFile(); setOldIndentation((TreeElement)first, IndentHelper.getInstance().getIndent(file.getProject(), file.getFileType(), first)); } public static int getOldIndentation(ASTNode node) { if (node == null) return -1; final Integer stored = node.getCopyableUserData(INDENT_INFO); return stored != null ? stored : -1; } public static void removeChildren(ASTNode parent, @NotNull ASTNode first, @NotNull ASTNode last) { final boolean tailingElement = last.getStartOffset() + last.getTextLength() == parent.getStartOffset() + parent.getTextLength(); final boolean forceReformat = needToForceReformat(parent, first, last); saveWhitespacesInfo(first); TreeElement child = (TreeElement)first; while (child != null) { //checkForOuters(child); if (child == last) break; child = child.getTreeNext(); } assert child == last : last + " is not a successor of " + first + " in the .getTreeNext() chain"; final ASTNode prevLeaf = TreeUtil.prevLeaf(first); final ASTNode nextLeaf = TreeUtil.nextLeaf(first); parent.removeRange(first, last.getTreeNext()); ASTNode nextLeafToAdjust = nextLeaf; if (nextLeafToAdjust != null && prevLeaf != null && nextLeafToAdjust.getTreeParent() == null) { //next element has invalidated nextLeafToAdjust = prevLeaf.getTreeNext(); } makePlaceHolderBetweenTokens(prevLeaf, nextLeafToAdjust, forceReformat, tailingElement); } private static boolean needToForceReformat(final ASTNode parent, final ASTNode first, final ASTNode last) { return parent == null || first.getStartOffset() != parent.getStartOffset() || parent.getText().trim().length() == getTrimmedTextLength(first, last) && needToForceReformat(parent.getTreeParent(), parent, parent); } private static int getTrimmedTextLength(ASTNode first, final ASTNode last) { final StringBuilder buffer = new StringBuilder(); while (first != last.getTreeNext()) { buffer.append(first.getText()); first = first.getTreeNext(); } return buffer.toString().trim().length(); } public static void replaceChild(ASTNode parent, @NotNull ASTNode oldChild, @NotNull ASTNode newChild) { saveWhitespacesInfo(oldChild); saveWhitespacesInfo(newChild); checkForOuters(oldChild); checkForOuters(newChild); LeafElement oldFirst = TreeUtil.findFirstLeaf(oldChild); parent.replaceChild(oldChild, newChild); final LeafElement firstLeaf = TreeUtil.findFirstLeaf(newChild); final ASTNode prevToken = TreeUtil.prevLeaf(newChild); if (firstLeaf != null) { final ASTNode nextLeaf = TreeUtil.nextLeaf(newChild); makePlaceHolderBetweenTokens(prevToken, firstLeaf, isFormattingRequired(prevToken, newChild), false); if (nextLeaf != null && !CharArrayUtil.containLineBreaks(nextLeaf.getText())) { makePlaceHolderBetweenTokens(TreeUtil.prevLeaf(nextLeaf), nextLeaf, false, false); } } else { if (oldFirst != null && prevToken == null) { ASTNode whitespaceNode = newChild.getTreeNext(); if (whitespaceNode != null && whitespaceNode.getElementType() == TokenType.WHITE_SPACE) { // Replacing non-empty prefix to empty shall remove whitespace parent.removeChild(whitespaceNode); } } makePlaceHolderBetweenTokens(prevToken, TreeUtil.nextLeaf(newChild), isFormattingRequired(prevToken, newChild), false); } } @Nullable private static ASTNode findFirstLeaf(ASTNode first, ASTNode last) { do { final LeafElement leaf = TreeUtil.findFirstLeaf(first); if (leaf != null) return leaf; first = first.getTreeNext(); if (first == null) return null; } while (first != last); return null; } @Nullable private static ASTNode findLastLeaf(ASTNode first, ASTNode last) { do { final ASTNode leaf = TreeUtil.findLastLeaf(last); if (leaf != null) return leaf; last = last.getTreePrev(); if (last == null) return null; } while (first != last); return null; } @Nullable private static ASTNode makePlaceHolderBetweenTokens(ASTNode left, ASTNode right, boolean forceReformat, boolean normalizeTrailingWS) { if (right == null) return left; markToReformatBefore(right, false); if (left == null) { markToReformatBefore(right, true); } else if (left.getElementType() == TokenType.WHITE_SPACE && left.getTreeNext() == null && normalizeTrailingWS) { // handle tailing whitespaces if element on the left has been removed final ASTNode prevLeaf = TreeUtil.prevLeaf(left); left.getTreeParent().removeChild(left); markToReformatBeforeOrInsertWhitespace(prevLeaf, right); left = right; } else if (left.getElementType() == TokenType.WHITE_SPACE && right.getElementType() == TokenType.WHITE_SPACE) { final String text; final int leftBlankLines = getBlankLines(left.getText()); final int rightBlankLines = getBlankLines(right.getText()); final boolean leaveRightText = leftBlankLines < rightBlankLines; if (leftBlankLines == 0 && rightBlankLines == 0) { text = left.getText() + right.getText(); } else if (leaveRightText) { text = right.getText(); } else { text = left.getText(); } if (leaveRightText || forceReformat) { final LeafElement merged = ASTFactory.whitespace(text); if (!leaveRightText) { left.getTreeParent().replaceChild(left, merged); right.getTreeParent().removeChild(right); } else { right.getTreeParent().replaceChild(right, merged); left.getTreeParent().removeChild(left); } left = merged; } else { right.getTreeParent().removeChild(right); } } else if (left.getElementType() != TokenType.WHITE_SPACE || forceReformat) { if (right.getElementType() == TokenType.WHITE_SPACE) { markWhitespaceForReformat(right); } else if (left.getElementType() == TokenType.WHITE_SPACE) { markWhitespaceForReformat(left); } else { markToReformatBeforeOrInsertWhitespace(left, right); } } return left; } private static void markWhitespaceForReformat(final ASTNode right) { final String text = right.getText(); final LeafElement merged = ASTFactory.whitespace(text); right.getTreeParent().replaceChild(right, merged); } private static void markToReformatBeforeOrInsertWhitespace(final ASTNode left, @NotNull final ASTNode right) { final Language leftLang = left != null ? PsiUtilCore.getNotAnyLanguage(left) : null; final Language rightLang = PsiUtilCore.getNotAnyLanguage(right); ASTNode generatedWhitespace = null; if (leftLang != null && leftLang.isKindOf(rightLang)) { generatedWhitespace = LanguageTokenSeparatorGenerators.INSTANCE.forLanguage(leftLang).generateWhitespaceBetweenTokens(left, right); } else if (rightLang.isKindOf(leftLang)) { generatedWhitespace = LanguageTokenSeparatorGenerators.INSTANCE.forLanguage(rightLang).generateWhitespaceBetweenTokens(left, right); } if (generatedWhitespace != null) { final TreeUtil.CommonParentState parentState = new TreeUtil.CommonParentState(); TreeUtil.prevLeaf((TreeElement)right, parentState); parentState.nextLeafBranchStart.getTreeParent().addChild(generatedWhitespace, parentState.nextLeafBranchStart); } else { markToReformatBefore(right, true); } } public static void markToReformatBefore(final ASTNode right, boolean value) { right.putCopyableUserData(REFORMAT_BEFORE_KEY, value ? true : null); } private static int getBlankLines(final String text) { int result = 0; int currentIndex = -1; while ((currentIndex = text.indexOf('\n', currentIndex + 1)) >= 0) result++; return result; } public static boolean isNodeGenerated(final ASTNode node) { return node == null || node.getCopyableUserData(GENERATED_FLAG) != null; } public static void setNodeGenerated(final ASTNode next, final boolean value) { if (next == null) return; next.putCopyableUserData(GENERATED_FLAG, value ? true : null); } public static void setNodeGeneratedRecursively(final ASTNode next, final boolean value) { if (next == null) return; setNodeGenerated(next, value); for (ASTNode child = next.getFirstChildNode(); child != null; child = child.getTreeNext()) { setNodeGeneratedRecursively(child, value); } } public static void setOldIndentation(final TreeElement treeElement, final int oldIndentation) { if (treeElement == null) return; treeElement.putCopyableUserData(INDENT_INFO, oldIndentation >= 0 ? oldIndentation : null); } public static boolean isMarkedToReformatBefore(final TreeElement element) { return element.getCopyableUserData(REFORMAT_BEFORE_KEY) != null; } @Nullable public static PsiElement createLineFeed(final PsiManager manager) { return Factory.createSingleLeafElement(TokenType.WHITE_SPACE, "\n", 0, 1, null, manager).getPsi(); } /** * Allows to answer if given node is configured to be reformatted. * * @param node node to check * @return <code>true</code> if given node is configured to be reformatted; <code>false</code> otherwise */ public static boolean isMarkedToReformat(final ASTNode node) { if (node.getCopyableUserData(REFORMAT_KEY) == null || !isSuspendedNodesReformattingAllowed()) { return false; } final NotNullFunction<ASTNode, Boolean> strategy = NODE_REFORMAT_STRATEGY.get(); return strategy == null || strategy.fun(node); } /** * Allows to define if given element should be reformatted later. * * @param node target element which <code>'reformat'</code> status should be changed * @param value <code>true</code> if the element should be reformatted; <code>false</code> otherwise */ public static void markToReformat(final ASTNode node, boolean value) { if (ALLOW_TO_MARK_NODES_TO_REFORMAT.get()) { node.putCopyableUserData(REFORMAT_KEY, value ? true : null); } } /** * We allow to mark particular {@link ASTNode AST nodes} to be reformatted later (e.g. we may want method definition and calls * to be reformatted when we perform <code>'change method signature'</code> refactoring. Hence, we mark corresponding expressions * to be reformatted). * <p/> * For convenience that is made automatically on AST/PSI level, i.e. every time target element change it automatically marks itself * to be reformatted. * <p/> * However, there is a possible case that particular element is changed because of formatting, hence, there is no need to mark * itself for postponed formatting one more time. This method allows to configure allowance of reformat markers processing * for the calling thread. I.e. this method may be called with <code>'false'</code> as an argument, hence, all further attempts * to {@link #markToReformat(ASTNode, boolean) mark node for postponed formatting} will have no effect until current method is * called with <code>'true'</code> as an argument. Hence, following usage scenario is expected: * <ol> * <li>This method is called with <code>'false'</code> argument;</li> * <li>Formatting is performed at dedicated <code>'try'</code> block;</li> * <li>This method is called with <code>'false'</code> argument from <code>'finally'</code> section;</li> * </ol> * * @param allow flag that defines if new reformat markers can be added from the current thread * @see #markToReformat(ASTNode, boolean) */ public static void allowToMarkNodesForPostponedFormatting(boolean allow) { ALLOW_TO_MARK_NODES_TO_REFORMAT.set(allow); } /** * @return <code>'allow suspended formatting'</code> flag value * @see #setAllowSuspendNodesReformatting(boolean) */ public static boolean isSuspendedNodesReformattingAllowed() { return ALLOW_NODES_REFORMATTING.get(); } /** * There is a possible case that particular PSI tree node is {@link #markToReformat(ASTNode, boolean) marked for reformatting}. * That means that there is a big chance that the node will be re-formatted during corresponding document processing * (e.g. on call to {@link PsiDocumentManager#doPostponedOperationsAndUnblockDocument(Document)}). * <p/> * However, there is a possible case that particular activity that triggers such document processing is not ready to the * situation when the document is modified because of postponed formatting. Hence, it may ask to suspend postponed formatting * for a while. This method allows to do that at thread-local manner. I.e. it's expected to be called as follows: * <pre> * <ol> * <li>This method is called with <code>'false'</code> argument;</li> * <li>Document is processed at dedicated <code>'try'</code> block;</li> * <li>This method is called with <code>'true'</code> argument from <code>'finally'</code> section;</li> * </ol> * </pre> */ public static void setAllowSuspendNodesReformatting(boolean allow) { ALLOW_NODES_REFORMATTING.set(allow); } /** * Allows to control the same process as {@link #setAllowSuspendNodesReformatting(boolean)} but on a node level. I.e. it allows * to answer if particular node can be reformatted if {@link #isSuspendedNodesReformattingAllowed() global reformatting is allowed}. * * @param strategy strategy to use; <code>null</code> as an indication that no fine-grained checking should be performed */ public static void setNodeReformatStrategy(@Nullable NotNullFunction<ASTNode, Boolean> strategy) { NODE_REFORMAT_STRATEGY.set(strategy); } }