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