42f4e112375ae0715e52e3716a13b730cf4a083d
[idea/community.git] / xml / impl / src / com / intellij / codeInsight / template / zencoding / XmlZenCodingInterpreter.java
1 /*
2  * Copyright 2000-2010 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.codeInsight.template.zencoding;
17
18 import com.intellij.codeInsight.template.CustomTemplateCallback;
19 import com.intellij.codeInsight.template.impl.TemplateImpl;
20 import com.intellij.lang.ASTNode;
21 import com.intellij.openapi.application.ApplicationManager;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.editor.Document;
24 import com.intellij.openapi.fileEditor.FileDocumentManager;
25 import com.intellij.openapi.fileTypes.StdFileTypes;
26 import com.intellij.openapi.project.Project;
27 import com.intellij.openapi.util.Pair;
28 import com.intellij.openapi.util.TextRange;
29 import com.intellij.openapi.vfs.VirtualFile;
30 import com.intellij.psi.*;
31 import com.intellij.psi.util.PsiTreeUtil;
32 import com.intellij.psi.xml.XmlChildRole;
33 import com.intellij.psi.xml.XmlTag;
34 import com.intellij.psi.xml.XmlToken;
35 import com.intellij.psi.xml.XmlTokenType;
36 import com.intellij.util.containers.HashMap;
37 import com.intellij.util.containers.HashSet;
38 import com.intellij.util.containers.IntArrayList;
39 import com.intellij.xml.util.HtmlUtil;
40 import org.jetbrains.annotations.NotNull;
41 import org.jetbrains.annotations.Nullable;
42
43 import java.util.*;
44
45 /**
46  * @author Eugene.Kudelevsky
47  */
48 class XmlZenCodingInterpreter {
49   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.template.zencoding.XmlZenCodingInterpreter");
50   private static final String ATTRS = "ATTRS";
51   private static final String NUMBER_IN_ITERATION_PLACE_HOLDER = "$";
52
53   private final List<Token> myTokens;
54
55   private final CustomTemplateCallback myCallback;
56   private final String mySurroundedText;
57
58   private State myState;
59
60   private XmlZenCodingInterpreter(List<Token> tokens,
61                                   CustomTemplateCallback callback,
62                                   State initialState,
63                                   String surroundedText) {
64     myTokens = tokens;
65     myCallback = callback;
66     mySurroundedText = surroundedText;
67     myState = initialState;
68   }
69
70   private void finish() {
71     myCallback.gotoEndOffset();
72   }
73
74   private void gotoChild(Object templateBoundsKey) {
75     int startOfTemplate = myCallback.getStartOfTemplate(templateBoundsKey);
76     int endOfTemplate = myCallback.getEndOfTemplate(templateBoundsKey);
77     int offset = myCallback.getOffset();
78
79     PsiFile file = myCallback.parseCurrentText(StdFileTypes.XML);
80
81     PsiElement element = file.findElementAt(offset);
82     if (element instanceof XmlToken && ((XmlToken)element).getTokenType() == XmlTokenType.XML_END_TAG_START) {
83       return;
84     }
85
86     int newOffset = -1;
87     XmlTag tag = PsiTreeUtil.findElementOfClassAtRange(file, startOfTemplate, endOfTemplate, XmlTag.class);
88     if (tag != null) {
89       for (PsiElement child : tag.getChildren()) {
90         if (child instanceof XmlToken && ((XmlToken)child).getTokenType() == XmlTokenType.XML_END_TAG_START) {
91           newOffset = child.getTextOffset();
92         }
93       }
94     }
95
96     if (newOffset >= 0) {
97       myCallback.fixEndOffset();
98       myCallback.moveToOffset(newOffset);
99     }
100   }
101
102   // returns if expanding finished
103
104   public static void interpret(List<Token> tokens,
105                                int startIndex,
106                                CustomTemplateCallback callback,
107                                State initialState,
108                                String surroundedText) {
109     XmlZenCodingInterpreter interpreter =
110       new XmlZenCodingInterpreter(tokens, callback, initialState, surroundedText);
111     interpreter.invoke(startIndex);
112   }
113
114   private void invoke(int startIndex) {
115     final int n = myTokens.size();
116     TemplateToken templateToken = null;
117     int number = -1;
118     for (int i = startIndex; i < n; i++) {
119       Token token = myTokens.get(i);
120       switch (myState) {
121         case OPERATION:
122           if (templateToken != null) {
123             if (token instanceof MarkerToken || token instanceof OperationToken) {
124               final char sign = token instanceof OperationToken ? ((OperationToken)token).mySign : XmlZenCodingTemplate.MARKER;
125               if (sign == '+' || (mySurroundedText == null && sign == XmlZenCodingTemplate.MARKER)) {
126                 final Object key = new Object();
127                 myCallback.fixStartOfTemplate(key);
128                 invokeTemplate(templateToken, myCallback, 0);
129                 myState = State.WORD;
130                 if (myCallback.getOffset() != myCallback.getEndOfTemplate(key)) {
131                   myCallback.fixEndOffset();
132                 }
133                 if (sign == '+') {
134                   myCallback.gotoEndOfTemplate(key);
135                 }
136                 templateToken = null;
137               }
138               else if (sign == '>' || (mySurroundedText != null && sign == XmlZenCodingTemplate.MARKER)) {
139                 startTemplateAndGotoChild(templateToken);
140                 templateToken = null;
141               }
142               else if (sign == '*') {
143                 myState = State.NUMBER;
144               }
145             }
146             else {
147               fail();
148             }
149           }
150           break;
151         case WORD:
152           if (token instanceof TemplateToken) {
153             templateToken = ((TemplateToken)token);
154             myState = State.OPERATION;
155           }
156           else {
157             fail();
158           }
159           break;
160         case NUMBER:
161           if (token instanceof NumberToken) {
162             number = ((NumberToken)token).myNumber;
163             myState = State.AFTER_NUMBER;
164           }
165           else {
166             fail();
167           }
168           break;
169         case AFTER_NUMBER:
170           if (token instanceof MarkerToken || token instanceof OperationToken) {
171             char sign = token instanceof OperationToken ? ((OperationToken)token).mySign : XmlZenCodingTemplate.MARKER;
172             if (sign == '+' || (mySurroundedText == null && sign == XmlZenCodingTemplate.MARKER)) {
173               invokeTemplateSeveralTimes(templateToken, 0, number);
174               templateToken = null;
175             }
176             else if (number > 1) {
177               invokeTemplateAndProcessTail(templateToken, 0, number, i + 1);
178               return;
179             }
180             else {
181               assert number == 1;
182               startTemplateAndGotoChild(templateToken);
183               templateToken = null;
184             }
185             myState = State.WORD;
186           }
187           else {
188             fail();
189           }
190           break;
191       }
192     }
193     if (mySurroundedText != null) {
194       insertText(myCallback, mySurroundedText);
195     }
196     finish();
197   }
198
199   private void startTemplateAndGotoChild(TemplateToken templateToken) {
200     final Object key = new Object();
201     myCallback.fixStartOfTemplate(key);
202     invokeTemplate(templateToken, myCallback, 0);
203     myState = State.WORD;
204     gotoChild(key);
205   }
206
207   private void invokeTemplateSeveralTimes(final TemplateToken templateToken,
208                                           final int startIndex,
209                                           final int count) {
210     final Object key = new Object();
211     myCallback.fixStartOfTemplate(key);
212     for (int i = startIndex; i < count; i++) {
213       invokeTemplate(templateToken, myCallback, i);
214       myState = State.WORD;
215       if (myCallback.getOffset() != myCallback.getEndOfTemplate(key)) {
216         myCallback.fixEndOffset();
217       }
218       myCallback.gotoEndOfTemplate(key);
219     }
220   }
221
222   private static void insertText(CustomTemplateCallback callback, String text) {
223     int offset = callback.getOffset();
224     callback.insertString(offset, text);
225   }
226
227   private void invokeTemplateAndProcessTail(final TemplateToken templateToken,
228                                             final int startIndex,
229                                             final int count,
230                                             final int tailStart) {
231     final Object key = new Object();
232     myCallback.fixStartOfTemplate(key);
233     for (int i = startIndex; i < count; i++) {
234       invokeTemplate(templateToken, myCallback, i);
235       gotoChild(key);
236       interpret(myTokens, tailStart, myCallback, State.WORD, mySurroundedText);
237       if (myCallback.getOffset() != myCallback.getEndOfTemplate(key)) {
238         myCallback.fixEndOffset();
239       }
240       myCallback.gotoEndOfTemplate(key);
241     }
242     finish();
243   }
244
245   private static boolean containsAttrsVar(TemplateImpl template) {
246     for (int i = 0; i < template.getVariableCount(); i++) {
247       String varName = template.getVariableNameAt(i);
248       if (ATTRS.equals(varName)) {
249         return true;
250       }
251     }
252     return false;
253   }
254
255   private static void removeVariablesWhichHasNoSegment(TemplateImpl template) {
256     Set<String> segments = new HashSet<String>();
257     for (int i = 0; i < template.getSegmentsCount(); i++) {
258       segments.add(template.getSegmentName(i));
259     }
260     IntArrayList varsToRemove = new IntArrayList();
261     for (int i = 0; i < template.getVariableCount(); i++) {
262       String varName = template.getVariableNameAt(i);
263       if (!segments.contains(varName)) {
264         varsToRemove.add(i);
265       }
266     }
267     for (int i = 0; i < varsToRemove.size(); i++) {
268       template.removeVariable(varsToRemove.get(i));
269     }
270   }
271
272   @Nullable
273   private static Map<String, String> buildPredefinedValues(List<Pair<String, String>> attribute2value, int numberInIteration) {
274     StringBuilder result = new StringBuilder();
275     for (Iterator<Pair<String, String>> it = attribute2value.iterator(); it.hasNext();) {
276       Pair<String, String> pair = it.next();
277       String name = pair.first;
278       String value = getValue(pair, numberInIteration);
279       result.append(name).append("=\"").append(value).append('"');
280       if (it.hasNext()) {
281         result.append(' ');
282       }
283     }
284     String attributes = result.toString();
285     attributes = attributes.length() > 0 ? ' ' + attributes : null;
286     Map<String, String> predefinedValues = null;
287     if (attributes != null) {
288       predefinedValues = new HashMap<String, String>();
289       predefinedValues.put(ATTRS, attributes);
290     }
291     return predefinedValues;
292   }
293
294   private static String getValue(Pair<String, String> pair, int numberInIteration) {
295     return pair.second.replace(NUMBER_IN_ITERATION_PLACE_HOLDER, Integer.toString(numberInIteration + 1));
296   }
297
298   @Nullable
299   private static String addAttrsVar(TemplateImpl modifiedTemplate, XmlTag tag) {
300     String text = tag.getContainingFile().getText();
301     PsiElement[] children = tag.getChildren();
302     if (children.length >= 1 &&
303         children[0] instanceof XmlToken &&
304         ((XmlToken)children[0]).getTokenType() == XmlTokenType.XML_START_TAG_START) {
305       PsiElement beforeAttrs = children[0];
306       if (children.length >= 2 && children[1] instanceof XmlToken && ((XmlToken)children[1]).getTokenType() == XmlTokenType.XML_NAME) {
307         beforeAttrs = children[1];
308       }
309       TextRange range = beforeAttrs.getTextRange();
310       if (range == null) {
311         return null;
312       }
313       int offset = range.getEndOffset();
314       text = text.substring(0, offset) + " $ATTRS$" + text.substring(offset);
315       modifiedTemplate.addVariable(ATTRS, "", "", false);
316       return text;
317     }
318     return null;
319   }
320
321   private static void invokeTemplate(TemplateToken token,
322                                      final CustomTemplateCallback callback,
323                                      int numberInIteration) {
324     List<Pair<String, String>> attr2value = new ArrayList<Pair<String, String>>(token.myAttribute2Value);
325     if (callback.isLiveTemplateApplicable(token.myKey)) {
326       invokeExistingLiveTemplate(token, callback, numberInIteration, attr2value);
327     }
328     else {
329       TemplateImpl template = new TemplateImpl("", "");
330       template.addTextSegment('<' + token.myKey);
331       if (attr2value.size() > 0) {
332         template.addVariable(ATTRS, "", "", false);
333         template.addVariableSegment(ATTRS);
334       }
335       template.addTextSegment(">");
336       if (XmlZenCodingTemplate.isTrueXml(callback) || !HtmlUtil.isSingleHtmlTag(token.myKey)) {
337         template.addVariableSegment(TemplateImpl.END);
338         template.addTextSegment("</" + token.myKey + ">");
339       }
340       template.setToReformat(true);
341       Map<String, String> predefinedValues = buildPredefinedValues(attr2value, numberInIteration);
342       callback.expandTemplate(template, predefinedValues);
343     }
344   }
345
346   private static void invokeExistingLiveTemplate(TemplateToken token,
347                                                  CustomTemplateCallback callback,
348                                                  int numberInIteration,
349                                                  List<Pair<String, String>> attr2value) {
350     if (token.myTemplate != null) {
351       if (attr2value.size() > 0 || XmlZenCodingTemplate.isTrueXml(callback)) {
352         TemplateImpl modifiedTemplate = token.myTemplate.copy();
353         XmlTag tag = XmlZenCodingTemplate.parseXmlTagInTemplate(token.myTemplate.getString(), callback, true);
354         if (tag != null) {
355           for (Iterator<Pair<String, String>> iterator = attr2value.iterator(); iterator.hasNext();) {
356             Pair<String, String> pair = iterator.next();
357             if (tag.getAttribute(pair.first) != null) {
358               tag.setAttribute(pair.first, getValue(pair, numberInIteration));
359               iterator.remove();
360             }
361           }
362           if (XmlZenCodingTemplate.isTrueXml(callback)) {
363             closeUnclosingTags(tag);
364           }
365           String text = null;
366           if (!containsAttrsVar(modifiedTemplate) && attr2value.size() > 0) {
367             String textWithAttrs = addAttrsVar(modifiedTemplate, tag);
368             if (textWithAttrs != null) {
369               text = textWithAttrs;
370             }
371             else {
372               for (Iterator<Pair<String, String>> iterator = attr2value.iterator(); iterator.hasNext();) {
373                 Pair<String, String> pair = iterator.next();
374                 tag.setAttribute(pair.first, getValue(pair, numberInIteration));
375                 iterator.remove();
376               }
377             }
378           }
379           if (text == null) {
380             text = tag.getContainingFile().getText();
381           }
382           modifiedTemplate.setString(text);
383           removeVariablesWhichHasNoSegment(modifiedTemplate);
384           Map<String, String> predefinedValues = buildPredefinedValues(attr2value, numberInIteration);
385           callback.expandTemplate(modifiedTemplate, predefinedValues);
386           return;
387         }
388       }
389       callback.expandTemplate(token.myTemplate, null);
390     }
391     else {
392       Map<String, String> predefinedValues = buildPredefinedValues(attr2value, numberInIteration);
393       callback.expandTemplate(token.myKey, predefinedValues);
394     }
395   }
396
397   private static boolean isTagClosed(@NotNull XmlTag tag) {
398     ASTNode node = tag.getNode();
399     assert node != null;
400     final ASTNode emptyTagEnd = XmlChildRole.EMPTY_TAG_END_FINDER.findChild(node);
401     final ASTNode endTagEnd = XmlChildRole.CLOSING_TAG_START_FINDER.findChild(node);
402     return emptyTagEnd != null || endTagEnd != null;
403   }
404
405   @SuppressWarnings({"ConstantConditions"})
406   private static void closeUnclosingTags(@NotNull XmlTag root) {
407     final List<SmartPsiElementPointer<XmlTag>> tagToClose = new ArrayList<SmartPsiElementPointer<XmlTag>>();
408     Project project = root.getProject();
409     final SmartPointerManager manager = SmartPointerManager.getInstance(project);
410     root.accept(new XmlRecursiveElementVisitor() {
411       @Override
412       public void visitXmlTag(final XmlTag tag) {
413         if (!isTagClosed(tag)) {
414           tagToClose.add(manager.createLazyPointer(tag));
415         }
416       }
417     });
418     for (final SmartPsiElementPointer<XmlTag> pointer : tagToClose) {
419       final XmlTag tag = pointer.getElement();
420       if (tag != null) {
421         final ASTNode child = XmlChildRole.START_TAG_END_FINDER.findChild(tag.getNode());
422         if (child != null) {
423           final int offset = child.getTextRange().getStartOffset();
424           VirtualFile file = tag.getContainingFile().getVirtualFile();
425           if (file != null) {
426             final Document document = FileDocumentManager.getInstance().getDocument(file);
427             ApplicationManager.getApplication().runWriteAction(new Runnable() {
428               public void run() {
429                 document.replaceString(offset, tag.getTextRange().getEndOffset(), "/>");
430               }
431             });
432           }
433         }
434       }
435     }
436     PsiDocumentManager.getInstance(project).commitAllDocuments();
437   }
438
439   private static void fail() {
440     LOG.error("Input string was checked incorrectly during isApplicable() invokation");
441   }
442 }