replaced <code></code> with more concise {@code}
[idea/community.git] / xml / impl / src / com / intellij / xml / template / formatter / AbstractXmlTemplateFormattingModelBuilder.java
1 /*
2  * Copyright 2000-2017 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.xml.template.formatter;
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.lang.xml.XMLLanguage;
23 import com.intellij.openapi.util.TextRange;
24 import com.intellij.psi.FileViewProvider;
25 import com.intellij.psi.PsiElement;
26 import com.intellij.psi.PsiFile;
27 import com.intellij.psi.codeStyle.CodeStyleSettings;
28 import com.intellij.psi.formatter.DocumentBasedFormattingModel;
29 import com.intellij.psi.formatter.FormatterUtil;
30 import com.intellij.psi.formatter.FormattingDocumentModelImpl;
31 import com.intellij.psi.formatter.xml.*;
32 import com.intellij.psi.templateLanguages.OuterLanguageElement;
33 import com.intellij.psi.templateLanguages.SimpleTemplateLanguageFormattingModelBuilder;
34 import com.intellij.psi.templateLanguages.TemplateLanguageFileViewProvider;
35 import com.intellij.psi.util.PsiTreeUtil;
36 import com.intellij.psi.xml.XmlAttributeValue;
37 import com.intellij.psi.xml.XmlTag;
38 import org.jetbrains.annotations.NotNull;
39 import org.jetbrains.annotations.Nullable;
40
41 import java.util.ArrayList;
42 import java.util.List;
43
44 /**
45  * <p>
46  * Suitable for XML/HTML templates. Creates a model which provides correct indentation for a hierarchy of nested markup/template language
47  * blocks. For example:
48  * <pre>
49  *    &lt;div&gt;
50  *       &lt;?if (condition):?&gt;
51  *         &lt;div&gt;content&lt;/div&gt;
52  *       &lt;?endif?&gt;
53  *     &lt;/div&gt;
54  * </pre>
55  * where template conditional block is indented inside HTML &lt;div&gt; tag and in turn &lt;div&gt;content&lt;div&gt; tag is indented
56  * inside its surrounding 'if' block.
57  */
58 public abstract class AbstractXmlTemplateFormattingModelBuilder extends SimpleTemplateLanguageFormattingModelBuilder {
59   @NotNull
60   @Override
61   public FormattingModel createModel(PsiElement element, CodeStyleSettings settings) {
62     final PsiFile psiFile = element.getContainingFile();
63     if (psiFile.getViewProvider() instanceof TemplateLanguageFileViewProvider) {
64       final TemplateLanguageFileViewProvider viewProvider = (TemplateLanguageFileViewProvider)psiFile.getViewProvider();
65       if (isTemplateFile(psiFile)) {
66         Language templateDataLanguage = viewProvider.getTemplateDataLanguage();
67         if (templateDataLanguage != psiFile.getLanguage()) {
68           return createDataLanguageFormattingModel(
69             viewProvider.getPsi(templateDataLanguage),
70             templateDataLanguage,
71             settings,
72             psiFile,
73             Indent.getNoneIndent());
74         }
75       }
76       else if (element instanceof OuterLanguageElement && isOuterLanguageElement(element)) {
77         FormattingModel model =
78           createTemplateFormattingModel(psiFile, viewProvider, (OuterLanguageElement)element, settings, Indent.getNoneIndent());
79         if (model != null) return model;
80       }
81     }
82     return super.createModel(element, settings);
83   }
84
85   @Nullable
86   FormattingModel createTemplateFormattingModel(@NotNull PsiFile psiFile,
87                                                        @NotNull TemplateLanguageFileViewProvider viewProvider,
88                                                        @NotNull OuterLanguageElement outerTemplateElement,
89                                                        @NotNull CodeStyleSettings settings,
90                                                        @Nullable Indent indent) {
91     List<PsiElement> templateElements = TemplateFormatUtil.findAllTemplateLanguageElementsInside(outerTemplateElement, viewProvider);
92     return createTemplateFormattingModel(psiFile, settings, getPolicy(settings, psiFile), templateElements, indent);
93   }
94
95   @Nullable
96   public FormattingModel createTemplateFormattingModel(PsiFile file,
97                                                        CodeStyleSettings settings,
98                                                        XmlFormattingPolicy xmlFormattingPolicy,
99                                                        List<PsiElement> elements,
100                                                        Indent indent) {
101     if (elements.size() == 0) return null;
102     List<Block> templateBlocks = new ArrayList<>();
103     for (PsiElement element : elements) {
104       if (!isMarkupLanguageElement(element) && !FormatterUtil.containsWhiteSpacesOnly(element.getNode())) {
105         templateBlocks.add(createTemplateLanguageBlock(element.getNode(), settings, xmlFormattingPolicy, indent, null, null));
106       }
107     }
108     if (templateBlocks.size() == 0) return null;
109     Block topBlock = templateBlocks.size() == 1 ? templateBlocks.get(0) : new CompositeTemplateBlock(templateBlocks);
110     return new DocumentBasedFormattingModel(topBlock, file.getProject(), settings, file.getFileType(), file);
111   }
112
113   /**
114    * Checks if the file is a template file (typically a template view provider contains two roots: one for template language and one for
115    * HTML). When creating a model for an element, the builder needs to know to which of the roots the element belongs. In most cases
116    * a simple 'instanceof' check should be sufficient.
117    *
118    * @param file The file to check
119    * @return True if the file is a template file.
120    */
121   protected abstract boolean isTemplateFile(PsiFile file);
122
123   /**
124    * Checks if the element is an outer language element inside XML/HTML. Such elements are created as placeholders for template language
125    * fragments.
126    *
127    * @param element The element to check.
128    * @return True if the element is an outer (template) language fragment in XML/HTML.
129    */
130   public abstract boolean isOuterLanguageElement(PsiElement element);
131
132   /**
133    * Checks if the element is a placeholder for XML/HTML fragment inside a template language PSI tree.
134    * @param element The element to check.
135    * @return True if the element covers a fragment of XML/HTML inside a template language.
136    */
137   public abstract boolean isMarkupLanguageElement(PsiElement element);
138
139   private FormattingModel createDataLanguageFormattingModel(PsiElement dataElement,
140                                                            Language language,
141                                                            CodeStyleSettings settings,
142                                                            PsiFile psiFile,
143                                                            @Nullable Indent indent) {
144     Block block = createDataLanguageRootBlock(dataElement, language, settings, getPolicy(settings, psiFile), psiFile, indent);
145     return new DocumentBasedFormattingModel(block, psiFile.getProject(), settings, psiFile.getFileType(), psiFile);
146   }
147
148   public Block createDataLanguageRootBlock(PsiElement dataElement,
149                                             Language language,
150                                             CodeStyleSettings settings,
151                                             XmlFormattingPolicy xmlFormattingPolicy,
152                                             PsiFile psiFile,
153                                             Indent indent) {
154     Block block;
155     if (dataElement instanceof XmlTag) {
156       block = createXmlTagBlock(dataElement.getNode(), null, null, xmlFormattingPolicy, indent);
157     }
158     else {
159       if (language.isKindOf(XMLLanguage.INSTANCE)) {
160         block =
161           createXmlBlock(dataElement.getNode(), null, Alignment.createAlignment(), xmlFormattingPolicy,
162                          indent,
163                          dataElement.getTextRange());
164       }
165       else {
166         final FormattingModelBuilder builder = LanguageFormatting.INSTANCE.forContext(language, dataElement);
167         if (builder != null && !isInsideXmlAttributeValue(dataElement)) {
168           FormattingModel otherLanguageModel = builder.createModel(dataElement, settings);
169           block = otherLanguageModel.getRootBlock();
170         }
171         else {
172           block = new ReadOnlyBlock(dataElement.getNode());
173         }
174       }
175     }
176     return block;
177   }
178
179   /**
180    * Creates a template language block. Although it is not strictly required, for the builder to merge blocks with XML/HTML sub-blocks,
181    * it is necessary to inherit the template language block from {@link TemplateLanguageBlock}. Actually the merge happens inside
182    * {@link TemplateLanguageBlock#buildChildren()} method.
183    *
184    * @param node                  The AST node to create the block for.
185    * @param settings              The current code style settings (note: you need to use {@link CodeStyleSettings#getCommonSettings(Language)}
186    *                              and {@link CodeStyleSettings#getCustomSettings(Class)} for template language settings.
187    * @param xmlFormattingPolicy   The current XML formatting policy.
188    * @param indent                The default indent to be used with the template block. It can be modified when XML/HTML and template
189    *                              blocks are merged.
190    * @param alignment             The template block alignment.
191    * @param wrap                  The template block wrap.
192    * @return The newly created template block.
193    */
194   protected abstract Block createTemplateLanguageBlock(ASTNode node,
195                                                        CodeStyleSettings settings,
196                                                        XmlFormattingPolicy xmlFormattingPolicy,
197                                                        Indent indent,
198                                                        @Nullable Alignment alignment,
199                                                        @Nullable Wrap wrap);
200
201   /**
202    * Creates an xml block. Override this method to create your own xml block if you want
203    * to control spacing etc. By default the method returns {@code TemplateXmlTagBlock} instance.
204    */
205   protected XmlTagBlock createXmlTagBlock(ASTNode node,
206                                           @Nullable Wrap wrap,
207                                           @Nullable Alignment alignment,
208                                           XmlFormattingPolicy policy,
209                                           @Nullable Indent indent) {
210     return new TemplateXmlTagBlock(this, node, wrap, alignment, policy, indent);
211   }
212
213   protected XmlBlock createXmlBlock(ASTNode node,
214                                     @Nullable Wrap wrap,
215                                     @Nullable Alignment alignment,
216                                     XmlFormattingPolicy policy,
217                                     @Nullable Indent indent,
218                                     @Nullable TextRange textRange) {
219     return new TemplateXmlBlock(this, node, wrap, alignment, policy, indent, textRange);
220   }
221
222   /**
223    * Creates a synthetic block containing given sub-blocks. Override this method to create your own synthetic block if you want
224    * to control spacing etc. between child blocks. By default the method returns {@code TemplateSyntheticBlock} instance.
225    *
226    * @param subBlocks   The sub-blocks which will be contained in the synthetic block.
227    * @param parent      Synthetic block's parent.
228    * @param indent      The sub-block default indent. Block merge algorithm may overwrite it if synthetic block is
229    *                    implementing {@code IndentInheritingBlock} interface.
230    * @param policy      Xml formatting policy.
231    * @param childIndent The indent to be used with child blocks.
232    * @return A newly created template synthetic block.
233    */
234   protected SyntheticBlock createSyntheticBlock(List<Block> subBlocks,
235                                                 Block parent,
236                                                 Indent indent,
237                                                 XmlFormattingPolicy policy,
238                                                 Indent childIndent) {
239     return new TemplateSyntheticBlock(subBlocks, parent, indent, policy, childIndent);
240   }
241
242   public List<Block> mergeWithTemplateBlocks(List<Block> markupBlocks,
243                                              CodeStyleSettings settings,
244                                              XmlFormattingPolicy xmlFormattingPolicy,
245                                              Indent childrenIndent) throws FragmentedTemplateException {
246     int templateLangRangeStart = Integer.MAX_VALUE;
247     int templateLangRangeEnd = -1;
248     int rangeStart = Integer.MAX_VALUE;
249     int rangeEnd = -1;
250     PsiFile templateFile = null;
251     List<Block> pureMarkupBlocks = new ArrayList<>();
252     for (Block block : markupBlocks) {
253       TextRange currRange = block.getTextRange();
254       rangeStart = Math.min(currRange.getStartOffset(), rangeStart);
255       rangeEnd = Math.max(currRange.getEndOffset(), rangeEnd);
256       boolean isMarkupBlock = true;
257       if (block instanceof AnotherLanguageBlockWrapper) {
258         AnotherLanguageBlockWrapper wrapper = (AnotherLanguageBlockWrapper)block;
259         PsiElement otherLangElement = wrapper.getNode().getPsi();
260         if (isOuterLanguageElement(otherLangElement)) {
261           isMarkupBlock = false;
262           if (templateFile == null) {
263             FileViewProvider provider = otherLangElement.getContainingFile().getViewProvider();
264             templateFile = provider.getPsi(provider.getBaseLanguage());
265           }
266           templateLangRangeStart = Math.min(currRange.getStartOffset(), templateLangRangeStart);
267           templateLangRangeEnd = Math.max(currRange.getEndOffset(), templateLangRangeEnd);
268         }
269       }
270       if (isMarkupBlock) {
271         pureMarkupBlocks.add(block);
272       }
273     }
274     if (templateLangRangeEnd > templateLangRangeStart && templateFile != null) {
275       List<Block> templateBlocks =
276         buildTemplateLanguageBlocksInside(templateFile, new TextRange(templateLangRangeStart, templateLangRangeEnd), settings,
277                                           xmlFormattingPolicy, childrenIndent);
278       if (pureMarkupBlocks.isEmpty()) {
279         return afterMerge(templateBlocks, true, settings, xmlFormattingPolicy);
280       }
281       return afterMerge(TemplateFormatUtil.mergeBlocks(pureMarkupBlocks, templateBlocks, new TextRange(rangeStart, rangeEnd)), false,
282                         settings, xmlFormattingPolicy);
283     }
284     return markupBlocks;
285   }
286
287
288   private List<Block> buildTemplateLanguageBlocksInside(@NotNull PsiFile templateFile,
289                                                         @NotNull TextRange range,
290                                                         CodeStyleSettings settings,
291                                                         XmlFormattingPolicy xmlFormattingPolicy,
292                                                         Indent childrenIndent) {
293     List<Block> templateBlocks = new ArrayList<>();
294     TemplateLanguageFileViewProvider viewProvider = (TemplateLanguageFileViewProvider)templateFile.getViewProvider();
295     List<PsiElement> templateElements = TemplateFormatUtil.findAllElementsInside(range,
296                                                                                  viewProvider,
297                                                                                  true);
298     FormattingModel localModel = createTemplateFormattingModel(templateFile, settings, xmlFormattingPolicy, templateElements, childrenIndent);
299     if (localModel != null) {
300       Block rootBlock = localModel.getRootBlock();
301       if (rootBlock instanceof CompositeTemplateBlock) {
302         templateBlocks.addAll(rootBlock.getSubBlocks());
303       }
304       else {
305         templateBlocks.add(rootBlock);
306       }
307     }
308     return templateBlocks;
309   }
310
311   /**
312    * The method is called after markup blocks are merged with template language blocks which may require some additional block
313    * rearrangement. By default returns the same block sequence.
314    *
315    * @param originalBlocks A sequence of template and markup blocks.
316    * @param templateOnly   True if originalBlocks contain only template blocks and no markup.
317    * @return Rearranged blocks.
318    */
319   protected List<Block> afterMerge(List<Block> originalBlocks,
320                                    boolean templateOnly,
321                                    CodeStyleSettings settings,
322                                    @NotNull XmlFormattingPolicy xmlFormattingPolicy) {
323     return originalBlocks;
324   }
325
326   protected static XmlFormattingPolicy getPolicy(CodeStyleSettings settings, PsiFile psiFile) {
327     final FormattingDocumentModelImpl documentModel = FormattingDocumentModelImpl.createOn(psiFile);
328     return new HtmlPolicy(settings, documentModel);
329   }
330   
331   private static boolean isInsideXmlAttributeValue(PsiElement element) {
332     XmlAttributeValue value = PsiTreeUtil.getParentOfType(element, XmlAttributeValue.class, true);
333     return value != null;
334   }
335 }