invoke even only live template
[idea/community.git] / xml / impl / src / com / intellij / codeInsight / template / zencoding / 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.zencoding;
17
18 import com.intellij.application.options.editor.WebEditorOptions;
19 import com.intellij.codeInsight.template.CustomTemplateCallback;
20 import com.intellij.codeInsight.template.TemplateInvokationListener;
21 import com.intellij.codeInsight.template.TemplateManager;
22 import com.intellij.codeInsight.template.impl.TemplateImpl;
23 import com.intellij.codeInsight.template.impl.TemplateSettings;
24 import com.intellij.lang.xml.XMLLanguage;
25 import com.intellij.openapi.editor.Editor;
26 import com.intellij.openapi.fileTypes.StdFileTypes;
27 import com.intellij.openapi.util.Pair;
28 import com.intellij.psi.PsiElement;
29 import com.intellij.psi.PsiFile;
30 import com.intellij.psi.PsiFileFactory;
31 import com.intellij.psi.xml.XmlDocument;
32 import com.intellij.psi.xml.XmlFile;
33 import com.intellij.psi.xml.XmlTag;
34 import com.intellij.util.LocalTimeCounter;
35 import com.intellij.util.containers.HashSet;
36 import org.apache.xerces.util.XML11Char;
37 import org.jetbrains.annotations.NotNull;
38 import org.jetbrains.annotations.Nullable;
39
40 import java.util.ArrayList;
41 import java.util.Collection;
42 import java.util.List;
43 import java.util.Set;
44
45 /**
46  * @author Eugene.Kudelevsky
47  */
48 public class XmlZenCodingTemplate {
49   static final char MARKER = '$';
50   private static final String OPERATIONS = ">+*";
51   private static final String SELECTORS = ".#[";
52   private static final String ID = "id";
53   private static final String CLASS = "class";
54
55   private static int parseNonNegativeInt(@NotNull String s) {
56     try {
57       return Integer.parseInt(s);
58     }
59     catch (Throwable ignored) {
60     }
61     return -1;
62   }
63
64   private static String getPrefix(@NotNull String templateKey) {
65     for (int i = 0, n = templateKey.length(); i < n; i++) {
66       char c = templateKey.charAt(i);
67       if (SELECTORS.indexOf(c) >= 0) {
68         return templateKey.substring(0, i);
69       }
70     }
71     return templateKey;
72   }
73
74   @Nullable
75   private static Pair<String, String> parseAttrNameAndValue(@NotNull String text) {
76     int eqIndex = text.indexOf('=');
77     if (eqIndex > 0) {
78       return new Pair<String, String>(text.substring(0, eqIndex), text.substring(eqIndex + 1));
79     }
80     return null;
81   }
82
83   @Nullable
84   private static TemplateToken parseSelectors(@NotNull String text) {
85     String templateKey = null;
86     List<Pair<String, String>> attributes = new ArrayList<Pair<String, String>>();
87     Set<String> definedAttrs = new HashSet<String>();
88     final List<String> classes = new ArrayList<String>();
89     StringBuilder builder = new StringBuilder();
90     char lastDelim = 0;
91     text += MARKER;
92     int classAttrPosition = -1;
93     for (int i = 0, n = text.length(); i < n; i++) {
94       char c = text.charAt(i);
95       if (c == '#' || c == '.' || c == '[' || c == ']' || i == n - 1) {
96         if (c != ']') {
97           switch (lastDelim) {
98             case 0:
99               templateKey = builder.toString();
100               break;
101             case '#':
102               if (!definedAttrs.add(ID)) {
103                 return null;
104               }
105               attributes.add(new Pair<String, String>(ID, builder.toString()));
106               break;
107             case '.':
108               if (builder.length() <= 0) {
109                 return null;
110               }
111               if (classAttrPosition < 0) {
112                 classAttrPosition = attributes.size();
113               }
114               classes.add(builder.toString());
115               break;
116             case ']':
117               if (builder.length() > 0) {
118                 return null;
119               }
120               break;
121             default:
122               return null;
123           }
124         }
125         else if (lastDelim != '[') {
126           return null;
127         }
128         else {
129           Pair<String, String> pair = parseAttrNameAndValue(builder.toString());
130           if (pair == null || !definedAttrs.add(pair.first)) {
131             return null;
132           }
133           attributes.add(pair);
134         }
135         lastDelim = c;
136         builder = new StringBuilder();
137       }
138       else {
139         builder.append(c);
140       }
141     }
142     if (classes.size() > 0) {
143       if (definedAttrs.contains(CLASS)) {
144         return null;
145       }
146       StringBuilder classesAttrValue = new StringBuilder();
147       for (int i = 0; i < classes.size(); i++) {
148         classesAttrValue.append(classes.get(i));
149         if (i < classes.size() - 1) {
150           classesAttrValue.append(' ');
151         }
152       }
153       assert classAttrPosition >= 0;
154       attributes.add(classAttrPosition, new Pair<String, String>(CLASS, classesAttrValue.toString()));
155     }
156     return new TemplateToken(templateKey, attributes);
157   }
158
159   private static boolean isXML11ValidQName(String str) {
160     final int colon = str.indexOf(':');
161     if (colon == 0 || colon == str.length() - 1) {
162       return false;
163     }
164     if (colon > 0) {
165       final String prefix = str.substring(0, colon);
166       final String localPart = str.substring(colon + 1);
167       return XML11Char.isXML11ValidNCName(prefix) && XML11Char.isXML11ValidNCName(localPart);
168     }
169     return XML11Char.isXML11ValidNCName(str);
170   }
171
172   @NotNull
173   private static TemplateImpl cacheTemplate(TemplateToken token, CustomTemplateCallback callback) {
174     TemplateImpl template = callback.findApplicableTemplate(token.myKey);
175     assert template != null;
176     token.myTemplate = template;
177     return template;
178   }
179
180   @Nullable
181   private static List<Token> parse(@NotNull String text, @NotNull CustomTemplateCallback callback) {
182     text += MARKER;
183     StringBuilder templateKeyBuilder = new StringBuilder();
184     List<Token> result = new ArrayList<Token>();
185     for (int i = 0, n = text.length(); i < n; i++) {
186       char c = text.charAt(i);
187       if (i == n - 1 || (i < n - 2 && OPERATIONS.indexOf(c) >= 0)) {
188         String key = templateKeyBuilder.toString();
189         templateKeyBuilder = new StringBuilder();
190         int num = parseNonNegativeInt(key);
191         if (num > 0) {
192           result.add(new NumberToken(num));
193         }
194         else {
195           if (key.length() == 0) {
196             return null;
197           }
198           String prefix = getPrefix(key);
199           boolean applicable = callback.isLiveTemplateApplicable(prefix);
200           if (!applicable && !isXML11ValidQName(prefix)) {
201             return null;
202           }
203           TemplateToken token = parseSelectors(key);
204           if (token == null) {
205             return null;
206           }
207           if (applicable && (token.myAttribute2Value.size() > 0 || callback.getFileType() == StdFileTypes.XHTML)) {
208             assert prefix.equals(token.myKey);
209             TemplateImpl template = cacheTemplate(token, callback);
210             if (token.myAttribute2Value.size() > 0) {
211               XmlTag tag = parseXmlTagInTemplate(template.getString(), callback, false);
212               if (tag == null) {
213                 return null;
214               }
215             }
216           }
217           result.add(token);
218         }
219         result.add(i < n - 1 ? new OperationToken(c) : new MarkerToken());
220       }
221       else if (!Character.isWhitespace(c)) {
222         templateKeyBuilder.append(c);
223       }
224       else {
225         return null;
226       }
227     }
228     return result;
229   }
230
231   private static boolean check(@NotNull Collection<Token> tokens) {
232     State state = State.WORD;
233     for (Token token : tokens) {
234       if (token instanceof MarkerToken) {
235         break;
236       }
237       switch (state) {
238         case OPERATION:
239           if (token instanceof OperationToken) {
240             state = ((OperationToken)token).mySign == '*' ? State.NUMBER : State.WORD;
241           }
242           else {
243             return false;
244           }
245           break;
246         case WORD:
247           if (token instanceof TemplateToken) {
248             state = State.OPERATION;
249           }
250           else {
251             return false;
252           }
253           break;
254         case NUMBER:
255           if (token instanceof NumberToken) {
256             state = State.AFTER_NUMBER;
257           }
258           else {
259             return false;
260           }
261           break;
262         case AFTER_NUMBER:
263           if (token instanceof OperationToken && ((OperationToken)token).mySign != '*') {
264             state = State.WORD;
265           }
266           else {
267             return false;
268           }
269           break;
270       }
271     }
272     return state == State.OPERATION || state == State.AFTER_NUMBER;
273   }
274
275   private static String computeKey(Editor editor, int startOffset) {
276     int offset = editor.getCaretModel().getOffset();
277     String s = editor.getDocument().getCharsSequence().subSequence(startOffset, offset).toString();
278     int index = 0;
279     while (index < s.length() && Character.isWhitespace(s.charAt(index))) {
280       index++;
281     }
282     String key = s.substring(index);
283     int lastWhitespaceIndex = -1;
284     for (int i = 0; i < key.length(); i++) {
285       if (Character.isWhitespace(key.charAt(i))) {
286         lastWhitespaceIndex = i;
287       }
288     }
289     if (lastWhitespaceIndex >= 0 && lastWhitespaceIndex < key.length() - 1) {
290       return key.substring(lastWhitespaceIndex + 1);
291     }
292     return key;
293   }
294
295   public String computeTemplateKey(@NotNull CustomTemplateCallback callback) {
296     Editor editor = callback.getEditor();
297     int offset = callback.getOffset();
298     PsiElement element = callback.getFile().findElementAt(offset > 0 ? offset - 1 : offset);
299     int line = editor.getCaretModel().getLogicalPosition().line;
300     int lineStart = editor.getDocument().getLineStartOffset(line);
301     int parentStart;
302     do {
303       parentStart = element != null ? element.getTextRange().getStartOffset() : 0;
304       int startOffset = parentStart > lineStart ? parentStart : lineStart;
305       String key = computeKey(editor, startOffset);
306       List<Token> tokens = parse(key, callback);
307       if (tokens != null && check(tokens)) {
308         // !! required if Zen Coding if invoked by TemplateManagerImpl action
309         /*if (tokens.size() == 2) {
310           Token token = tokens.get(0);
311           if (token instanceof TemplateToken) {
312             if (key.equals(((TemplateToken)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<Token> tokens = parse(key, callback);
344     assert tokens != null;
345     XmlZenCodingInterpreter interpreter = new XmlZenCodingInterpreter(tokens, callback, State.WORD, listener);
346     interpreter.invoke(0);
347   }
348
349   public void wrap(String selection, @NotNull CustomTemplateCallback callback, @Nullable TemplateInvokationListener listener) {
350   }
351
352   @Nullable
353   static XmlTag parseXmlTagInTemplate(String templateString, CustomTemplateCallback callback, boolean createPhysicalFile) {
354     XmlFile xmlFile = (XmlFile)PsiFileFactory.getInstance(callback.getProject())
355       .createFileFromText("dummy.xml", StdFileTypes.XML, templateString, LocalTimeCounter.currentTime(), createPhysicalFile);
356     XmlDocument document = xmlFile.getDocument();
357     return document == null ? null : document.getRootTag();
358   }
359
360   public static boolean startZenCoding(Editor editor, PsiFile file) {
361     int caretAt = editor.getCaretModel().getOffset();
362     XmlZenCodingTemplate template = new XmlZenCodingTemplate();
363     if (template.isApplicable(file, caretAt, false)) {
364       final CustomTemplateCallback callback = new CustomTemplateCallback(editor, file);
365       String key = template.computeTemplateKey(callback);
366       if (key != null) {
367         int offsetBeforeKey = caretAt - key.length();
368         callback.getEditor().getDocument().deleteString(offsetBeforeKey, caretAt);
369         template.expand(key, callback, new TemplateInvokationListener() {
370           public void finished(boolean inSeparateEvent) {
371             callback.finish();
372           }
373         });
374         return true;
375       }
376       // if it is simple live template invokation, we should start it using TemplateManager because template may be ambiguous
377       TemplateManager manager = TemplateManager.getInstance(file.getProject());
378       return manager.startTemplate(editor, TemplateSettings.getInstance().getDefaultShortcutChar());
379     }
380     return false;
381   }
382 }