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