2 * Copyright 2000-2010 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.intellij.codeInsight.template;
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;
39 * @author Eugene.Kudelevsky
41 public class XmlZenCodingTemplate implements CustomLiveTemplate {
42 private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.template.XmlZenCodingTemplate");
44 private static final String ATTRS = "ATTRS";
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 = "$";
53 private static enum MyState {
54 OPERATION, WORD, AFTER_NUMBER, NUMBER
57 private static class MyToken {
60 private static class MyMarkerToken extends MyToken {
63 private static class MyTemplateToken extends MyToken {
65 final List<Pair<String, String>> myAttribute2Value;
67 MyTemplateToken(String key, List<Pair<String, String>> attribute2value) {
69 myAttribute2Value = attribute2value;
73 private static class MyNumberToken extends MyToken {
76 MyNumberToken(int number) {
81 private static class MyOperationToken extends MyToken {
84 MyOperationToken(char sign) {
89 private static int parseNonNegativeInt(@NotNull String s) {
91 return Integer.parseInt(s);
93 catch (Throwable ignored) {
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);
109 private static Pair<String, String> parseAttrNameAndValue(@NotNull String text) {
110 int eqIndex = text.indexOf('=');
112 return new Pair<String, String>(text.substring(0, eqIndex), text.substring(eqIndex + 1));
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();
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) {
133 templateKey = builder.toString();
136 if (!definedAttrs.add(ID)) {
139 attributes.add(new Pair<String, String>(ID, builder.toString()));
142 if (builder.length() <= 0) {
145 if (classAttrPosition < 0) {
146 classAttrPosition = attributes.size();
148 classes.add(builder.toString());
151 if (builder.length() > 0) {
159 else if (lastDelim != '[') {
163 Pair<String, String> pair = parseAttrNameAndValue(builder.toString());
164 if (pair == null || !definedAttrs.add(pair.first)) {
167 attributes.add(pair);
170 builder = new StringBuilder();
176 if (classes.size() > 0) {
177 if (definedAttrs.contains(CLASS)) {
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(' ');
187 assert classAttrPosition >= 0;
188 attributes.add(classAttrPosition, new Pair<String, String>(CLASS, classesAttrValue.toString()));
190 return new MyTemplateToken(templateKey, attributes);
194 private static List<MyToken> parse(@NotNull String text, @NotNull CustomTemplateCallback callback) {
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);
205 result.add(new MyNumberToken(num));
208 if (key.length() == 0) {
211 String prefix = getPrefix(key);
212 if (!callback.isLiveTemplateApplicable(prefix) && !XML11Char.isXML11ValidQName(prefix)) {
215 MyTemplateToken token = parseSelectors(key);
221 result.add(i < n - 1 ? new MyOperationToken(c) : new MyMarkerToken());
223 else if (!Character.isWhitespace(c)) {
224 templateKeyBuilder.append(c);
233 private static boolean check(@NotNull Collection<MyToken> tokens) {
234 MyState state = MyState.WORD;
235 for (MyToken token : tokens) {
236 if (token instanceof MyMarkerToken) {
241 if (token instanceof MyOperationToken) {
242 state = ((MyOperationToken)token).mySign == '*' ? MyState.NUMBER : MyState.WORD;
249 if (token instanceof MyTemplateToken) {
250 state = MyState.OPERATION;
257 if (token instanceof MyNumberToken) {
258 state = MyState.AFTER_NUMBER;
265 if (token instanceof MyOperationToken && ((MyOperationToken)token).mySign != '*') {
266 state = MyState.WORD;
274 return state == MyState.OPERATION || state == MyState.AFTER_NUMBER;
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();
281 while (index < s.length() && Character.isWhitespace(s.charAt(index))) {
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;
291 if (lastWhitespaceIndex >= 0 && lastWhitespaceIndex < key.length() - 1) {
292 return key.substring(lastWhitespaceIndex + 1);
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);
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
321 if (element != null) {
322 element = element.getParent();
325 while (element != null && parentStart > lineStart);
329 public boolean isApplicable(PsiFile file, int offset, boolean selection) {
330 WebEditorOptions webEditorOptions = WebEditorOptions.getInstance();
331 if (!webEditorOptions.isZenCodingEnabled()) {
334 if (file.getLanguage() instanceof XMLLanguage) {
335 PsiElement element = file.findElementAt(offset > 0 ? offset - 1 : offset);
336 if (element == null || element.getLanguage() instanceof XMLLanguage) {
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);
350 public void wrap(String selection, @NotNull CustomTemplateCallback callback, @Nullable TemplateInvokationListener listener) {
353 private static void fail() {
354 LOG.error("Input string was checked incorrectly during isApplicable() invokation");
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('"');
369 return result.toString();
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);
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);
390 return callback.startTemplate(token.myKey, predefinedValues, listener);
393 TemplateImpl template = new TemplateImpl("", "");
394 template.addTextSegment('<' + token.myKey);
395 if (attributes != null) {
396 template.addVariable(ATTRS, "", "", false);
397 template.addVariableSegment(ATTRS);
399 template.addTextSegment(">");
400 template.addVariableSegment(TemplateImpl.END);
401 template.addTextSegment("</" + token.myKey + ">");
402 template.setToReformat(true);
403 return callback.startTemplate(template, predefinedValues, listener);
407 private static int findPlaceToInsertAttrs(@NotNull TemplateImpl template) {
408 String s = template.getString();
409 if (s.length() > 0) {
410 if (s.charAt(0) != '<') {
414 while (i < s.length() && !Character.isWhitespace(s.charAt(i)) && s.charAt(i) != '>') {
420 if (s.indexOf('>', i) >= i) {
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);
434 String newTemplateString = templateString.substring(0, offset) + attributes + templateString.substring(offset);
435 TemplateImpl newTemplate = template.copy();
436 newTemplate.setString(newTemplateString);
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)) {
452 private class MyInterpreter {
453 private final List<MyToken> myTokens;
454 private final CustomTemplateCallback myCallback;
455 private final TemplateInvokationListener myListener;
456 private MyState myState;
458 private MyInterpreter(List<MyToken> tokens,
459 CustomTemplateCallback callback,
460 MyState initialState,
461 TemplateInvokationListener listener) {
463 myCallback = callback;
464 myListener = listener;
465 myState = initialState;
468 private void finish(boolean inSeparateEvent) {
469 myCallback.gotoEndOffset();
470 if (myListener != null) {
471 myListener.finished(inSeparateEvent);
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();
481 PsiFile file = myCallback.getFile();
483 PsiElement element = file.findElementAt(offset);
484 if (element instanceof XmlToken && ((XmlToken)element).getTokenType() == XmlTokenType.XML_END_TAG_START) {
489 XmlTag tag = PsiTreeUtil.findElementOfClassAtRange(file, startOfTemplate, endOfTemplate, XmlTag.class);
491 for (PsiElement child : tag.getChildren()) {
492 if (child instanceof XmlToken && ((XmlToken)child).getTokenType() == XmlTokenType.XML_END_TAG_START) {
493 newOffset = child.getTextOffset();
498 if (newOffset >= 0) {
499 myCallback.fixEndOffset();
500 editor.getCaretModel().moveToOffset(newOffset);
503 /*CharSequence tagName = getPrecedingTagName(text, offset, startOfTemplate);
504 if (tagName != null) {
505 *//*if (!hasClosingTag(text, tagName, offset, endOfTemplate)) {
506 document.insertString(offset, "</" + tagName + '>');
509 else if (offset != endOfTemplate) {
510 tagName = getPrecedingTagName(text, endOfTemplate, startOfTemplate);
511 if (tagName != null) {
513 document.insertString(endOfTemplate, "</" + tagName + '>');*//*
514 editor.getCaretModel().moveToOffset(endOfTemplate);
519 public boolean invoke(int startIndex) {
520 final int n = myTokens.size();
521 MyTemplateToken templateToken = null;
523 for (int i = startIndex; i < n; i++) {
524 final int finalI = i;
525 MyToken token = myTokens.get(i);
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();
541 myCallback.gotoEndOfTemplate(key);
543 if (inSeparateEvent) {
548 if (!invokeTemplate(templateToken, myCallback, listener, -1)) {
551 templateToken = null;
553 else if (sign == '>') {
554 if (!startTemplateAndGotoChild(templateToken, finalI)) {
557 templateToken = null;
559 else if (sign == '*') {
560 myState = MyState.NUMBER;
569 if (token instanceof MyTemplateToken) {
570 templateToken = ((MyTemplateToken)token);
571 myState = MyState.OPERATION;
578 if (token instanceof MyNumberToken) {
579 number = ((MyNumberToken)token).myNumber;
580 myState = MyState.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)) {
593 templateToken = null;
595 else if (number > 1) {
596 return invokeTemplateAndProcessTail(templateToken, 0, number, i + 1);
600 if (!startTemplateAndGotoChild(templateToken, finalI)) {
603 templateToken = null;
605 myState = MyState.WORD;
613 finish(startIndex == n);
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;
624 if (inSeparateEvent) {
629 if (!invokeTemplate(templateToken, myCallback, listener, -1)) {
635 private boolean invokeTemplateSeveralTimes(final MyTemplateToken templateToken,
636 final int startIndex,
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();
649 myCallback.gotoEndOfTemplate(key);
650 if (inSeparateEvent) {
651 if (finalI + 1 < count) {
652 invokeTemplateSeveralTimes(templateToken, finalI + 1, count, globalIndex);
655 invoke(globalIndex + 1);
660 if (!invokeTemplate(templateToken, myCallback, listener, i)) {
667 private boolean invokeTemplateAndProcessTail(final MyTemplateToken templateToken,
668 final int startIndex,
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) {
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();
684 myCallback.gotoEndOfTemplate(key);
685 if (inSeparateEvent) {
686 invokeTemplateAndProcessTail(templateToken, finalI + 1, count, tailStart);
690 if (interpreter.invoke(tailStart)) {
691 if (inSeparateEvent) {
692 invokeTemplateAndProcessTail(templateToken, finalI + 1, count, tailStart);
700 if (!invokeTemplate(templateToken, myCallback, listener, i) || flag[0]) {