fix compilation
[idea/community.git] / xml / impl / src / com / intellij / codeInsight / template / XmlZenCodingTemplate.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;
17
18 import com.intellij.application.options.editor.WebEditorOptions;
19 import com.intellij.codeInsight.template.impl.TemplateImpl;
20 import com.intellij.lang.xml.XMLLanguage;
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.psi.PsiElement;
25 import com.intellij.psi.PsiFile;
26 import com.intellij.psi.util.PsiTreeUtil;
27 import com.intellij.psi.xml.XmlTag;
28 import com.intellij.psi.xml.XmlToken;
29 import com.intellij.psi.xml.XmlTokenType;
30 import com.intellij.util.containers.HashMap;
31 import com.intellij.util.containers.HashSet;
32 import org.jetbrains.annotations.NotNull;
33 import org.jetbrains.annotations.Nullable;
34
35 import java.util.*;
36
37 /**
38  * @author Eugene.Kudelevsky
39  */
40 public class XmlZenCodingTemplate implements CustomLiveTemplate {
41   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.template.XmlZenCodingTemplate");
42
43   private static final String ATTRS = "ATTRS";
44
45   private static final String OPERATIONS = ">+*";
46   private static final String SELECTORS = ".#[";
47   private static final char MARKER = '$';
48   private static final String ID = "id";
49   private static final String CLASS = "class";
50   private static final String NUMBER_IN_ITERATION_PLACE_HOLDER = "$";
51
52   private static enum MyState {
53     OPERATION, WORD, AFTER_NUMBER, NUMBER
54   }
55
56   private static class MyToken {
57   }
58
59   private static class MyMarkerToken extends MyToken {
60   }
61
62   private static class MyTemplateToken extends MyToken {
63     final String myKey;
64     final List<Pair<String, String>> myAttribute2Value;
65
66     MyTemplateToken(String key, List<Pair<String, String>> attribute2value) {
67       myKey = key;
68       myAttribute2Value = attribute2value;
69     }
70   }
71
72   private static class MyNumberToken extends MyToken {
73     final int myNumber;
74
75     MyNumberToken(int number) {
76       myNumber = number;
77     }
78   }
79
80   private static class MyOperationToken extends MyToken {
81     final char mySign;
82
83     MyOperationToken(char sign) {
84       mySign = sign;
85     }
86   }
87
88   private static int parseNonNegativeInt(@NotNull String s) {
89     try {
90       return Integer.parseInt(s);
91     }
92     catch (Throwable ignored) {
93     }
94     return -1;
95   }
96
97   private static String getPrefix(@NotNull String templateKey) {
98     for (int i = 0, n = templateKey.length(); i < n; i++) {
99       char c = templateKey.charAt(i);
100       if (SELECTORS.indexOf(c) >= 0) {
101         return templateKey.substring(0, i);
102       }
103     }
104     return templateKey;
105   }
106
107   @Nullable
108   private static Pair<String, String> parseAttrNameAndValue(@NotNull String text) {
109     int eqIndex = text.indexOf('=');
110     if (eqIndex > 0) {
111       return new Pair<String, String>(text.substring(0, eqIndex), text.substring(eqIndex + 1));
112     }
113     return null;
114   }
115
116   @Nullable
117   private static MyTemplateToken parseSelectors(@NotNull String text) {
118     String templateKey = null;
119     List<Pair<String, String>> attributes = new ArrayList<Pair<String, String>>();
120     Set<String> definedAttrs = new HashSet<String>();
121     final List<String> classes = new ArrayList<String>();
122     StringBuilder builder = new StringBuilder();
123     char lastDelim = 0;
124     text += MARKER;
125     int classAttrPosition = -1;
126     for (int i = 0, n = text.length(); i < n; i++) {
127       char c = text.charAt(i);
128       if (c == '#' || c == '.' || c == '[' || c == ']' || i == n - 1) {
129         if (c != ']') {
130           switch (lastDelim) {
131             case 0:
132               templateKey = builder.toString();
133               break;
134             case '#':
135               if (!definedAttrs.add(ID)) {
136                 return null;
137               }
138               attributes.add(new Pair<String, String>(ID, builder.toString()));
139               break;
140             case '.':
141               if (builder.length() <= 0) {
142                 return null;
143               }
144               if (classAttrPosition < 0) {
145                 classAttrPosition = attributes.size();
146               }
147               classes.add(builder.toString());
148               break;
149             case ']':
150               if (builder.length() > 0) {
151                 return null;
152               }
153               break;
154             default:
155               return null;
156           }
157         }
158         else if (lastDelim != '[') {
159           return null;
160         }
161         else {
162           Pair<String, String> pair = parseAttrNameAndValue(builder.toString());
163           if (pair == null || !definedAttrs.add(pair.first)) {
164             return null;
165           }
166           attributes.add(pair);
167         }
168         lastDelim = c;
169         builder = new StringBuilder();
170       }
171       else {
172         builder.append(c);
173       }
174     }
175     if (classes.size() > 0) {
176       if (definedAttrs.contains(CLASS)) {
177         return null;
178       }
179       StringBuilder classesAttrValue = new StringBuilder();
180       for (int i = 0; i < classes.size(); i++) {
181         classesAttrValue.append(classes.get(i));
182         if (i < classes.size() - 1) {
183           classesAttrValue.append(' ');
184         }
185       }
186       assert classAttrPosition >= 0;
187       attributes.add(classAttrPosition, new Pair<String, String>(CLASS, classesAttrValue.toString()));
188     }
189     return new MyTemplateToken(templateKey, attributes);
190   }
191
192   @Nullable
193   private static List<MyToken> parse(@NotNull String text, @NotNull CustomTemplateCallback callback) {
194     text += MARKER;
195     StringBuilder templateKeyBuilder = new StringBuilder();
196     List<MyToken> result = new ArrayList<MyToken>();
197     for (int i = 0, n = text.length(); i < n; i++) {
198       char c = text.charAt(i);
199       if (i == n - 1 || (i < n - 2 && OPERATIONS.indexOf(c) >= 0)) {
200         String key = templateKeyBuilder.toString();
201         templateKeyBuilder = new StringBuilder();
202         int num = parseNonNegativeInt(key);
203         if (num > 0) {
204           result.add(new MyNumberToken(num));
205         }
206         else {
207           if (key.length() == 0) {
208             return null;
209           }
210           String prefix = getPrefix(key);
211           if (!callback.isLiveTemplateApplicable(prefix) && prefix.indexOf('<') >= 0 /*&& !XML11Char.isXML11ValidQName(prefix)*/) {
212             return null;
213           }
214           MyTemplateToken token = parseSelectors(key);
215           if (token == null) {
216             return null;
217           }
218           result.add(token);
219         }
220         result.add(i < n - 1 ? new MyOperationToken(c) : new MyMarkerToken());
221       }
222       else if (!Character.isWhitespace(c)) {
223         templateKeyBuilder.append(c);
224       }
225       else {
226         return null;
227       }
228     }
229     return result;
230   }
231
232   private static boolean check(@NotNull Collection<MyToken> tokens) {
233     MyState state = MyState.WORD;
234     for (MyToken token : tokens) {
235       if (token instanceof MyMarkerToken) {
236         break;
237       }
238       switch (state) {
239         case OPERATION:
240           if (token instanceof MyOperationToken) {
241             state = ((MyOperationToken)token).mySign == '*' ? MyState.NUMBER : MyState.WORD;
242           }
243           else {
244             return false;
245           }
246           break;
247         case WORD:
248           if (token instanceof MyTemplateToken) {
249             state = MyState.OPERATION;
250           }
251           else {
252             return false;
253           }
254           break;
255         case NUMBER:
256           if (token instanceof MyNumberToken) {
257             state = MyState.AFTER_NUMBER;
258           }
259           else {
260             return false;
261           }
262           break;
263         case AFTER_NUMBER:
264           if (token instanceof MyOperationToken && ((MyOperationToken)token).mySign != '*') {
265             state = MyState.WORD;
266           }
267           else {
268             return false;
269           }
270           break;
271       }
272     }
273     return state == MyState.OPERATION || state == MyState.AFTER_NUMBER;
274   }
275
276   private static String computeKey(Editor editor, int startOffset) {
277     int offset = editor.getCaretModel().getOffset();
278     String s = editor.getDocument().getCharsSequence().subSequence(startOffset, offset).toString();
279     int index = 0;
280     while (index < s.length() && Character.isWhitespace(s.charAt(index))) {
281       index++;
282     }
283     String key = s.substring(index);
284     int lastWhitespaceIndex = -1;
285     for (int i = 0; i < key.length(); i++) {
286       if (Character.isWhitespace(key.charAt(i))) {
287         lastWhitespaceIndex = i;
288       }
289     }
290     if (lastWhitespaceIndex >= 0 && lastWhitespaceIndex < key.length() - 1) {
291       return key.substring(lastWhitespaceIndex + 1);
292     }
293     return key;
294   }
295
296   public String computeTemplateKey(@NotNull CustomTemplateCallback callback) {
297     Editor editor = callback.getEditor();
298     int offset = callback.getOffset();
299     PsiElement element = callback.getFile().findElementAt(offset > 0 ? offset - 1 : offset);
300     int line = editor.getCaretModel().getLogicalPosition().line;
301     int lineStart = editor.getDocument().getLineStartOffset(line);
302     int parentStart;
303     do {
304       parentStart = element != null ? element.getTextRange().getStartOffset() : 0;
305       int startOffset = parentStart > lineStart ? parentStart : lineStart;
306       String key = computeKey(editor, startOffset);
307       List<MyToken> tokens = parse(key, callback);
308       if (tokens != null && check(tokens)) {
309         if (tokens.size() == 2) {
310           MyToken token = tokens.get(0);
311           if (token instanceof MyTemplateToken) {
312             if (key.equals(((MyTemplateToken)token).myKey) && callback.isLiveTemplateApplicable(key)) {
313               // do not activate only live template
314               return null;
315             }
316           }
317         }
318         return key;
319       }
320       if (element != null) {
321         element = element.getParent();
322       }
323     }
324     while (element != null && parentStart > lineStart);
325     return null;
326   }
327
328   public boolean isApplicable(PsiFile file, int offset, boolean selection) {
329     WebEditorOptions webEditorOptions = WebEditorOptions.getInstance();
330     if (!webEditorOptions.isZenCodingEnabled()) {
331       return false;
332     }
333     if (file.getLanguage() instanceof XMLLanguage) {
334       PsiElement element = file.findElementAt(offset > 0 ? offset - 1 : offset);
335       if (element == null || element.getLanguage() instanceof XMLLanguage) {
336         return true;
337       }
338     }
339     return false;
340   }
341
342   public void expand(String key, @NotNull CustomTemplateCallback callback, @Nullable TemplateInvokationListener listener) {
343     List<MyToken> tokens = parse(key, callback);
344     assert tokens != null;
345     MyInterpreter interpreter = new MyInterpreter(tokens, callback, MyState.WORD, listener);
346     interpreter.invoke(0);
347   }
348
349   public void wrap(String selection, @NotNull CustomTemplateCallback callback, @Nullable TemplateInvokationListener listener) {
350   }
351
352   private static void fail() {
353     LOG.error("Input string was checked incorrectly during isApplicable() invokation");
354   }
355
356   @NotNull
357   private static String buildAttributesString(List<Pair<String, String>> attribute2value, int numberInIteration) {
358     StringBuilder result = new StringBuilder();
359     for (Iterator<Pair<String, String>> it = attribute2value.iterator(); it.hasNext();) {
360       Pair<String, String> pair = it.next();
361       String name = pair.first;
362       String value = pair.second.replace(NUMBER_IN_ITERATION_PLACE_HOLDER, Integer.toString(numberInIteration + 1));
363       result.append(name).append("=\"").append(value).append('"');
364       if (it.hasNext()) {
365         result.append(' ');
366       }
367     }
368     return result.toString();
369   }
370
371   private static boolean invokeTemplate(MyTemplateToken token,
372                                         final CustomTemplateCallback callback,
373                                         final TemplateInvokationListener listener,
374                                         int numberInIteration) {
375     String attributes = buildAttributesString(token.myAttribute2Value, numberInIteration);
376     attributes = attributes.length() > 0 ? ' ' + attributes : null;
377     Map<String, String> predefinedValues = null;
378     if (attributes != null) {
379       predefinedValues = new HashMap<String, String>();
380       predefinedValues.put(ATTRS, attributes);
381     }
382     if (callback.isLiveTemplateApplicable(token.myKey)) {
383       if (attributes != null && !callback.templateContainsVars(token.myKey, ATTRS)) {
384         TemplateImpl newTemplate = generateTemplateWithAttributes(token.myKey, attributes, callback);
385         if (newTemplate != null) {
386           return callback.startTemplate(newTemplate, predefinedValues, listener);
387         }
388       }
389       return callback.startTemplate(token.myKey, predefinedValues, listener);
390     }
391     else {
392       TemplateImpl template = new TemplateImpl("", "");
393       template.addTextSegment('<' + token.myKey);
394       if (attributes != null) {
395         template.addVariable(ATTRS, "", "", false);
396         template.addVariableSegment(ATTRS);
397       }
398       template.addTextSegment(">");
399       template.addVariableSegment(TemplateImpl.END);
400       template.addTextSegment("</" + token.myKey + ">");
401       template.setToReformat(true);
402       return callback.startTemplate(template, predefinedValues, listener);
403     }
404   }
405
406   private static int findPlaceToInsertAttrs(@NotNull TemplateImpl template) {
407     String s = template.getString();
408     if (s.length() > 0) {
409       if (s.charAt(0) != '<') {
410         return -1;
411       }
412       int i = 1;
413       while (i < s.length() && !Character.isWhitespace(s.charAt(i)) && s.charAt(i) != '>') {
414         i++;
415       }
416       if (i == 1) {
417         return -1;
418       }
419       if (s.indexOf('>', i) >= i) {
420         return i;
421       }
422     }
423     return -1;
424   }
425
426   @Nullable
427   private static TemplateImpl generateTemplateWithAttributes(String key, String attributes, CustomTemplateCallback callback) {
428     TemplateImpl template = callback.findApplicableTemplate(key);
429     assert template != null;
430     String templateString = template.getString();
431     int offset = findPlaceToInsertAttrs(template);
432     if (offset >= 0) {
433       String newTemplateString = templateString.substring(0, offset) + attributes + templateString.substring(offset);
434       TemplateImpl newTemplate = template.copy();
435       newTemplate.setString(newTemplateString);
436       return newTemplate;
437     }
438     return null;
439   }
440
441   /*private static boolean hasClosingTag(CharSequence text, CharSequence tagName, int offset, int rightBound) {
442     if (offset + 1 < text.length() && text.charAt(offset) == '<' && text.charAt(offset + 1) == '/') {
443       CharSequence closingTagName = parseTagName(text, offset + 2, rightBound);
444       if (tagName.equals(closingTagName)) {
445         return true;
446       }
447     }
448     return false;
449   }*/
450
451   private class MyInterpreter {
452     private final List<MyToken> myTokens;
453     private final CustomTemplateCallback myCallback;
454     private final TemplateInvokationListener myListener;
455     private MyState myState;
456
457     private MyInterpreter(List<MyToken> tokens,
458                           CustomTemplateCallback callback,
459                           MyState initialState,
460                           TemplateInvokationListener listener) {
461       myTokens = tokens;
462       myCallback = callback;
463       myListener = listener;
464       myState = initialState;
465     }
466
467     private void finish(boolean inSeparateEvent) {
468       myCallback.gotoEndOffset();
469       if (myListener != null) {
470         myListener.finished(inSeparateEvent);
471       }
472     }
473
474     private void gotoChild(Object templateBoundsKey) {
475       int startOfTemplate = myCallback.getStartOfTemplate(templateBoundsKey);
476       int endOfTemplate = myCallback.getEndOfTemplate(templateBoundsKey);
477       Editor editor = myCallback.getEditor();
478       int offset = myCallback.getOffset();
479
480       PsiFile file = myCallback.getFile();
481
482       PsiElement element = file.findElementAt(offset);
483       if (element instanceof XmlToken && ((XmlToken)element).getTokenType() == XmlTokenType.XML_END_TAG_START) {
484         return;
485       }
486
487       int newOffset = -1;
488       XmlTag tag = PsiTreeUtil.findElementOfClassAtRange(file, startOfTemplate, endOfTemplate, XmlTag.class);
489       if (tag != null) {
490         for (PsiElement child : tag.getChildren()) {
491           if (child instanceof XmlToken && ((XmlToken)child).getTokenType() == XmlTokenType.XML_END_TAG_START) {
492             newOffset = child.getTextOffset();
493           }
494         }
495       }
496
497       if (newOffset >= 0) {
498         myCallback.fixEndOffset();
499         editor.getCaretModel().moveToOffset(newOffset);
500       }
501
502       /*CharSequence tagName = getPrecedingTagName(text, offset, startOfTemplate);
503       if (tagName != null) {
504         *//*if (!hasClosingTag(text, tagName, offset, endOfTemplate)) {
505           document.insertString(offset, "</" + tagName + '>');
506         }*//*
507       }
508       else if (offset != endOfTemplate) {
509         tagName = getPrecedingTagName(text, endOfTemplate, startOfTemplate);
510         if (tagName != null) {
511           *//*fixEndOffset();
512           document.insertString(endOfTemplate, "</" + tagName + '>');*//*
513           editor.getCaretModel().moveToOffset(endOfTemplate);
514         }
515       }*/
516     }
517
518     public boolean invoke(int startIndex) {
519       final int n = myTokens.size();
520       MyTemplateToken templateToken = null;
521       int number = -1;
522       for (int i = startIndex; i < n; i++) {
523         final int finalI = i;
524         MyToken token = myTokens.get(i);
525         switch (myState) {
526           case OPERATION:
527             if (templateToken != null) {
528               if (token instanceof MyMarkerToken || token instanceof MyOperationToken) {
529                 final char sign = token instanceof MyOperationToken ? ((MyOperationToken)token).mySign : MARKER;
530                 if (sign == MARKER || sign == '+') {
531                   final Object key = new Object();
532                   myCallback.fixStartOfTemplate(key);
533                   TemplateInvokationListener listener = new TemplateInvokationListener() {
534                     public void finished(boolean inSeparateEvent) {
535                       myState = MyState.WORD;
536                       if (myCallback.getOffset() != myCallback.getEndOfTemplate(key)) {
537                         myCallback.fixEndOffset();
538                       }
539                       if (sign == '+') {
540                         myCallback.gotoEndOfTemplate(key);
541                       }
542                       if (inSeparateEvent) {
543                         invoke(finalI + 1);
544                       }
545                     }
546                   };
547                   if (!invokeTemplate(templateToken, myCallback, listener, -1)) {
548                     return false;
549                   }
550                   templateToken = null;
551                 }
552                 else if (sign == '>') {
553                   if (!startTemplateAndGotoChild(templateToken, finalI)) {
554                     return false;
555                   }
556                   templateToken = null;
557                 }
558                 else if (sign == '*') {
559                   myState = MyState.NUMBER;
560                 }
561               }
562               else {
563                 fail();
564               }
565             }
566             break;
567           case WORD:
568             if (token instanceof MyTemplateToken) {
569               templateToken = ((MyTemplateToken)token);
570               myState = MyState.OPERATION;
571             }
572             else {
573               fail();
574             }
575             break;
576           case NUMBER:
577             if (token instanceof MyNumberToken) {
578               number = ((MyNumberToken)token).myNumber;
579               myState = MyState.AFTER_NUMBER;
580             }
581             else {
582               fail();
583             }
584             break;
585           case AFTER_NUMBER:
586             if (token instanceof MyMarkerToken || token instanceof MyOperationToken) {
587               char sign = token instanceof MyOperationToken ? ((MyOperationToken)token).mySign : MARKER;
588               if (sign == MARKER || sign == '+') {
589                 if (!invokeTemplateSeveralTimes(templateToken, 0, number, finalI)) {
590                   return false;
591                 }
592                 templateToken = null;
593               }
594               else if (number > 1) {
595                 return invokeTemplateAndProcessTail(templateToken, 0, number, i + 1);
596               }
597               else {
598                 assert number == 1;
599                 if (!startTemplateAndGotoChild(templateToken, finalI)) {
600                   return false;
601                 }
602                 templateToken = null;
603               }
604               myState = MyState.WORD;
605             }
606             else {
607               fail();
608             }
609             break;
610         }
611       }
612       finish(startIndex == n);
613       return true;
614     }
615
616     private boolean startTemplateAndGotoChild(MyTemplateToken templateToken, final int index) {
617       final Object key = new Object();
618       myCallback.fixStartOfTemplate(key);
619       TemplateInvokationListener listener = new TemplateInvokationListener() {
620         public void finished(boolean inSeparateEvent) {
621           myState = MyState.WORD;
622           gotoChild(key);
623           if (inSeparateEvent) {
624             invoke(index + 1);
625           }
626         }
627       };
628       if (!invokeTemplate(templateToken, myCallback, listener, -1)) {
629         return false;
630       }
631       return true;
632     }
633
634     private boolean invokeTemplateSeveralTimes(final MyTemplateToken templateToken,
635                                                final int startIndex,
636                                                final int count,
637                                                final int globalIndex) {
638       final Object key = new Object();
639       myCallback.fixStartOfTemplate(key);
640       for (int i = startIndex; i < count; i++) {
641         final int finalI = i;
642         TemplateInvokationListener listener = new TemplateInvokationListener() {
643           public void finished(boolean inSeparateEvent) {
644             myState = MyState.WORD;
645             if (myCallback.getOffset() != myCallback.getEndOfTemplate(key)) {
646               myCallback.fixEndOffset();
647             }
648             myCallback.gotoEndOfTemplate(key);
649             if (inSeparateEvent) {
650               if (finalI + 1 < count) {
651                 invokeTemplateSeveralTimes(templateToken, finalI + 1, count, globalIndex);
652               }
653               else {
654                 invoke(globalIndex + 1);
655               }
656             }
657           }
658         };
659         if (!invokeTemplate(templateToken, myCallback, listener, i)) {
660           return false;
661         }
662       }
663       return true;
664     }
665
666     private boolean invokeTemplateAndProcessTail(final MyTemplateToken templateToken,
667                                                  final int startIndex,
668                                                  final int count,
669                                                  final int tailStart) {
670       final Object key = new Object();
671       myCallback.fixStartOfTemplate(key);
672       for (int i = startIndex; i < count; i++) {
673         final int finalI = i;
674         final boolean[] flag = new boolean[]{false};
675         TemplateInvokationListener listener = new TemplateInvokationListener() {
676           public void finished(boolean inSeparateEvent) {
677             gotoChild(key);
678             MyInterpreter interpreter = new MyInterpreter(myTokens, myCallback, MyState.WORD, new TemplateInvokationListener() {
679               public void finished(boolean inSeparateEvent) {
680                 if (myCallback.getOffset() != myCallback.getEndOfTemplate(key)) {
681                   myCallback.fixEndOffset();
682                 }
683                 myCallback.gotoEndOfTemplate(key);
684                 if (inSeparateEvent) {
685                   invokeTemplateAndProcessTail(templateToken, finalI + 1, count, tailStart);
686                 }
687               }
688             });
689             if (interpreter.invoke(tailStart)) {
690               if (inSeparateEvent) {
691                 invokeTemplateAndProcessTail(templateToken, finalI + 1, count, tailStart);
692               }
693             }
694             else {
695               flag[0] = true;
696             }
697           }
698         };
699         if (!invokeTemplate(templateToken, myCallback, listener, i) || flag[0]) {
700           return false;
701         }
702       }
703       finish(count == 0);
704       return true;
705     }
706   }
707 }