f46965b3cb5c543d0d63b0507ab9b63758a1576f
[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.CodeInsightBundle;
20 import com.intellij.codeInsight.template.CustomLiveTemplate;
21 import com.intellij.codeInsight.template.CustomTemplateCallback;
22 import com.intellij.codeInsight.template.TemplateInvokationListener;
23 import com.intellij.codeInsight.template.impl.TemplateImpl;
24 import com.intellij.lang.xml.XMLLanguage;
25 import com.intellij.openapi.application.ApplicationManager;
26 import com.intellij.openapi.command.CommandProcessor;
27 import com.intellij.openapi.editor.Editor;
28 import com.intellij.openapi.editor.EditorModificationUtil;
29 import com.intellij.openapi.fileTypes.FileType;
30 import com.intellij.openapi.fileTypes.StdFileTypes;
31 import com.intellij.openapi.ui.InputValidatorEx;
32 import com.intellij.openapi.ui.Messages;
33 import com.intellij.openapi.util.Pair;
34 import com.intellij.psi.PsiDocumentManager;
35 import com.intellij.psi.PsiElement;
36 import com.intellij.psi.PsiFile;
37 import com.intellij.psi.PsiFileFactory;
38 import com.intellij.psi.util.PsiTreeUtil;
39 import com.intellij.psi.xml.*;
40 import com.intellij.util.LocalTimeCounter;
41 import com.intellij.util.containers.HashSet;
42 import com.intellij.xml.XmlBundle;
43 import org.apache.xerces.util.XML11Char;
44 import org.jetbrains.annotations.NotNull;
45 import org.jetbrains.annotations.Nullable;
46
47 import java.util.ArrayList;
48 import java.util.Collection;
49 import java.util.List;
50 import java.util.Set;
51
52 /**
53  * @author Eugene.Kudelevsky
54  */
55 public class XmlZenCodingTemplate implements CustomLiveTemplate {
56   static final char MARKER = '$';
57   private static final String OPERATIONS = ">+*";
58   private static final String SELECTORS = ".#[";
59   private static final String ID = "id";
60   private static final String CLASS = "class";
61
62   private static int parseNonNegativeInt(@NotNull String s) {
63     try {
64       return Integer.parseInt(s);
65     }
66     catch (Throwable ignored) {
67     }
68     return -1;
69   }
70
71   private static String getPrefix(@NotNull String templateKey) {
72     for (int i = 0, n = templateKey.length(); i < n; i++) {
73       char c = templateKey.charAt(i);
74       if (SELECTORS.indexOf(c) >= 0) {
75         return templateKey.substring(0, i);
76       }
77     }
78     return templateKey;
79   }
80
81   @Nullable
82   private static Pair<String, String> parseAttrNameAndValue(@NotNull String text) {
83     int eqIndex = text.indexOf('=');
84     if (eqIndex > 0) {
85       return new Pair<String, String>(text.substring(0, eqIndex), text.substring(eqIndex + 1));
86     }
87     return null;
88   }
89
90   @Nullable
91   private static TemplateToken parseSelectors(@NotNull String text) {
92     String templateKey = null;
93     List<Pair<String, String>> attributes = new ArrayList<Pair<String, String>>();
94     Set<String> definedAttrs = new HashSet<String>();
95     final List<String> classes = new ArrayList<String>();
96     StringBuilder builder = new StringBuilder();
97     char lastDelim = 0;
98     text += MARKER;
99     int classAttrPosition = -1;
100     for (int i = 0, n = text.length(); i < n; i++) {
101       char c = text.charAt(i);
102       if (c == '#' || c == '.' || c == '[' || c == ']' || i == n - 1) {
103         if (c != ']') {
104           switch (lastDelim) {
105             case 0:
106               templateKey = builder.toString();
107               break;
108             case '#':
109               if (!definedAttrs.add(ID)) {
110                 return null;
111               }
112               attributes.add(new Pair<String, String>(ID, builder.toString()));
113               break;
114             case '.':
115               if (builder.length() <= 0) {
116                 return null;
117               }
118               if (classAttrPosition < 0) {
119                 classAttrPosition = attributes.size();
120               }
121               classes.add(builder.toString());
122               break;
123             case ']':
124               if (builder.length() > 0) {
125                 return null;
126               }
127               break;
128             default:
129               return null;
130           }
131         }
132         else if (lastDelim != '[') {
133           return null;
134         }
135         else {
136           Pair<String, String> pair = parseAttrNameAndValue(builder.toString());
137           if (pair == null || !definedAttrs.add(pair.first)) {
138             return null;
139           }
140           attributes.add(pair);
141         }
142         lastDelim = c;
143         builder = new StringBuilder();
144       }
145       else {
146         builder.append(c);
147       }
148     }
149     if (classes.size() > 0) {
150       if (definedAttrs.contains(CLASS)) {
151         return null;
152       }
153       StringBuilder classesAttrValue = new StringBuilder();
154       for (int i = 0; i < classes.size(); i++) {
155         classesAttrValue.append(classes.get(i));
156         if (i < classes.size() - 1) {
157           classesAttrValue.append(' ');
158         }
159       }
160       assert classAttrPosition >= 0;
161       attributes.add(classAttrPosition, new Pair<String, String>(CLASS, classesAttrValue.toString()));
162     }
163     return new TemplateToken(templateKey, attributes);
164   }
165
166   private static boolean isXML11ValidQName(String str) {
167     final int colon = str.indexOf(':');
168     if (colon == 0 || colon == str.length() - 1) {
169       return false;
170     }
171     if (colon > 0) {
172       final String prefix = str.substring(0, colon);
173       final String localPart = str.substring(colon + 1);
174       return XML11Char.isXML11ValidNCName(prefix) && XML11Char.isXML11ValidNCName(localPart);
175     }
176     return XML11Char.isXML11ValidNCName(str);
177   }
178
179   public static boolean isTrueXml(CustomTemplateCallback callback) {
180     FileType type = callback.getFileType();
181     return type == StdFileTypes.XHTML || type == StdFileTypes.JSPX || type == StdFileTypes.XML;
182   }
183
184   @Nullable
185   private static List<Token> parse(@NotNull String text, @NotNull CustomTemplateCallback callback) {
186     text += MARKER;
187     StringBuilder templateKeyBuilder = new StringBuilder();
188     List<Token> result = new ArrayList<Token>();
189     for (int i = 0, n = text.length(); i < n; i++) {
190       char c = text.charAt(i);
191       if (i == n - 1 || (i < n - 2 && OPERATIONS.indexOf(c) >= 0)) {
192         String key = templateKeyBuilder.toString();
193         templateKeyBuilder = new StringBuilder();
194         int num = parseNonNegativeInt(key);
195         if (num > 0) {
196           result.add(new NumberToken(num));
197         }
198         else {
199           if (key.length() == 0) {
200             return null;
201           }
202           String prefix = getPrefix(key);
203           TemplateImpl template = callback.findApplicableTemplate(prefix);
204           if (template == null && !isXML11ValidQName(prefix)) {
205             return null;
206           }
207           TemplateToken token = parseSelectors(key);
208           if (token == null) {
209             return null;
210           }
211           if (template != null && (token.myAttribute2Value.size() > 0 || isTrueXml(callback))) {
212             assert prefix.equals(token.myKey);
213             token.myTemplate = template;
214             if (token.myAttribute2Value.size() > 0) {
215               XmlTag tag = parseXmlTagInTemplate(template.getString(), callback, false);
216               if (tag == null) {
217                 return null;
218               }
219             }
220           }
221           result.add(token);
222         }
223         result.add(i < n - 1 ? new OperationToken(c) : new MarkerToken());
224       }
225       else if (!Character.isWhitespace(c)) {
226         templateKeyBuilder.append(c);
227       }
228       else {
229         return null;
230       }
231     }
232     return result;
233   }
234
235   private static boolean check(@NotNull Collection<Token> tokens) {
236     State state = State.WORD;
237     for (Token token : tokens) {
238       if (token instanceof MarkerToken) {
239         break;
240       }
241       switch (state) {
242         case OPERATION:
243           if (token instanceof OperationToken) {
244             state = ((OperationToken)token).mySign == '*' ? State.NUMBER : State.WORD;
245           }
246           else {
247             return false;
248           }
249           break;
250         case WORD:
251           if (token instanceof TemplateToken) {
252             state = State.OPERATION;
253           }
254           else {
255             return false;
256           }
257           break;
258         case NUMBER:
259           if (token instanceof NumberToken) {
260             state = State.AFTER_NUMBER;
261           }
262           else {
263             return false;
264           }
265           break;
266         case AFTER_NUMBER:
267           if (token instanceof OperationToken && ((OperationToken)token).mySign != '*') {
268             state = State.WORD;
269           }
270           else {
271             return false;
272           }
273           break;
274       }
275     }
276     return state == State.OPERATION || state == State.AFTER_NUMBER;
277   }
278
279   private static String computeKey(Editor editor, int startOffset) {
280     int offset = editor.getCaretModel().getOffset();
281     String s = editor.getDocument().getCharsSequence().subSequence(startOffset, offset).toString();
282     int index = 0;
283     while (index < s.length() && Character.isWhitespace(s.charAt(index))) {
284       index++;
285     }
286     String key = s.substring(index);
287     int lastWhitespaceIndex = -1;
288     for (int i = 0; i < key.length(); i++) {
289       if (Character.isWhitespace(key.charAt(i))) {
290         lastWhitespaceIndex = i;
291       }
292     }
293     if (lastWhitespaceIndex >= 0 && lastWhitespaceIndex < key.length() - 1) {
294       return key.substring(lastWhitespaceIndex + 1);
295     }
296     return key;
297   }
298
299   public String computeTemplateKey(@NotNull CustomTemplateCallback callback) {
300     Editor editor = callback.getEditor();
301     PsiElement element = callback.getContext();
302     int line = editor.getCaretModel().getLogicalPosition().line;
303     int lineStart = editor.getDocument().getLineStartOffset(line);
304     int parentStart;
305     do {
306       parentStart = element != null ? element.getTextRange().getStartOffset() : 0;
307       int startOffset = parentStart > lineStart ? parentStart : lineStart;
308       String key = computeKey(editor, startOffset);
309       if (checkTemplateKey(key, callback)) {
310         return key;
311       }
312       if (element != null) {
313         element = element.getParent();
314       }
315     }
316     while (element != null && parentStart > lineStart);
317     return null;
318   }
319
320   private static boolean checkTemplateKey(String key, CustomTemplateCallback callback) {
321     List<Token> tokens = parse(key, callback);
322     if (tokens != null && check(tokens)) {
323       // !! required if Zen Coding if invoked by TemplateManagerImpl action
324       /*if (tokens.size() == 2) {
325         Token token = tokens.get(0);
326         if (token instanceof TemplateToken) {
327           if (key.equals(((TemplateToken)token).myKey) && callback.isLiveTemplateApplicable(key)) {
328             // do not activate only live template
329             return null;
330           }
331         }
332       }*/
333       return true;
334     }
335     return false;
336   }
337
338   public boolean isApplicable(PsiFile file, int offset, boolean selection) {
339     return isApplicable(file, offset);
340   }
341
342   private static boolean isApplicable(PsiFile file, int offset) {
343     WebEditorOptions webEditorOptions = WebEditorOptions.getInstance();
344     if (!webEditorOptions.isZenCodingEnabled()) {
345       return false;
346     }
347     PsiElement element = file.findElementAt(offset > 0 ? offset - 1 : offset);
348     if (element == null) {
349       element = file;
350     }
351     if (element.getLanguage() instanceof XMLLanguage) {
352       if (PsiTreeUtil.getParentOfType(element, XmlAttributeValue.class) != null) {
353         return false;
354       }
355       if (PsiTreeUtil.getParentOfType(element, XmlComment.class) != null) {
356         return false;
357       }
358       return true;
359     }
360     return false;
361   }
362
363   public void expand(String key, @NotNull CustomTemplateCallback callback) {
364     expand(key, callback, null);
365   }
366
367   private static void expand(String key,
368                              @NotNull CustomTemplateCallback callback,
369                              String surroundedText) {
370     List<Token> tokens = parse(key, callback);
371     assert tokens != null;
372     XmlZenCodingInterpreter.interpret(tokens, 0, callback, State.WORD, surroundedText);
373   }
374
375   public void wrap(final String selection,
376                    @NotNull final CustomTemplateCallback callback,
377                    @Nullable final TemplateInvokationListener listener) {
378     InputValidatorEx validator = new InputValidatorEx() {
379       public String getErrorText(String inputString) {
380         if (!checkTemplateKey(inputString, callback)) {
381           return XmlBundle.message("zen.coding.incorrect.abbreviation.error");
382         }
383         return null;
384       }
385
386       public boolean checkInput(String inputString) {
387         return getErrorText(inputString) == null;
388       }
389
390       public boolean canClose(String inputString) {
391         return checkInput(inputString);
392       }
393     };
394     final String abbreviation = Messages
395       .showInputDialog(callback.getProject(), XmlBundle.message("zen.coding.enter.abbreviation.dialog.label"),
396                        XmlBundle.message("zen.coding.title"), Messages.getQuestionIcon(), "", validator);
397     if (abbreviation != null) {
398       doWrap(selection, abbreviation, callback, listener);
399     }
400   }
401
402   private static void doWrap(final String selection,
403                              final String abbreviation,
404                              final CustomTemplateCallback callback,
405                              final TemplateInvokationListener listener) {
406     ApplicationManager.getApplication().runWriteAction(new Runnable() {
407       public void run() {
408         CommandProcessor.getInstance().executeCommand(callback.getProject(), new Runnable() {
409           public void run() {
410             EditorModificationUtil.deleteSelectedText(callback.getEditor());
411             PsiDocumentManager.getInstance(callback.getProject()).commitAllDocuments();
412             callback.fixInitialState();
413             expand(abbreviation, callback, selection);
414             if (listener != null) {
415               listener.finished();
416             }
417           }
418         }, CodeInsightBundle.message("insert.code.template.command"), null);
419       }
420     });
421   }
422
423   @NotNull
424   public String getTitle() {
425     return XmlBundle.message("zen.coding.title");
426   }
427
428   public char getShortcut() {
429     return WebEditorOptions.getInstance().getZenCodingExpandShortcut();
430   }
431
432   @Nullable
433   static XmlTag parseXmlTagInTemplate(String templateString, CustomTemplateCallback callback, boolean createPhysicalFile) {
434     XmlFile xmlFile = (XmlFile)PsiFileFactory.getInstance(callback.getProject())
435       .createFileFromText("dummy.xml", StdFileTypes.XML, templateString, LocalTimeCounter.currentTime(), createPhysicalFile);
436     XmlDocument document = xmlFile.getDocument();
437     return document == null ? null : document.getRootTag();
438   }
439
440   public static boolean startZenCoding(Editor editor, PsiFile file, String abbreviation) {
441     int caretAt = editor.getCaretModel().getOffset();
442     if (isApplicable(file, caretAt)) {
443       final CustomTemplateCallback callback = new CustomTemplateCallback(editor, file);
444       if (abbreviation != null) {
445         String selection = callback.getEditor().getSelectionModel().getSelectedText();
446         assert selection != null;
447         selection = selection.trim();
448         doWrap(selection, abbreviation, callback, new TemplateInvokationListener() {
449           public void finished() {
450             callback.finish();
451           }
452         });
453       }
454       else {
455         XmlZenCodingTemplate template = new XmlZenCodingTemplate();
456         String key = template.computeTemplateKey(callback);
457         if (key != null) {
458           int offsetBeforeKey = caretAt - key.length();
459           callback.getEditor().getDocument().deleteString(offsetBeforeKey, caretAt);
460           template.expand(key, callback);
461           callback.finish();
462           return true;
463         }
464         // if it is simple live template invokation, we should start it using TemplateManager because template may be ambiguous
465         /*TemplateManager manager = TemplateManager.getInstance(file.getProject());
466         return manager.startTemplate(editor, TemplateSettings.getInstance().getDefaultShortcutChar());*/
467       }
468     }
469     return false;
470   }
471 }