refactoring, support css zen-coding selectors
[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.codeInsight.template.CustomLiveTemplate;
19 import com.intellij.codeInsight.template.CustomTemplateCallback;
20 import com.intellij.codeInsight.template.TemplateInvokationListener;
21 import com.intellij.codeInsight.template.impl.TemplateImpl;
22 import com.intellij.lang.xml.XMLLanguage;
23 import com.intellij.openapi.editor.Editor;
24 import com.intellij.openapi.fileTypes.FileType;
25 import com.intellij.openapi.fileTypes.StdFileTypes;
26 import com.intellij.openapi.util.Pair;
27 import com.intellij.psi.PsiElement;
28 import com.intellij.psi.PsiFile;
29 import com.intellij.psi.PsiFileFactory;
30 import com.intellij.psi.util.PsiTreeUtil;
31 import com.intellij.psi.xml.*;
32 import com.intellij.util.LocalTimeCounter;
33 import com.intellij.util.containers.HashSet;
34 import org.apache.xerces.util.XML11Char;
35 import org.jetbrains.annotations.NotNull;
36 import org.jetbrains.annotations.Nullable;
37
38 import java.util.ArrayList;
39 import java.util.List;
40 import java.util.Set;
41
42 /**
43  * @author Eugene.Kudelevsky
44  */
45 public class XmlZenCodingTemplate extends ZenCodingTemplate {
46   private static final String SELECTORS = ".#[";
47   private static final String ID = "id";
48   private static final String CLASS = "class";
49
50   private static String getPrefix(@NotNull String templateKey) {
51     for (int i = 0, n = templateKey.length(); i < n; i++) {
52       char c = templateKey.charAt(i);
53       if (SELECTORS.indexOf(c) >= 0) {
54         return templateKey.substring(0, i);
55       }
56     }
57     return templateKey;
58   }
59
60   @Nullable
61   private static Pair<String, String> parseAttrNameAndValue(@NotNull String text) {
62     int eqIndex = text.indexOf('=');
63     if (eqIndex > 0) {
64       return new Pair<String, String>(text.substring(0, eqIndex), text.substring(eqIndex + 1));
65     }
66     return null;
67   }
68
69   @Nullable
70   private static TemplateToken parseSelectors(@NotNull String text) {
71     String templateKey = null;
72     List<Pair<String, String>> attributes = new ArrayList<Pair<String, String>>();
73     Set<String> definedAttrs = new HashSet<String>();
74     final List<String> classes = new ArrayList<String>();
75     StringBuilder builder = new StringBuilder();
76     char lastDelim = 0;
77     text += MARKER;
78     int classAttrPosition = -1;
79     for (int i = 0, n = text.length(); i < n; i++) {
80       char c = text.charAt(i);
81       if (c == '#' || c == '.' || c == '[' || c == ']' || i == n - 1) {
82         if (c != ']') {
83           switch (lastDelim) {
84             case 0:
85               templateKey = builder.toString();
86               break;
87             case '#':
88               if (!definedAttrs.add(ID)) {
89                 return null;
90               }
91               attributes.add(new Pair<String, String>(ID, builder.toString()));
92               break;
93             case '.':
94               if (builder.length() <= 0) {
95                 return null;
96               }
97               if (classAttrPosition < 0) {
98                 classAttrPosition = attributes.size();
99               }
100               classes.add(builder.toString());
101               break;
102             case ']':
103               if (builder.length() > 0) {
104                 return null;
105               }
106               break;
107             default:
108               return null;
109           }
110         }
111         else if (lastDelim != '[') {
112           return null;
113         }
114         else {
115           Pair<String, String> pair = parseAttrNameAndValue(builder.toString());
116           if (pair == null || !definedAttrs.add(pair.first)) {
117             return null;
118           }
119           attributes.add(pair);
120         }
121         lastDelim = c;
122         builder = new StringBuilder();
123       }
124       else {
125         builder.append(c);
126       }
127     }
128     if (classes.size() > 0) {
129       if (definedAttrs.contains(CLASS)) {
130         return null;
131       }
132       StringBuilder classesAttrValue = new StringBuilder();
133       for (int i = 0; i < classes.size(); i++) {
134         classesAttrValue.append(classes.get(i));
135         if (i < classes.size() - 1) {
136           classesAttrValue.append(' ');
137         }
138       }
139       assert classAttrPosition >= 0;
140       attributes.add(classAttrPosition, new Pair<String, String>(CLASS, classesAttrValue.toString()));
141     }
142     return new TemplateToken(templateKey, attributes);
143   }
144
145   private static boolean isXML11ValidQName(String str) {
146     final int colon = str.indexOf(':');
147     if (colon == 0 || colon == str.length() - 1) {
148       return false;
149     }
150     if (colon > 0) {
151       final String prefix = str.substring(0, colon);
152       final String localPart = str.substring(colon + 1);
153       return XML11Char.isXML11ValidNCName(prefix) && XML11Char.isXML11ValidNCName(localPart);
154     }
155     return XML11Char.isXML11ValidNCName(str);
156   }
157
158   public static boolean isTrueXml(CustomTemplateCallback callback) {
159     FileType type = callback.getFileType();
160     return type == StdFileTypes.XHTML || type == StdFileTypes.JSPX || type == StdFileTypes.XML;
161   }
162
163   @Override
164   @Nullable
165   protected TemplateToken parseTemplateKey(String key, CustomTemplateCallback callback) {
166     String prefix = getPrefix(key);
167     TemplateImpl template = callback.findApplicableTemplate(prefix);
168     if (template == null && !isXML11ValidQName(prefix)) {
169       return null;
170     }
171     TemplateToken token = parseSelectors(key);
172     if (token == null) {
173       return null;
174     }
175     if (template != null && (token.myAttribute2Value.size() > 0 || isTrueXml(callback))) {
176       assert prefix.equals(token.myKey);
177       token.myTemplate = template;
178       if (token.myAttribute2Value.size() > 0) {
179         XmlTag tag = parseXmlTagInTemplate(template.getString(), callback, false);
180         if (tag == null) {
181           return null;
182         }
183       }
184     }
185     return token;
186   }
187
188   @Nullable
189   static XmlTag parseXmlTagInTemplate(String templateString, CustomTemplateCallback callback, boolean createPhysicalFile) {
190     XmlFile xmlFile = (XmlFile)PsiFileFactory.getInstance(callback.getProject())
191       .createFileFromText("dummy.xml", StdFileTypes.XML, templateString, LocalTimeCounter.currentTime(), createPhysicalFile);
192     XmlDocument document = xmlFile.getDocument();
193     return document == null ? null : document.getRootTag();
194   }
195
196   protected boolean isApplicable(@NotNull PsiElement element) {
197     if (element.getLanguage() instanceof XMLLanguage) {
198       if (PsiTreeUtil.getParentOfType(element, XmlAttributeValue.class) != null) {
199         return false;
200       }
201       if (PsiTreeUtil.getParentOfType(element, XmlComment.class) != null) {
202         return false;
203       }
204       return true;
205     }
206     return false;
207   }
208
209   public static boolean startZenCoding(Editor editor, PsiFile file, String abbreviation) {
210     int caretAt = editor.getCaretModel().getOffset();
211     XmlZenCodingTemplate template = CustomLiveTemplate.EP_NAME.findExtension(XmlZenCodingTemplate.class);
212     if (abbreviation != null && !template.supportsWrapping()) {
213       return false;
214     }
215     if (template.isApplicable(file, caretAt)) {
216       final CustomTemplateCallback callback = new CustomTemplateCallback(editor, file);
217       if (abbreviation != null) {
218         String selection = callback.getEditor().getSelectionModel().getSelectedText();
219         assert selection != null;
220         selection = selection.trim();
221         template.doWrap(selection, abbreviation, callback, new TemplateInvokationListener() {
222           public void finished() {
223             callback.startAllExpandedTemplates();
224           }
225         });
226       }
227       else {
228         String key = template.computeTemplateKey(callback);
229         if (key != null) {
230           template.expand(key, callback);
231           callback.startAllExpandedTemplates();
232           return true;
233         }
234         // if it is simple live template invokation, we should start it using TemplateManager because template may be ambiguous
235         /*TemplateManager manager = TemplateManager.getInstance(file.getProject());
236         return manager.startTemplate(editor, TemplateSettings.getInstance().getDefaultShortcutChar());*/
237       }
238     }
239     return false;
240   }
241
242   public String computeTemplateKey(@NotNull CustomTemplateCallback callback) {
243     Editor editor = callback.getEditor();
244     PsiElement element = callback.getContext();
245     int line = editor.getCaretModel().getLogicalPosition().line;
246     int lineStart = editor.getDocument().getLineStartOffset(line);
247     int elementStart;
248     do {
249       elementStart = element != null ? element.getTextRange().getStartOffset() : 0;
250       int startOffset = elementStart > lineStart ? elementStart : lineStart;
251       String key = computeKey(editor, startOffset);
252       if (checkTemplateKey(key, callback)) {
253         return key;
254       }
255       if (element != null) {
256         element = element.getParent();
257       }
258     }
259     while (element != null && elementStart > lineStart);
260     return null;
261   }
262
263   public boolean supportsWrapping() {
264     return true;
265   }
266 }