blob: b2b5b5ff8eeec724d30228f44928882756063c0b [file] [log] [blame]
/*
* Copyright 2000-2014 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.xml;
import com.intellij.formatting.*;
import com.intellij.lang.*;
import com.intellij.lang.xml.XMLLanguage;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.psi.formatter.WhiteSpaceFormattingStrategy;
import com.intellij.psi.formatter.WhiteSpaceFormattingStrategyFactory;
import com.intellij.psi.formatter.common.AbstractBlock;
import com.intellij.psi.impl.source.SourceTreeToPsiMap;
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.search.PsiElementProcessor;
import com.intellij.psi.templateLanguages.TemplateLanguageFileViewProvider;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import com.intellij.psi.xml.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractXmlBlock extends AbstractBlock {
protected XmlFormattingPolicy myXmlFormattingPolicy;
protected final XmlInjectedLanguageBlockBuilder myInjectedBlockBuilder;
private final boolean myPreserveSpace;
protected AbstractXmlBlock(final ASTNode node,
final Wrap wrap,
final Alignment alignment,
final XmlFormattingPolicy policy) {
this(node, wrap, alignment, policy, false);
}
protected AbstractXmlBlock(final ASTNode node,
final Wrap wrap,
final Alignment alignment,
final XmlFormattingPolicy policy,
final boolean preserveSpace) {
super(node, wrap, alignment);
myXmlFormattingPolicy = policy;
if (node.getTreeParent() == null) {
myXmlFormattingPolicy.setRootBlock(node, this);
}
myInjectedBlockBuilder = new XmlInjectedLanguageBlockBuilder(myXmlFormattingPolicy);
myPreserveSpace = shouldPreserveSpace(node, preserveSpace);
}
/**
* Handles xml:space='preserve|default' attribute.
* See <a href="http://www.w3.org/TR/2004/REC-xml-20040204/#sec-white-space">Extensible Markup Language (XML) 1.0 (Third Edition),
* White Space Handling</a>
*
* @return True if the space must be preserved (xml:space='preserve'), false if the attribute
* contains 'default'. If the attribute is not defined, return the current value.
*/
private static boolean shouldPreserveSpace(ASTNode node, boolean defaultValue) {
if (node.getPsi() instanceof XmlTag) {
XmlTag tag = (XmlTag)node.getPsi();
if (tag != null) {
XmlAttribute spaceAttr = tag.getAttribute("xml:space");
if (spaceAttr != null) {
String value = spaceAttr.getValue();
if ("preserve".equals(value)) {
return true;
}
if ("default".equals(value)) {
return false;
}
}
}
}
return defaultValue;
}
public boolean isPreserveSpace() {
return myPreserveSpace;
}
public static WrapType getWrapType(final int type) {
if (type == CommonCodeStyleSettings.DO_NOT_WRAP) return WrapType.NONE;
if (type == CommonCodeStyleSettings.WRAP_ALWAYS) return WrapType.ALWAYS;
if (type == CommonCodeStyleSettings.WRAP_AS_NEEDED) return WrapType.NORMAL;
return WrapType.CHOP_DOWN_IF_LONG;
}
protected Alignment chooseAlignment(final ASTNode child, final Alignment attrAlignment, final Alignment textAlignment) {
if (myNode.getElementType() == XmlElementType.XML_TEXT) return getAlignment();
final IElementType elementType = child.getElementType();
if (elementType == XmlElementType.XML_ATTRIBUTE && myXmlFormattingPolicy.getShouldAlignAttributes()) return attrAlignment;
if (elementType == XmlElementType.XML_TEXT && myXmlFormattingPolicy.getShouldAlignText()) return textAlignment;
return null;
}
private Wrap getTagEndWrapping(final XmlTag parent) {
return Wrap.createWrap(myXmlFormattingPolicy.getWrappingTypeForTagEnd(parent), true);
}
protected Wrap chooseWrap(final ASTNode child, final Wrap tagBeginWrap, final Wrap attrWrap, final Wrap textWrap) {
if (myNode.getElementType() == XmlElementType.XML_TEXT) return textWrap;
final IElementType elementType = child.getElementType();
if (elementType == XmlElementType.XML_ATTRIBUTE) return attrWrap;
if (elementType == XmlTokenType.XML_START_TAG_START) return tagBeginWrap;
if (elementType == XmlTokenType.XML_END_TAG_START) {
final PsiElement parent = SourceTreeToPsiMap.treeElementToPsi(child.getTreeParent());
if (parent instanceof XmlTag) {
final XmlTag tag = (XmlTag)parent;
if (canWrapTagEnd(tag)) {
return getTagEndWrapping(tag);
}
}
return null;
}
if (elementType == XmlElementType.XML_TEXT || elementType == XmlTokenType.XML_DATA_CHARACTERS) return textWrap;
return null;
}
protected boolean canWrapTagEnd(final XmlTag tag) {
return tag.getSubTags().length > 0;
}
protected XmlTag getTag() {
return getTag(myNode);
}
protected static XmlTag getTag(final ASTNode node) {
final PsiElement element = SourceTreeToPsiMap.treeElementToPsi(node);
if (element instanceof XmlTag) {
return (XmlTag)element;
}
else {
return null;
}
}
protected Wrap createTagBeginWrapping(final XmlTag tag) {
return Wrap.createWrap(myXmlFormattingPolicy.getWrappingTypeForTagBegin(tag), true);
}
@Nullable
protected
ASTNode processChild(List<Block> result,
final ASTNode child,
final Wrap wrap,
final Alignment alignment,
final Indent indent) {
final Language myLanguage = myNode.getPsi().getLanguage();
final PsiElement childPsi = child.getPsi();
final Language childLanguage = childPsi.getLanguage();
if (useMyFormatter(myLanguage, childLanguage, childPsi)) {
XmlTag tag = getAnotherTreeTag(child);
if (tag != null
&& containsTag(tag)
&& doesNotIntersectSubTagsWith(tag)) {
ASTNode currentChild = createAnotherTreeNode(result, child, tag, indent, wrap, alignment);
if (currentChild == null) {
return null;
}
while (currentChild != null && currentChild.getTreeParent() != myNode && currentChild.getTreeParent() != child.getTreeParent()) {
currentChild = processAllChildrenFrom(result, currentChild, wrap, alignment, indent);
if (currentChild != null && (currentChild.getTreeParent() == myNode || currentChild.getTreeParent() == child.getTreeParent())) {
return currentChild;
}
if (currentChild != null) {
currentChild = currentChild.getTreeParent();
}
}
return currentChild;
}
processSimpleChild(child, indent, result, wrap, alignment);
return child;
}
else {
myInjectedBlockBuilder.addInjectedLanguageBlockWrapper(result, child, indent, 0, null);
return child;
}
}
protected boolean doesNotIntersectSubTagsWith(final PsiElement tag) {
final TextRange tagRange = tag.getTextRange();
final XmlTag[] subTags = getSubTags();
for (XmlTag subTag : subTags) {
final TextRange subTagRange = subTag.getTextRange();
if (subTagRange.getEndOffset() < tagRange.getStartOffset()) continue;
if (subTagRange.getStartOffset() > tagRange.getEndOffset()) return true;
if (tagRange.getStartOffset() > subTagRange.getStartOffset() && tagRange.getEndOffset() < subTagRange.getEndOffset()) return false;
if (tagRange.getEndOffset() > subTagRange.getStartOffset() && tagRange.getEndOffset() < subTagRange.getEndOffset()) return false;
}
return true;
}
private XmlTag[] getSubTags() {
if (myNode instanceof XmlTag) {
return ((XmlTag)myNode.getPsi()).getSubTags();
}
else if (myNode.getPsi() instanceof XmlElement) {
return collectSubTags((XmlElement)myNode.getPsi());
}
else {
return new XmlTag[0];
}
}
private static XmlTag[] collectSubTags(final XmlElement node) {
final List<XmlTag> result = new ArrayList<XmlTag>();
node.processElements(new PsiElementProcessor() {
@Override
public boolean execute(@NotNull final PsiElement element) {
if (element instanceof XmlTag) {
result.add((XmlTag)element);
}
return true;
}
}, node);
return result.toArray(new XmlTag[result.size()]);
}
protected boolean containsTag(final PsiElement tag) {
final ASTNode closingTagStart = XmlChildRole.CLOSING_TAG_START_FINDER.findChild(myNode);
final ASTNode startTagStart = XmlChildRole.START_TAG_END_FINDER.findChild(myNode);
if (closingTagStart == null && startTagStart == null) {
return tag.getTextRange().getEndOffset() <= myNode.getTextRange().getEndOffset();
}
else if (closingTagStart == null) {
return false;
}
else {
return tag.getTextRange().getEndOffset() <= closingTagStart.getTextRange().getEndOffset();
}
}
private ASTNode processAllChildrenFrom(final List<Block> result,
@NotNull final ASTNode child,
final Wrap wrap,
final Alignment alignment,
final Indent indent) {
ASTNode resultNode = child;
ASTNode currentChild = child.getTreeNext();
while (currentChild != null && currentChild.getElementType() != XmlTokenType.XML_END_TAG_START) {
if (!containsWhiteSpacesOnly(currentChild)) {
currentChild = processChild(result, currentChild, wrap, alignment, indent);
resultNode = currentChild;
}
if (currentChild != null) {
currentChild = currentChild.getTreeNext();
}
}
return resultNode;
}
protected void processSimpleChild(final ASTNode child,
final Indent indent,
final List<Block> result,
final Wrap wrap,
final Alignment alignment) {
if (isXmlTag(child)) {
result.add(createTagBlock(child, indent != null ? indent : Indent.getNoneIndent(), wrap, alignment));
} else if (child.getElementType() == XmlElementType.XML_DOCTYPE) {
result.add(
new XmlBlock(child, wrap, alignment, myXmlFormattingPolicy, indent, null, isPreserveSpace()) {
@Override
protected Wrap getDefaultWrap(final ASTNode node) {
final IElementType type = node.getElementType();
return type == XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN
? Wrap.createWrap(getWrapType(myXmlFormattingPolicy.getAttributesWrap()), false) : null;
}
}
);
}
else {
result.add(createSimpleChild(child, indent, wrap, alignment));
}
}
protected XmlBlock createSimpleChild(final ASTNode child, final Indent indent, final Wrap wrap, final Alignment alignment) {
return new XmlBlock(child, wrap, alignment, myXmlFormattingPolicy, indent, null, isPreserveSpace());
}
protected XmlTagBlock createTagBlock(final ASTNode child, final Indent indent, final Wrap wrap, final Alignment alignment) {
return new XmlTagBlock(child, wrap, alignment, myXmlFormattingPolicy, indent != null ? indent : Indent.getNoneIndent(), isPreserveSpace());
}
@Nullable
protected XmlTag findXmlTagAt(final ASTNode child, final int startOffset) {
return null;
}
@Nullable
protected ASTNode createAnotherTreeNode(final List<Block> result,
final ASTNode child,
PsiElement tag,
final Indent indent,
final Wrap wrap, final Alignment alignment) {
return null;
}
@Nullable
protected Block createAnotherTreeTagBlock(final PsiElement tag, final Indent childIndent) {
return null;
}
protected XmlFormattingPolicy createPolicyFor() {
return myXmlFormattingPolicy;
}
@Nullable
protected XmlTag getAnotherTreeTag(final ASTNode child) {
return null;
}
protected boolean isXmlTag(final ASTNode child) {
return isXmlTag(child.getPsi());
}
protected boolean isXmlTag(final PsiElement psi) {
return psi instanceof XmlTag;
}
protected boolean useMyFormatter(final Language myLanguage, final Language childLanguage, final PsiElement childPsi) {
if (myLanguage == childLanguage ||
childLanguage == StdFileTypes.HTML.getLanguage() ||
childLanguage == StdFileTypes.XHTML.getLanguage() ||
childLanguage == StdFileTypes.XML.getLanguage()) {
return true;
}
final FormattingModelBuilder childFormatter = LanguageFormatting.INSTANCE.forLanguage(childLanguage);
return childFormatter == null ||
childFormatter instanceof DelegatingFormattingModelBuilder &&
((DelegatingFormattingModelBuilder)childFormatter).dontFormatMyModel();
}
protected boolean isJspxJavaContainingNode(final ASTNode child) {
return false;
}
public abstract boolean insertLineBreakBeforeTag();
public abstract boolean removeLineBreakBeforeTag();
protected Spacing createDefaultSpace(boolean forceKeepLineBreaks, final boolean inText) {
boolean shouldKeepLineBreaks = getShouldKeepLineBreaks(inText, forceKeepLineBreaks);
return Spacing.createSpacing(0, Integer.MAX_VALUE, 0, shouldKeepLineBreaks, myXmlFormattingPolicy.getKeepBlankLines());
}
private boolean getShouldKeepLineBreaks(final boolean inText, final boolean forceKeepLineBreaks) {
if (forceKeepLineBreaks) {
return true;
}
if (inText && myXmlFormattingPolicy.getShouldKeepLineBreaksInText()) {
return true;
}
if (!inText && myXmlFormattingPolicy.getShouldKeepLineBreaks()) {
return true;
}
return false;
}
public abstract boolean isTextElement();
private static final Logger LOG = Logger.getInstance("#com.intellij.psi.formatter.xml.AbstractXmlBlock");
protected void createJspTextNode(final List<Block> localResult, final ASTNode child, final Indent indent) {
}
@Nullable
protected static ASTNode findChildAfter(@NotNull final ASTNode child, final int endOffset) {
TreeElement fileNode = TreeUtil.getFileElement((TreeElement)child);
final LeafElement leaf = fileNode.findLeafElementAt(endOffset);
if (leaf != null && leaf.getStartOffset() == endOffset && endOffset > 0) {
return fileNode.findLeafElementAt(endOffset - 1);
}
return leaf;
}
@Override
public boolean isLeaf() {
return (isComment(myNode)) ||
myNode.getElementType() == TokenType.WHITE_SPACE ||
myNode.getElementType() == XmlTokenType.XML_DATA_CHARACTERS ||
myNode.getElementType() == XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN;
}
private static boolean isComment(final ASTNode node) {
final PsiElement psiElement = SourceTreeToPsiMap.treeElementToPsi(node);
if (psiElement instanceof PsiComment) return true;
final ParserDefinition parserDefinition = LanguageParserDefinitions.INSTANCE.forLanguage(psiElement.getLanguage());
if (parserDefinition == null) return false;
final TokenSet commentTokens = parserDefinition.getCommentTokens();
return commentTokens.contains(node.getElementType());
}
public void setXmlFormattingPolicy(final XmlFormattingPolicy xmlFormattingPolicy) {
myXmlFormattingPolicy = xmlFormattingPolicy;
}
protected boolean buildInjectedPsiBlocks(List<Block> result, final ASTNode child, Wrap wrap, Alignment alignment, Indent indent) {
if (myInjectedBlockBuilder.addInjectedBlocks(result, child, wrap, alignment, indent)) {
return true;
}
PsiFile containingFile = child.getPsi().getContainingFile();
FileViewProvider fileViewProvider = containingFile.getViewProvider();
if (fileViewProvider instanceof TemplateLanguageFileViewProvider) {
Language templateLanguage = ((TemplateLanguageFileViewProvider)fileViewProvider).getTemplateDataLanguage();
PsiElement at = fileViewProvider.findElementAt(child.getStartOffset(), templateLanguage);
if (at instanceof XmlToken) {
at = at.getParent();
}
// TODO: several comments
if (at instanceof PsiComment &&
at.getTextRange().equals(child.getTextRange()) &&
at.getNode() != child) {
return buildInjectedPsiBlocks(result, at.getNode(), wrap, alignment, indent);
}
}
return false;
}
public boolean isCDATAStart() {
return myNode.getElementType() == XmlTokenType.XML_CDATA_START;
}
public boolean isCDATAEnd() {
return myNode.getElementType() == XmlTokenType.XML_CDATA_END;
}
public static boolean containsWhiteSpacesOnly(@NotNull ASTNode node) {
PsiElement psiElement = node.getPsi();
if (psiElement instanceof PsiWhiteSpace) return true;
Language nodeLang = psiElement.getLanguage();
if (!nodeLang.isKindOf(XMLLanguage.INSTANCE) ||
isTextOnlyNode(node) ||
node.getElementType() == XmlElementType.XML_PROLOG) {
WhiteSpaceFormattingStrategy strategy = WhiteSpaceFormattingStrategyFactory.getStrategy(nodeLang);
int length = node.getTextLength();
return strategy.check(node.getChars(), 0, length) >= length;
}
return false;
}
private static boolean isTextOnlyNode(@NotNull ASTNode node) {
if (node.getPsi() instanceof XmlText) return true;
ASTNode firstChild = node.getFirstChildNode();
ASTNode lastChild = node.getLastChildNode();
if (firstChild != null && firstChild == lastChild && firstChild.getPsi() instanceof XmlText) {
return true;
}
return false;
}
}