IOOBE: XmlTagBlock.isAfterAttribute
[idea/community.git] / xml / impl / src / com / intellij / psi / formatter / xml / XmlTagBlock.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.psi.formatter.xml;
17
18 import com.intellij.formatting.*;
19 import com.intellij.lang.ASTNode;
20 import com.intellij.lang.Language;
21 import com.intellij.lang.LanguageFormatting;
22 import com.intellij.openapi.util.Ref;
23 import com.intellij.openapi.util.TextRange;
24 import com.intellij.psi.PsiElement;
25 import com.intellij.psi.PsiFile;
26 import com.intellij.psi.PsiLanguageInjectionHost;
27 import com.intellij.psi.tree.IElementType;
28 import com.intellij.psi.formatter.FormatterUtil;
29 import com.intellij.psi.xml.XmlElementType;
30 import com.intellij.psi.xml.XmlTag;
31 import org.jetbrains.annotations.NotNull;
32 import org.jetbrains.annotations.Nullable;
33
34 import java.util.ArrayList;
35 import java.util.List;
36
37 public class XmlTagBlock extends AbstractXmlBlock{
38   private final Indent myIndent;
39
40   public XmlTagBlock(final ASTNode node,
41                      final Wrap wrap,
42                      final Alignment alignment,
43                      final XmlFormattingPolicy policy,
44                      final Indent indent) {
45     super(node, wrap, alignment, policy);
46     myIndent = indent;
47   }
48
49   protected List<Block> buildChildren() {
50     ASTNode child = myNode.getFirstChildNode();
51     final Wrap attrWrap = Wrap.createWrap(getWrapType(myXmlFormattingPolicy.getAttributesWrap()), false);
52     final Wrap textWrap = Wrap.createWrap(getWrapType(myXmlFormattingPolicy.getTextWrap(getTag())), true);
53     final Wrap tagBeginWrap = createTagBeginWrapping(getTag());
54     final Alignment attrAlignment = Alignment.createAlignment();
55     final Alignment textAlignment = Alignment.createAlignment();
56     final ArrayList<Block> result = new ArrayList<Block>(3);
57     ArrayList<Block> localResult = new ArrayList<Block>(1);
58
59     boolean insideTag = true;
60
61     while (child != null) {
62       if (!FormatterUtil.containsWhiteSpacesOnly(child) && child.getTextLength() > 0){
63
64         Wrap wrap = chooseWrap(child, tagBeginWrap, attrWrap, textWrap);
65         Alignment alignment = chooseAlignment(child, attrAlignment, textAlignment);
66
67         if (child.getElementType() == XmlElementType.XML_TAG_END) {
68           child = processChild(localResult,child, wrap, alignment, null);
69           result.add(createTagDescriptionNode(localResult));
70           localResult = new ArrayList<Block>(1);
71           insideTag = true;
72         }
73         else if (child.getElementType() == XmlElementType.XML_START_TAG_START) {
74           insideTag = false;
75           if (!localResult.isEmpty()) {
76             result.add(createTagContentNode(localResult));
77           }
78           localResult = new ArrayList<Block>(1);
79           child = processChild(localResult,child, wrap, alignment, null);
80         }
81         else if (child.getElementType() == XmlElementType.XML_END_TAG_START) {
82           insideTag = false;
83           if (!localResult.isEmpty()) {
84             result.add(createTagContentNode(localResult));
85             localResult = new ArrayList<Block>(1);
86           }
87           child = processChild(localResult,child, wrap, alignment, null);
88         } else if (child.getElementType() == XmlElementType.XML_EMPTY_ELEMENT_END) {
89           child = processChild(localResult,child, wrap, alignment, null);
90           result.add(createTagDescriptionNode(localResult));
91           localResult = new ArrayList<Block>(1);
92         }
93         else if (isJspxJavaContainingNode(child)) {
94           createJspTextNode(localResult, child, getChildIndent());
95         }
96         /*
97         else if (child.getElementType() == ElementType.XML_TEXT) {
98           child  = createXmlTextBlocks(localResult, child, wrap, alignment);
99         }
100         */
101         else {
102           final Indent indent;
103
104           if (isJspResult(localResult)) {
105             //indent = FormatterEx.getInstance().getNoneIndent();
106             indent = getChildrenIndent();
107           } else if (!insideTag) {
108             indent = null;
109           }
110           else {
111             indent = getChildrenIndent();
112           }
113
114           child = processChild(localResult,child, wrap, alignment, indent);
115         }
116       }
117       if (child != null) {
118         child = child.getTreeNext();
119       }
120     }
121
122     if (!localResult.isEmpty()) {
123       result.add(createTagContentNode(localResult));
124     }
125
126     return result;
127
128   }
129
130   protected boolean isJspResult(final ArrayList<Block> localResult) {
131     return false;
132   }
133
134   protected
135   @Nullable
136   ASTNode processChild(List<Block> result, final ASTNode child, final Wrap wrap, final Alignment alignment, final Indent indent) {
137     IElementType type = child.getElementType();
138     if (type == XmlElementType.XML_TEXT) {
139       final PsiElement parent = child.getPsi().getParent();
140
141       if (parent instanceof XmlTag && ((XmlTag)parent).getSubTags().length == 0) {
142         if (buildInjectedPsiBlocks(result, child, wrap, alignment, indent)) return child;
143       }
144       return createXmlTextBlocks(result, child, wrap, alignment);
145     } else if (type == XmlElementType.XML_COMMENT) {
146       if (buildInjectedPsiBlocks(result, child, wrap, alignment, indent)) return child;
147       return super.processChild(result, child, wrap, alignment, indent);
148     }
149     else {
150       return super.processChild(result, child, wrap, alignment, indent);
151     }
152   }
153
154   private boolean buildInjectedPsiBlocks(List<Block> result, final ASTNode child, Wrap wrap, Alignment alignment, Indent indent) {
155     final PsiFile[] injectedFile = new PsiFile[1];
156     final Ref<Integer> offset = new Ref<Integer>();
157     final Ref<Integer> offset2 = new Ref<Integer>();
158     final Ref<Integer> prefixLength = new Ref<Integer>();
159     final Ref<Integer> suffixLength = new Ref<Integer>();
160
161     ((PsiLanguageInjectionHost)child.getPsi()).processInjectedPsi(new PsiLanguageInjectionHost.InjectedPsiVisitor() {
162       public void visit(@NotNull final PsiFile injectedPsi, @NotNull final List<PsiLanguageInjectionHost.Shred> places) {
163         if (places.size() == 1) {
164           final PsiLanguageInjectionHost.Shred shred = places.get(0);
165           final TextRange textRange = shred.getRangeInsideHost();
166           String childText;
167
168           if (( child.getTextLength() == textRange.getEndOffset() &&
169                 textRange.getStartOffset() == 0
170               ) ||
171               ( canProcessFragments((childText = child.getText()).substring(0, textRange.getStartOffset())) &&
172                 canProcessFragments(childText.substring(textRange.getEndOffset()))
173               )
174              ) {
175             injectedFile[0] = injectedPsi;
176             offset.set(textRange.getStartOffset());
177             offset2.set(textRange.getEndOffset());
178             prefixLength.set(shred.prefix != null ? shred.prefix.length():0);
179             suffixLength.set(shred.suffix != null ? shred.suffix.length():0);
180           }
181         }
182       }
183
184       private boolean canProcessFragments(String s) {
185         IElementType type = child.getElementType();
186         if (type == XmlElementType.XML_TEXT) {
187           s = s.trim();
188           s = s.replace("<![CDATA[","");
189           s = s.replace("]]>","");
190         } else if (type == XmlElementType.XML_COMMENT) {   // <!--[if IE]>, <![endif]--> of conditional comments injection
191           s = "";
192         }
193
194         return s.length() == 0;
195       }
196     });
197
198     if  (injectedFile[0] != null) {
199       final Language childLanguage = injectedFile[0].getLanguage();
200       final FormattingModelBuilder builder = LanguageFormatting.INSTANCE.forContext(childLanguage, child.getPsi());
201
202       if (builder != null) {
203         final int startOffset = offset.get().intValue();
204         final int endOffset = offset2.get().intValue();
205         TextRange range = child.getTextRange();
206
207         int childOffset = range.getStartOffset();
208         if (startOffset != 0) {
209           final ASTNode leaf = child.findLeafElementAt(startOffset - 1);
210           result.add(new XmlBlock(leaf, wrap, alignment, myXmlFormattingPolicy, indent, new TextRange(childOffset, childOffset + startOffset)));
211         }
212
213         createAnotherLanguageBlockWrapper(childLanguage, injectedFile[0].getNode(), result, indent,
214                                           childOffset + startOffset,
215                                           new TextRange(prefixLength.get(), injectedFile[0].getTextLength() - suffixLength.get()));
216
217         if (endOffset != child.getTextLength()) {
218           final ASTNode leaf = child.findLeafElementAt(endOffset);
219           result.add(new XmlBlock(leaf, wrap, alignment, myXmlFormattingPolicy, indent, new TextRange(childOffset + endOffset, range.getEndOffset())));
220         }
221         return true;
222       }
223     }
224     return false;
225   }
226
227   private Indent getChildrenIndent() {
228     return myXmlFormattingPolicy.indentChildrenOf(getTag())
229            ? Indent.getNormalIndent()
230            : Indent.getNoneIndent();
231   }
232
233   public Indent getIndent() {
234     return myIndent;
235   }
236
237   private ASTNode createXmlTextBlocks(final List<Block> list, final ASTNode textNode, final Wrap wrap, final Alignment alignment) {
238     ASTNode child = textNode.getFirstChildNode();
239     return createXmlTextBlocks(list, textNode, child, wrap, alignment);
240   }
241
242   private ASTNode createXmlTextBlocks(final List<Block> list, final ASTNode textNode, ASTNode child,
243                                       final Wrap wrap,
244                                       final Alignment alignment
245   ) {
246     while (child != null) {
247       if (!FormatterUtil.containsWhiteSpacesOnly(child) && child.getTextLength() > 0){
248         final Indent indent = getChildrenIndent();
249         child = processChild(list,child,  wrap, alignment, indent);
250         if (child == null) return child;
251         if (child.getTreeParent() != textNode) {
252           if (child.getTreeParent() != myNode) {
253             return createXmlTextBlocks(list, child.getTreeParent(), child.getTreeNext(), wrap, alignment);
254           } else {
255             return child;
256           }
257         }
258       }
259       child = child.getTreeNext();
260     }
261     return textNode;
262   }
263
264   private Block createTagContentNode(final ArrayList<Block> localResult) {
265     return createSyntheticBlock(localResult, getChildrenIndent());
266   }
267
268   protected Block createSyntheticBlock(final ArrayList<Block> localResult, final Indent childrenIndent) {
269     return new SyntheticBlock(localResult, this, Indent.getNoneIndent(), myXmlFormattingPolicy, childrenIndent);
270   }
271
272   private Block createTagDescriptionNode(final ArrayList<Block> localResult) {
273     return createSyntheticBlock(localResult, null);
274   }
275
276   public Spacing getSpacing(Block child1, Block child2) {
277     if(child1 instanceof AbstractSyntheticBlock && child2 instanceof AbstractSyntheticBlock) {
278       return getSpacing((AbstractSyntheticBlock)child1, (AbstractSyntheticBlock)child2);
279     }
280     return null;
281   }
282
283   protected Spacing getSpacing(final AbstractSyntheticBlock syntheticBlock1, final AbstractSyntheticBlock syntheticBlock2) {
284     if (syntheticBlock2.startsWithCDATA() || syntheticBlock1.endsWithCDATA()) {
285       return Spacing.getReadOnlySpacing();
286     }
287
288     if (syntheticBlock2.isJspTextBlock() || syntheticBlock1.isJspTextBlock()) {
289       return Spacing.createSafeSpacing(myXmlFormattingPolicy.getShouldKeepLineBreaks(), myXmlFormattingPolicy.getKeepBlankLines());
290     }
291
292     if (syntheticBlock2.isJspxTextBlock() || syntheticBlock1.isJspxTextBlock()) {
293       return Spacing.createSpacing(0, 0, 1, myXmlFormattingPolicy.getShouldKeepLineBreaks(), myXmlFormattingPolicy.getKeepBlankLines());
294     }
295
296     if (myXmlFormattingPolicy.keepWhiteSpacesInsideTag(getTag())) return Spacing.getReadOnlySpacing();
297
298     if (myXmlFormattingPolicy.getShouldKeepWhiteSpaces()) {
299       return Spacing.getReadOnlySpacing();
300     }
301
302     if (syntheticBlock2.startsWithTag() ) {
303       final XmlTag startTag = syntheticBlock2.getStartTag();
304       if (myXmlFormattingPolicy.keepWhiteSpacesInsideTag(startTag) && startTag.textContains('\n')) {
305         return getChildrenIndent() != Indent.getNoneIndent() ? Spacing.getReadOnlySpacing():Spacing.createSpacing(0,0,0,true,myXmlFormattingPolicy.getKeepBlankLines());
306       }
307     }
308
309     boolean saveSpacesBetweenTagAndText = myXmlFormattingPolicy.shouldSaveSpacesBetweenTagAndText() &&
310       syntheticBlock1.getTextRange().getEndOffset() < syntheticBlock2.getTextRange().getStartOffset();
311
312     if (syntheticBlock1.endsWithTextElement() && syntheticBlock2.startsWithTextElement()) {
313       return Spacing.createSafeSpacing(myXmlFormattingPolicy.getShouldKeepLineBreaksInText(), myXmlFormattingPolicy.getKeepBlankLines());
314     }
315
316     if (syntheticBlock1.endsWithText()) { //text</tag
317       if (syntheticBlock1.insertLineFeedAfter()) {
318         return Spacing.createDependentLFSpacing(0, 0, getTag().getTextRange(), myXmlFormattingPolicy.getShouldKeepLineBreaks(), myXmlFormattingPolicy.getKeepBlankLines());
319       }
320       if (saveSpacesBetweenTagAndText) {
321         return Spacing.createSafeSpacing(myXmlFormattingPolicy.getShouldKeepLineBreaks(), myXmlFormattingPolicy.getKeepBlankLines());
322       }
323       return Spacing.createSpacing(0, 0, 0, myXmlFormattingPolicy.getShouldKeepLineBreaks(), myXmlFormattingPolicy.getKeepBlankLines());
324
325     } else if (syntheticBlock1.isTagDescription() && syntheticBlock2.isTagDescription()) { //></
326       return Spacing.createSpacing(0, 0, 0, myXmlFormattingPolicy.getShouldKeepLineBreaks(), myXmlFormattingPolicy.getKeepBlankLines());
327     } else if (syntheticBlock2.startsWithText()) { //>text
328       if (saveSpacesBetweenTagAndText) {
329         return Spacing.createSafeSpacing(true, myXmlFormattingPolicy.getKeepBlankLines());
330       }
331       return Spacing.createSpacing(0, 0, 0, true, myXmlFormattingPolicy.getKeepBlankLines());
332     } else if (syntheticBlock1.isTagDescription() && syntheticBlock2.startsWithTag()) {
333       return Spacing.createSpacing(0, 0, 0, true, myXmlFormattingPolicy.getKeepBlankLines());
334     } else if (syntheticBlock1.insertLineFeedAfter()) {
335       return Spacing.createSpacing(0,0,1,true,myXmlFormattingPolicy.getKeepBlankLines());
336     } else if (syntheticBlock1.endsWithTag() && syntheticBlock2.isTagDescription()) {
337       return Spacing.createSpacing(0, 0, 0, true, myXmlFormattingPolicy.getKeepBlankLines());
338     } else {
339       return createDefaultSpace(true, true);
340     }
341   }
342
343   public boolean insertLineBreakBeforeTag() {
344     return myXmlFormattingPolicy.insertLineBreakBeforeTag(getTag());
345   }
346
347   public boolean removeLineBreakBeforeTag() {
348     return myXmlFormattingPolicy.removeLineBreakBeforeTag(getTag());
349   }
350
351   public boolean isTextElement() {
352     return myXmlFormattingPolicy.isTextElement(getTag());
353   }
354
355   @Override
356   @NotNull
357   public ChildAttributes getChildAttributes(final int newChildIndex) {
358     if (isAfterAttribute(newChildIndex)) {
359       List<Block> subBlocks = getSubBlocks();
360       Block subBlock = subBlocks.get(newChildIndex - 1);
361       int prevSubBlockChildrenCount = subBlock.getSubBlocks().size();
362       return subBlock.getChildAttributes(prevSubBlockChildrenCount);
363     }
364     else {
365       if (myXmlFormattingPolicy.indentChildrenOf(getTag())) {
366         return new ChildAttributes(Indent.getNormalIndent(), null);
367       } else {
368         return new ChildAttributes(Indent.getNoneIndent(), null);
369       }
370     }
371   }
372
373   private boolean isAfterAttribute(final int newChildIndex) {
374     List<Block> subBlocks = getSubBlocks();
375     int index = newChildIndex - 1;
376     Block prevBlock = index < subBlocks.size() ? subBlocks.get(index):null;
377     return prevBlock instanceof SyntheticBlock && ((SyntheticBlock)prevBlock).endsWithAttribute();
378   }
379 }