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