implementationClass="com.intellij.codeInsight.hint.XmlImplementationTextSelectioner"/>
<basicWordSelectionFilter implementation="com.intellij.codeInsight.editorActions.XmlBasicWordSelectionFilter"/>
<defaultLiveTemplatesProvider implementation="com.intellij.codeInsight.template.XmlDefaultLiveTemplatesProvider"/>
- <customLiveTemplate implementation="com.intellij.codeInsight.template.XmlZenCodingTemplate"/>
+ <customLiveTemplate implementation="com.intellij.codeInsight.template.zencoding.XmlZenCodingTemplate"/>
<productivityFeaturesProvider implementation="com.intellij.featureStatistics.XmlProductivityFeatureProvider"/>
<idIndexer filetype="XML" implementationClass="com.intellij.psi.impl.cache.impl.idCache.XmlIdIndexer"/>
+++ /dev/null
-/*
- * Copyright 2000-2010 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.intellij.codeInsight.template;
-
-import com.intellij.application.options.editor.WebEditorOptions;
-import com.intellij.codeInsight.template.impl.TemplateImpl;
-import com.intellij.lang.xml.XMLLanguage;
-import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.editor.Editor;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.util.Pair;
-import com.intellij.openapi.util.TextRange;
-import com.intellij.psi.PsiElement;
-import com.intellij.psi.PsiFile;
-import com.intellij.psi.PsiFileFactory;
-import com.intellij.psi.util.PsiTreeUtil;
-import com.intellij.psi.xml.*;
-import com.intellij.util.containers.HashMap;
-import com.intellij.util.containers.HashSet;
-import com.intellij.util.containers.IntArrayList;
-import org.apache.xerces.util.XML11Char;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.util.*;
-
-/**
- * @author Eugene.Kudelevsky
- */
-public class XmlZenCodingTemplate implements CustomLiveTemplate {
- private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.template.XmlZenCodingTemplate");
-
- private static final String ATTRS = "ATTRS";
-
- private static final String OPERATIONS = ">+*";
- private static final String SELECTORS = ".#[";
- private static final char MARKER = '$';
- private static final String ID = "id";
- private static final String CLASS = "class";
- private static final String NUMBER_IN_ITERATION_PLACE_HOLDER = "$";
-
- private static enum MyState {
- OPERATION, WORD, AFTER_NUMBER, NUMBER
- }
-
- private static class MyToken {
- }
-
- private static class MyMarkerToken extends MyToken {
- }
-
- private static class MyTemplateToken extends MyToken {
- final String myKey;
- final List<Pair<String, String>> myAttribute2Value;
- TemplateImpl myTemplate;
-
- MyTemplateToken(String key, List<Pair<String, String>> attribute2value) {
- myKey = key;
- myAttribute2Value = attribute2value;
- }
- }
-
- private static class MyNumberToken extends MyToken {
- final int myNumber;
-
- MyNumberToken(int number) {
- myNumber = number;
- }
- }
-
- private static class MyOperationToken extends MyToken {
- final char mySign;
-
- MyOperationToken(char sign) {
- mySign = sign;
- }
- }
-
- private static int parseNonNegativeInt(@NotNull String s) {
- try {
- return Integer.parseInt(s);
- }
- catch (Throwable ignored) {
- }
- return -1;
- }
-
- private static String getPrefix(@NotNull String templateKey) {
- for (int i = 0, n = templateKey.length(); i < n; i++) {
- char c = templateKey.charAt(i);
- if (SELECTORS.indexOf(c) >= 0) {
- return templateKey.substring(0, i);
- }
- }
- return templateKey;
- }
-
- @Nullable
- private static Pair<String, String> parseAttrNameAndValue(@NotNull String text) {
- int eqIndex = text.indexOf('=');
- if (eqIndex > 0) {
- return new Pair<String, String>(text.substring(0, eqIndex), text.substring(eqIndex + 1));
- }
- return null;
- }
-
- @Nullable
- private static MyTemplateToken parseSelectors(@NotNull String text) {
- String templateKey = null;
- List<Pair<String, String>> attributes = new ArrayList<Pair<String, String>>();
- Set<String> definedAttrs = new HashSet<String>();
- final List<String> classes = new ArrayList<String>();
- StringBuilder builder = new StringBuilder();
- char lastDelim = 0;
- text += MARKER;
- int classAttrPosition = -1;
- for (int i = 0, n = text.length(); i < n; i++) {
- char c = text.charAt(i);
- if (c == '#' || c == '.' || c == '[' || c == ']' || i == n - 1) {
- if (c != ']') {
- switch (lastDelim) {
- case 0:
- templateKey = builder.toString();
- break;
- case '#':
- if (!definedAttrs.add(ID)) {
- return null;
- }
- attributes.add(new Pair<String, String>(ID, builder.toString()));
- break;
- case '.':
- if (builder.length() <= 0) {
- return null;
- }
- if (classAttrPosition < 0) {
- classAttrPosition = attributes.size();
- }
- classes.add(builder.toString());
- break;
- case ']':
- if (builder.length() > 0) {
- return null;
- }
- break;
- default:
- return null;
- }
- }
- else if (lastDelim != '[') {
- return null;
- }
- else {
- Pair<String, String> pair = parseAttrNameAndValue(builder.toString());
- if (pair == null || !definedAttrs.add(pair.first)) {
- return null;
- }
- attributes.add(pair);
- }
- lastDelim = c;
- builder = new StringBuilder();
- }
- else {
- builder.append(c);
- }
- }
- if (classes.size() > 0) {
- if (definedAttrs.contains(CLASS)) {
- return null;
- }
- StringBuilder classesAttrValue = new StringBuilder();
- for (int i = 0; i < classes.size(); i++) {
- classesAttrValue.append(classes.get(i));
- if (i < classes.size() - 1) {
- classesAttrValue.append(' ');
- }
- }
- assert classAttrPosition >= 0;
- attributes.add(classAttrPosition, new Pair<String, String>(CLASS, classesAttrValue.toString()));
- }
- return new MyTemplateToken(templateKey, attributes);
- }
-
- private static boolean isXML11ValidQName(String str) {
- final int colon = str.indexOf(':');
- if (colon == 0 || colon == str.length() - 1) {
- return false;
- }
- if (colon > 0) {
- final String prefix = str.substring(0, colon);
- final String localPart = str.substring(colon + 1);
- return XML11Char.isXML11ValidNCName(prefix) && XML11Char.isXML11ValidNCName(localPart);
- }
- return XML11Char.isXML11ValidNCName(str);
- }
-
- private static boolean containsAttrsVar(TemplateImpl template) {
- for (int i = 0; i < template.getVariableCount(); i++) {
- String varName = template.getVariableNameAt(i);
- if (ATTRS.equals(varName)) {
- return true;
- }
- }
- return false;
- }
-
- private static void removeVariablesWhichHasNoSegment(TemplateImpl template) {
- Set<String> segments = new HashSet<String>();
- for (int i = 0; i < template.getSegmentsCount(); i++) {
- segments.add(template.getSegmentName(i));
- }
- IntArrayList varsToRemove = new IntArrayList();
- for (int i = 0; i < template.getVariableCount(); i++) {
- String varName = template.getVariableNameAt(i);
- if (!segments.contains(varName)) {
- varsToRemove.add(i);
- }
- }
- for (int i = 0; i < varsToRemove.size(); i++) {
- template.removeVariable(varsToRemove.get(i));
- }
- }
-
- @Nullable
- private static XmlTag parseXmlTagInTemplate(String templateString, Project project) {
- XmlFile xmlFile = (XmlFile)PsiFileFactory.getInstance(project).createFileFromText("dummy.xml", templateString);
- XmlDocument document = xmlFile.getDocument();
- return document == null ? null : document.getRootTag();
- }
-
- private static boolean generateTemplateAndAddToToken(MyTemplateToken token, CustomTemplateCallback callback) {
- TemplateImpl template = callback.findApplicableTemplate(token.myKey);
- assert template != null;
- XmlTag tag = parseXmlTagInTemplate(template.getString(), callback.getProject());
- if (tag == null) {
- return false;
- }
- token.myTemplate = template;
- return true;
- }
-
- @Nullable
- private static List<MyToken> parse(@NotNull String text, @NotNull CustomTemplateCallback callback) {
- text += MARKER;
- StringBuilder templateKeyBuilder = new StringBuilder();
- List<MyToken> result = new ArrayList<MyToken>();
- for (int i = 0, n = text.length(); i < n; i++) {
- char c = text.charAt(i);
- if (i == n - 1 || (i < n - 2 && OPERATIONS.indexOf(c) >= 0)) {
- String key = templateKeyBuilder.toString();
- templateKeyBuilder = new StringBuilder();
- int num = parseNonNegativeInt(key);
- if (num > 0) {
- result.add(new MyNumberToken(num));
- }
- else {
- if (key.length() == 0) {
- return null;
- }
- String prefix = getPrefix(key);
- boolean applicable = callback.isLiveTemplateApplicable(prefix);
- if (!applicable && !isXML11ValidQName(prefix)) {
- return null;
- }
- MyTemplateToken token = parseSelectors(key);
- if (token == null) {
- return null;
- }
- if (applicable && token.myAttribute2Value.size() > 0) {
- assert prefix.equals(token.myKey);
- if (!generateTemplateAndAddToToken(token, callback)) {
- return null;
- }
- }
- result.add(token);
- }
- result.add(i < n - 1 ? new MyOperationToken(c) : new MyMarkerToken());
- }
- else if (!Character.isWhitespace(c)) {
- templateKeyBuilder.append(c);
- }
- else {
- return null;
- }
- }
- return result;
- }
-
- private static boolean check(@NotNull Collection<MyToken> tokens) {
- MyState state = MyState.WORD;
- for (MyToken token : tokens) {
- if (token instanceof MyMarkerToken) {
- break;
- }
- switch (state) {
- case OPERATION:
- if (token instanceof MyOperationToken) {
- state = ((MyOperationToken)token).mySign == '*' ? MyState.NUMBER : MyState.WORD;
- }
- else {
- return false;
- }
- break;
- case WORD:
- if (token instanceof MyTemplateToken) {
- state = MyState.OPERATION;
- }
- else {
- return false;
- }
- break;
- case NUMBER:
- if (token instanceof MyNumberToken) {
- state = MyState.AFTER_NUMBER;
- }
- else {
- return false;
- }
- break;
- case AFTER_NUMBER:
- if (token instanceof MyOperationToken && ((MyOperationToken)token).mySign != '*') {
- state = MyState.WORD;
- }
- else {
- return false;
- }
- break;
- }
- }
- return state == MyState.OPERATION || state == MyState.AFTER_NUMBER;
- }
-
- private static String computeKey(Editor editor, int startOffset) {
- int offset = editor.getCaretModel().getOffset();
- String s = editor.getDocument().getCharsSequence().subSequence(startOffset, offset).toString();
- int index = 0;
- while (index < s.length() && Character.isWhitespace(s.charAt(index))) {
- index++;
- }
- String key = s.substring(index);
- int lastWhitespaceIndex = -1;
- for (int i = 0; i < key.length(); i++) {
- if (Character.isWhitespace(key.charAt(i))) {
- lastWhitespaceIndex = i;
- }
- }
- if (lastWhitespaceIndex >= 0 && lastWhitespaceIndex < key.length() - 1) {
- return key.substring(lastWhitespaceIndex + 1);
- }
- return key;
- }
-
- public String computeTemplateKey(@NotNull CustomTemplateCallback callback) {
- Editor editor = callback.getEditor();
- int offset = callback.getOffset();
- PsiElement element = callback.getFile().findElementAt(offset > 0 ? offset - 1 : offset);
- int line = editor.getCaretModel().getLogicalPosition().line;
- int lineStart = editor.getDocument().getLineStartOffset(line);
- int parentStart;
- do {
- parentStart = element != null ? element.getTextRange().getStartOffset() : 0;
- int startOffset = parentStart > lineStart ? parentStart : lineStart;
- String key = computeKey(editor, startOffset);
- List<MyToken> tokens = parse(key, callback);
- if (tokens != null && check(tokens)) {
- if (tokens.size() == 2) {
- MyToken token = tokens.get(0);
- if (token instanceof MyTemplateToken) {
- if (key.equals(((MyTemplateToken)token).myKey) && callback.isLiveTemplateApplicable(key)) {
- // do not activate only live template
- return null;
- }
- }
- }
- return key;
- }
- if (element != null) {
- element = element.getParent();
- }
- }
- while (element != null && parentStart > lineStart);
- return null;
- }
-
- public boolean isApplicable(PsiFile file, int offset, boolean selection) {
- WebEditorOptions webEditorOptions = WebEditorOptions.getInstance();
- if (!webEditorOptions.isZenCodingEnabled()) {
- return false;
- }
- if (file.getLanguage() instanceof XMLLanguage) {
- PsiElement element = file.findElementAt(offset > 0 ? offset - 1 : offset);
- if (element == null || element.getLanguage() instanceof XMLLanguage) {
- return true;
- }
- }
- return false;
- }
-
- public void expand(String key, @NotNull CustomTemplateCallback callback, @Nullable TemplateInvokationListener listener) {
- List<MyToken> tokens = parse(key, callback);
- assert tokens != null;
- MyInterpreter interpreter = new MyInterpreter(tokens, callback, MyState.WORD, listener);
- interpreter.invoke(0);
- }
-
- public void wrap(String selection, @NotNull CustomTemplateCallback callback, @Nullable TemplateInvokationListener listener) {
- }
-
- private static void fail() {
- LOG.error("Input string was checked incorrectly during isApplicable() invokation");
- }
-
- @Nullable
- private static Map<String, String> buildPredefinedValues(List<Pair<String, String>> attribute2value, int numberInIteration) {
- StringBuilder result = new StringBuilder();
- for (Iterator<Pair<String, String>> it = attribute2value.iterator(); it.hasNext();) {
- Pair<String, String> pair = it.next();
- String name = pair.first;
- String value = getValue(pair, numberInIteration);
- result.append(name).append("=\"").append(value).append('"');
- if (it.hasNext()) {
- result.append(' ');
- }
- }
- String attributes = result.toString();
- attributes = attributes.length() > 0 ? ' ' + attributes : null;
- Map<String, String> predefinedValues = null;
- if (attributes != null) {
- predefinedValues = new HashMap<String, String>();
- predefinedValues.put(ATTRS, attributes);
- }
- return predefinedValues;
- }
-
- private static String getValue(Pair<String, String> pair, int numberInIteration) {
- return pair.second.replace(NUMBER_IN_ITERATION_PLACE_HOLDER, Integer.toString(numberInIteration + 1));
- }
-
- @Nullable
- private static String addAttrsVar(TemplateImpl modifiedTemplate, XmlTag tag) {
- String text = tag.getContainingFile().getText();
- PsiElement[] children = tag.getChildren();
- if (children.length >= 1 &&
- children[0] instanceof XmlToken &&
- ((XmlToken)children[0]).getTokenType() == XmlTokenType.XML_START_TAG_START) {
- PsiElement beforeAttrs = children[0];
- if (children.length >= 2 && children[1] instanceof XmlToken && ((XmlToken)children[1]).getTokenType() == XmlTokenType.XML_NAME) {
- beforeAttrs = children[1];
- }
- TextRange range = beforeAttrs.getTextRange();
- if (range == null) {
- return null;
- }
- int offset = range.getEndOffset();
- text = text.substring(0, offset) + " $ATTRS$" + text.substring(offset);
- modifiedTemplate.addVariable(ATTRS, "", "", false);
- return text;
- }
- return null;
- }
-
- private static boolean invokeTemplate(MyTemplateToken token,
- final CustomTemplateCallback callback,
- final TemplateInvokationListener listener,
- int numberInIteration) {
- List<Pair<String, String>> attr2value = new ArrayList<Pair<String, String>>(token.myAttribute2Value);
- if (callback.isLiveTemplateApplicable(token.myKey)) {
- if (token.myTemplate != null) {
- TemplateImpl modifiedTemplate = token.myTemplate.copy();
- XmlTag tag = parseXmlTagInTemplate(token.myTemplate.getString(), callback.getProject());
- assert tag != null;
- for (Iterator<Pair<String, String>> iterator = attr2value.iterator(); iterator.hasNext();) {
- Pair<String, String> pair = iterator.next();
- if (tag.getAttribute(pair.first) != null) {
- tag.setAttribute(pair.first, getValue(pair, numberInIteration));
- iterator.remove();
- }
- }
- String text = null;
- if (!containsAttrsVar(modifiedTemplate) && attr2value.size() > 0) {
- String textWithAttrs = addAttrsVar(modifiedTemplate, tag);
- if (textWithAttrs != null) {
- text = textWithAttrs;
- }
- else {
- for (Iterator<Pair<String, String>> iterator = attr2value.iterator(); iterator.hasNext();) {
- Pair<String, String> pair = iterator.next();
- tag.setAttribute(pair.first, getValue(pair, numberInIteration));
- iterator.remove();
- }
- }
- }
- if (text == null) {
- text = tag.getContainingFile().getText();
- }
- modifiedTemplate.setString(text);
- removeVariablesWhichHasNoSegment(modifiedTemplate);
- Map<String, String> predefinedValues = buildPredefinedValues(attr2value, numberInIteration);
- return callback.startTemplate(modifiedTemplate, predefinedValues, listener);
- }
- else {
- Map<String, String> predefinedValues = buildPredefinedValues(attr2value, numberInIteration);
- return callback.startTemplate(token.myKey, predefinedValues, listener);
- }
- }
- else {
- TemplateImpl template = new TemplateImpl("", "");
- template.addTextSegment('<' + token.myKey);
- if (attr2value.size() > 0) {
- template.addVariable(ATTRS, "", "", false);
- template.addVariableSegment(ATTRS);
- }
- template.addTextSegment(">");
- template.addVariableSegment(TemplateImpl.END);
- template.addTextSegment("</" + token.myKey + ">");
- template.setToReformat(true);
- Map<String, String> predefinedValues = buildPredefinedValues(attr2value, numberInIteration);
- return callback.startTemplate(template, predefinedValues, listener);
- }
- }
-
- /*private static boolean hasClosingTag(CharSequence text, CharSequence tagName, int offset, int rightBound) {
- if (offset + 1 < text.length() && text.charAt(offset) == '<' && text.charAt(offset + 1) == '/') {
- CharSequence closingTagName = parseTagName(text, offset + 2, rightBound);
- if (tagName.equals(closingTagName)) {
- return true;
- }
- }
- return false;
- }*/
-
- private class MyInterpreter {
- private final List<MyToken> myTokens;
- private final CustomTemplateCallback myCallback;
- private final TemplateInvokationListener myListener;
- private MyState myState;
-
- private MyInterpreter(List<MyToken> tokens,
- CustomTemplateCallback callback,
- MyState initialState,
- TemplateInvokationListener listener) {
- myTokens = tokens;
- myCallback = callback;
- myListener = listener;
- myState = initialState;
- }
-
- private void finish(boolean inSeparateEvent) {
- myCallback.gotoEndOffset();
- if (myListener != null) {
- myListener.finished(inSeparateEvent);
- }
- }
-
- private void gotoChild(Object templateBoundsKey) {
- int startOfTemplate = myCallback.getStartOfTemplate(templateBoundsKey);
- int endOfTemplate = myCallback.getEndOfTemplate(templateBoundsKey);
- Editor editor = myCallback.getEditor();
- int offset = myCallback.getOffset();
-
- PsiFile file = myCallback.getFile();
-
- PsiElement element = file.findElementAt(offset);
- if (element instanceof XmlToken && ((XmlToken)element).getTokenType() == XmlTokenType.XML_END_TAG_START) {
- return;
- }
-
- int newOffset = -1;
- XmlTag tag = PsiTreeUtil.findElementOfClassAtRange(file, startOfTemplate, endOfTemplate, XmlTag.class);
- if (tag != null) {
- for (PsiElement child : tag.getChildren()) {
- if (child instanceof XmlToken && ((XmlToken)child).getTokenType() == XmlTokenType.XML_END_TAG_START) {
- newOffset = child.getTextOffset();
- }
- }
- }
-
- if (newOffset >= 0) {
- myCallback.fixEndOffset();
- editor.getCaretModel().moveToOffset(newOffset);
- }
-
- /*CharSequence tagName = getPrecedingTagName(text, offset, startOfTemplate);
- if (tagName != null) {
- *//*if (!hasClosingTag(text, tagName, offset, endOfTemplate)) {
- document.insertString(offset, "</" + tagName + '>');
- }*//*
- }
- else if (offset != endOfTemplate) {
- tagName = getPrecedingTagName(text, endOfTemplate, startOfTemplate);
- if (tagName != null) {
- *//*fixEndOffset();
- document.insertString(endOfTemplate, "</" + tagName + '>');*//*
- editor.getCaretModel().moveToOffset(endOfTemplate);
- }
- }*/
- }
-
- public boolean invoke(int startIndex) {
- final int n = myTokens.size();
- MyTemplateToken templateToken = null;
- int number = -1;
- for (int i = startIndex; i < n; i++) {
- final int finalI = i;
- MyToken token = myTokens.get(i);
- switch (myState) {
- case OPERATION:
- if (templateToken != null) {
- if (token instanceof MyMarkerToken || token instanceof MyOperationToken) {
- final char sign = token instanceof MyOperationToken ? ((MyOperationToken)token).mySign : MARKER;
- if (sign == MARKER || sign == '+') {
- final Object key = new Object();
- myCallback.fixStartOfTemplate(key);
- TemplateInvokationListener listener = new TemplateInvokationListener() {
- public void finished(boolean inSeparateEvent) {
- myState = MyState.WORD;
- if (myCallback.getOffset() != myCallback.getEndOfTemplate(key)) {
- myCallback.fixEndOffset();
- }
- if (sign == '+') {
- myCallback.gotoEndOfTemplate(key);
- }
- if (inSeparateEvent) {
- invoke(finalI + 1);
- }
- }
- };
- if (!invokeTemplate(templateToken, myCallback, listener, 0)) {
- return false;
- }
- templateToken = null;
- }
- else if (sign == '>') {
- if (!startTemplateAndGotoChild(templateToken, finalI)) {
- return false;
- }
- templateToken = null;
- }
- else if (sign == '*') {
- myState = MyState.NUMBER;
- }
- }
- else {
- fail();
- }
- }
- break;
- case WORD:
- if (token instanceof MyTemplateToken) {
- templateToken = ((MyTemplateToken)token);
- myState = MyState.OPERATION;
- }
- else {
- fail();
- }
- break;
- case NUMBER:
- if (token instanceof MyNumberToken) {
- number = ((MyNumberToken)token).myNumber;
- myState = MyState.AFTER_NUMBER;
- }
- else {
- fail();
- }
- break;
- case AFTER_NUMBER:
- if (token instanceof MyMarkerToken || token instanceof MyOperationToken) {
- char sign = token instanceof MyOperationToken ? ((MyOperationToken)token).mySign : MARKER;
- if (sign == MARKER || sign == '+') {
- if (!invokeTemplateSeveralTimes(templateToken, 0, number, finalI)) {
- return false;
- }
- templateToken = null;
- }
- else if (number > 1) {
- return invokeTemplateAndProcessTail(templateToken, 0, number, i + 1);
- }
- else {
- assert number == 1;
- if (!startTemplateAndGotoChild(templateToken, finalI)) {
- return false;
- }
- templateToken = null;
- }
- myState = MyState.WORD;
- }
- else {
- fail();
- }
- break;
- }
- }
- finish(startIndex == n);
- return true;
- }
-
- private boolean startTemplateAndGotoChild(MyTemplateToken templateToken, final int index) {
- final Object key = new Object();
- myCallback.fixStartOfTemplate(key);
- TemplateInvokationListener listener = new TemplateInvokationListener() {
- public void finished(boolean inSeparateEvent) {
- myState = MyState.WORD;
- gotoChild(key);
- if (inSeparateEvent) {
- invoke(index + 1);
- }
- }
- };
- if (!invokeTemplate(templateToken, myCallback, listener, 0)) {
- return false;
- }
- return true;
- }
-
- private boolean invokeTemplateSeveralTimes(final MyTemplateToken templateToken,
- final int startIndex,
- final int count,
- final int globalIndex) {
- final Object key = new Object();
- myCallback.fixStartOfTemplate(key);
- for (int i = startIndex; i < count; i++) {
- final int finalI = i;
- TemplateInvokationListener listener = new TemplateInvokationListener() {
- public void finished(boolean inSeparateEvent) {
- myState = MyState.WORD;
- if (myCallback.getOffset() != myCallback.getEndOfTemplate(key)) {
- myCallback.fixEndOffset();
- }
- myCallback.gotoEndOfTemplate(key);
- if (inSeparateEvent) {
- if (finalI + 1 < count) {
- invokeTemplateSeveralTimes(templateToken, finalI + 1, count, globalIndex);
- }
- else {
- invoke(globalIndex + 1);
- }
- }
- }
- };
- if (!invokeTemplate(templateToken, myCallback, listener, i)) {
- return false;
- }
- }
- return true;
- }
-
- private boolean invokeTemplateAndProcessTail(final MyTemplateToken templateToken,
- final int startIndex,
- final int count,
- final int tailStart) {
- final Object key = new Object();
- myCallback.fixStartOfTemplate(key);
- for (int i = startIndex; i < count; i++) {
- final int finalI = i;
- final boolean[] flag = new boolean[]{false};
- TemplateInvokationListener listener = new TemplateInvokationListener() {
- public void finished(boolean inSeparateEvent) {
- gotoChild(key);
- MyInterpreter interpreter = new MyInterpreter(myTokens, myCallback, MyState.WORD, new TemplateInvokationListener() {
- public void finished(boolean inSeparateEvent) {
- if (myCallback.getOffset() != myCallback.getEndOfTemplate(key)) {
- myCallback.fixEndOffset();
- }
- myCallback.gotoEndOfTemplate(key);
- if (inSeparateEvent) {
- invokeTemplateAndProcessTail(templateToken, finalI + 1, count, tailStart);
- }
- }
- });
- if (interpreter.invoke(tailStart)) {
- if (inSeparateEvent) {
- invokeTemplateAndProcessTail(templateToken, finalI + 1, count, tailStart);
- }
- }
- else {
- flag[0] = true;
- }
- }
- };
- if (!invokeTemplate(templateToken, myCallback, listener, i) || flag[0]) {
- return false;
- }
- }
- finish(count == 0);
- return true;
- }
- }
-
-}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright 2000-2010 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.codeInsight.template.zencoding;
+
+/**
+ * @author Eugene.Kudelevsky
+ */
+class MarkerToken extends Token {
+}
--- /dev/null
+/*
+ * Copyright 2000-2010 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.codeInsight.template.zencoding;
+
+/**
+ * @author Eugene.Kudelevsky
+ */
+class NumberToken extends Token {
+ final int myNumber;
+
+ NumberToken(int number) {
+ myNumber = number;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2000-2010 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.codeInsight.template.zencoding;
+
+/**
+ * @author Eugene.Kudelevsky
+ */
+class OperationToken extends Token {
+ final char mySign;
+
+ OperationToken(char sign) {
+ mySign = sign;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2000-2010 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.codeInsight.template.zencoding;
+
+/**
+ * @author Eugene.Kudelevsky
+ */
+enum State {
+ OPERATION, WORD, AFTER_NUMBER, NUMBER
+}
--- /dev/null
+/*
+ * Copyright 2000-2010 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.codeInsight.template.zencoding;
+
+import com.intellij.codeInsight.template.impl.TemplateImpl;
+import com.intellij.openapi.util.Pair;
+
+import java.util.List;
+
+/**
+ * @author Eugene.Kudelevsky
+ */
+class TemplateToken extends Token {
+ final String myKey;
+ final List<Pair<String, String>> myAttribute2Value;
+ TemplateImpl myTemplate;
+
+ TemplateToken(String key, List<Pair<String, String>> attribute2value) {
+ myKey = key;
+ myAttribute2Value = attribute2value;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2000-2010 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.codeInsight.template.zencoding;
+
+/**
+ * @author Eugene.Kudelevsky
+ */
+class Token {
+}
--- /dev/null
+/*
+ * Copyright 2000-2010 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.codeInsight.template.zencoding;
+
+import com.intellij.codeInsight.template.CustomTemplateCallback;
+import com.intellij.codeInsight.template.TemplateInvokationListener;
+import com.intellij.codeInsight.template.impl.TemplateImpl;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.psi.xml.*;
+import com.intellij.util.containers.HashMap;
+import com.intellij.util.containers.HashSet;
+import com.intellij.util.containers.IntArrayList;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+
+/**
+ * @author Eugene.Kudelevsky
+ */
+class XmlZenCodingInterpreter {
+ private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.template.zencoding.XmlZenCodingInterpreter");
+ private static final String ATTRS = "ATTRS";
+
+ private final List<Token> myTokens;
+ private final CustomTemplateCallback myCallback;
+ private final TemplateInvokationListener myListener;
+ private static final String NUMBER_IN_ITERATION_PLACE_HOLDER = "$";
+ private State myState;
+
+ XmlZenCodingInterpreter(List<Token> tokens,
+ CustomTemplateCallback callback,
+ State initialState,
+ TemplateInvokationListener listener) {
+ myTokens = tokens;
+ myCallback = callback;
+ myListener = listener;
+ myState = initialState;
+ }
+
+ private void finish(boolean inSeparateEvent) {
+ myCallback.gotoEndOffset();
+ if (myListener != null) {
+ myListener.finished(inSeparateEvent);
+ }
+ }
+
+ private void gotoChild(Object templateBoundsKey) {
+ int startOfTemplate = myCallback.getStartOfTemplate(templateBoundsKey);
+ int endOfTemplate = myCallback.getEndOfTemplate(templateBoundsKey);
+ Editor editor = myCallback.getEditor();
+ int offset = myCallback.getOffset();
+
+ PsiFile file = myCallback.getFile();
+
+ PsiElement element = file.findElementAt(offset);
+ if (element instanceof XmlToken && ((XmlToken)element).getTokenType() == XmlTokenType.XML_END_TAG_START) {
+ return;
+ }
+
+ int newOffset = -1;
+ XmlTag tag = PsiTreeUtil.findElementOfClassAtRange(file, startOfTemplate, endOfTemplate, XmlTag.class);
+ if (tag != null) {
+ for (PsiElement child : tag.getChildren()) {
+ if (child instanceof XmlToken && ((XmlToken)child).getTokenType() == XmlTokenType.XML_END_TAG_START) {
+ newOffset = child.getTextOffset();
+ }
+ }
+ }
+
+ if (newOffset >= 0) {
+ myCallback.fixEndOffset();
+ editor.getCaretModel().moveToOffset(newOffset);
+ }
+
+ /*CharSequence tagName = getPrecedingTagName(text, offset, startOfTemplate);
+ if (tagName != null) {
+ *//*if (!hasClosingTag(text, tagName, offset, endOfTemplate)) {
+ document.insertString(offset, "</" + tagName + '>');
+ }*//*
+ }
+ else if (offset != endOfTemplate) {
+ tagName = getPrecedingTagName(text, endOfTemplate, startOfTemplate);
+ if (tagName != null) {
+ *//*fixEndOffset();
+ document.insertString(endOfTemplate, "</" + tagName + '>');*//*
+ editor.getCaretModel().moveToOffset(endOfTemplate);
+ }
+ }*/
+ }
+
+ public boolean invoke(int startIndex) {
+ final int n = myTokens.size();
+ TemplateToken templateToken = null;
+ int number = -1;
+ for (int i = startIndex; i < n; i++) {
+ final int finalI = i;
+ Token token = myTokens.get(i);
+ switch (myState) {
+ case OPERATION:
+ if (templateToken != null) {
+ if (token instanceof MarkerToken || token instanceof OperationToken) {
+ final char sign = token instanceof OperationToken ? ((OperationToken)token).mySign : XmlZenCodingTemplate.MARKER;
+ if (sign == XmlZenCodingTemplate.MARKER || sign == '+') {
+ final Object key = new Object();
+ myCallback.fixStartOfTemplate(key);
+ TemplateInvokationListener listener = new TemplateInvokationListener() {
+ public void finished(boolean inSeparateEvent) {
+ myState = State.WORD;
+ if (myCallback.getOffset() != myCallback.getEndOfTemplate(key)) {
+ myCallback.fixEndOffset();
+ }
+ if (sign == '+') {
+ myCallback.gotoEndOfTemplate(key);
+ }
+ if (inSeparateEvent) {
+ invoke(finalI + 1);
+ }
+ }
+ };
+ if (!invokeTemplate(templateToken, myCallback, listener, 0)) {
+ return false;
+ }
+ templateToken = null;
+ }
+ else if (sign == '>') {
+ if (!startTemplateAndGotoChild(templateToken, finalI)) {
+ return false;
+ }
+ templateToken = null;
+ }
+ else if (sign == '*') {
+ myState = State.NUMBER;
+ }
+ }
+ else {
+ fail();
+ }
+ }
+ break;
+ case WORD:
+ if (token instanceof TemplateToken) {
+ templateToken = ((TemplateToken)token);
+ myState = State.OPERATION;
+ }
+ else {
+ fail();
+ }
+ break;
+ case NUMBER:
+ if (token instanceof NumberToken) {
+ number = ((NumberToken)token).myNumber;
+ myState = State.AFTER_NUMBER;
+ }
+ else {
+ fail();
+ }
+ break;
+ case AFTER_NUMBER:
+ if (token instanceof MarkerToken || token instanceof OperationToken) {
+ char sign = token instanceof OperationToken ? ((OperationToken)token).mySign : XmlZenCodingTemplate.MARKER;
+ if (sign == XmlZenCodingTemplate.MARKER || sign == '+') {
+ if (!invokeTemplateSeveralTimes(templateToken, 0, number, finalI)) {
+ return false;
+ }
+ templateToken = null;
+ }
+ else if (number > 1) {
+ return invokeTemplateAndProcessTail(templateToken, 0, number, i + 1);
+ }
+ else {
+ assert number == 1;
+ if (!startTemplateAndGotoChild(templateToken, finalI)) {
+ return false;
+ }
+ templateToken = null;
+ }
+ myState = State.WORD;
+ }
+ else {
+ fail();
+ }
+ break;
+ }
+ }
+ finish(startIndex == n);
+ return true;
+ }
+
+ private boolean startTemplateAndGotoChild(TemplateToken templateToken, final int index) {
+ final Object key = new Object();
+ myCallback.fixStartOfTemplate(key);
+ TemplateInvokationListener listener = new TemplateInvokationListener() {
+ public void finished(boolean inSeparateEvent) {
+ myState = State.WORD;
+ gotoChild(key);
+ if (inSeparateEvent) {
+ invoke(index + 1);
+ }
+ }
+ };
+ if (!invokeTemplate(templateToken, myCallback, listener, 0)) {
+ return false;
+ }
+ return true;
+ }
+
+ private boolean invokeTemplateSeveralTimes(final TemplateToken templateToken,
+ final int startIndex,
+ final int count,
+ final int globalIndex) {
+ final Object key = new Object();
+ myCallback.fixStartOfTemplate(key);
+ for (int i = startIndex; i < count; i++) {
+ final int finalI = i;
+ TemplateInvokationListener listener = new TemplateInvokationListener() {
+ public void finished(boolean inSeparateEvent) {
+ myState = State.WORD;
+ if (myCallback.getOffset() != myCallback.getEndOfTemplate(key)) {
+ myCallback.fixEndOffset();
+ }
+ myCallback.gotoEndOfTemplate(key);
+ if (inSeparateEvent) {
+ if (finalI + 1 < count) {
+ invokeTemplateSeveralTimes(templateToken, finalI + 1, count, globalIndex);
+ }
+ else {
+ invoke(globalIndex + 1);
+ }
+ }
+ }
+ };
+ if (!invokeTemplate(templateToken, myCallback, listener, i)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean invokeTemplateAndProcessTail(final TemplateToken templateToken,
+ final int startIndex,
+ final int count,
+ final int tailStart) {
+ final Object key = new Object();
+ myCallback.fixStartOfTemplate(key);
+ for (int i = startIndex; i < count; i++) {
+ final int finalI = i;
+ final boolean[] flag = new boolean[]{false};
+ TemplateInvokationListener listener = new TemplateInvokationListener() {
+ public void finished(boolean inSeparateEvent) {
+ gotoChild(key);
+ XmlZenCodingInterpreter interpreter =
+ new XmlZenCodingInterpreter(myTokens, myCallback, State.WORD, new TemplateInvokationListener() {
+ public void finished(boolean inSeparateEvent) {
+ if (myCallback.getOffset() != myCallback.getEndOfTemplate(key)) {
+ myCallback.fixEndOffset();
+ }
+ myCallback.gotoEndOfTemplate(key);
+ if (inSeparateEvent) {
+ invokeTemplateAndProcessTail(templateToken, finalI + 1, count, tailStart);
+ }
+ }
+ });
+ if (interpreter.invoke(tailStart)) {
+ if (inSeparateEvent) {
+ invokeTemplateAndProcessTail(templateToken, finalI + 1, count, tailStart);
+ }
+ }
+ else {
+ flag[0] = true;
+ }
+ }
+ };
+ if (!invokeTemplate(templateToken, myCallback, listener, i) || flag[0]) {
+ return false;
+ }
+ }
+ finish(count == 0);
+ return true;
+ }
+
+ private static boolean containsAttrsVar(TemplateImpl template) {
+ for (int i = 0; i < template.getVariableCount(); i++) {
+ String varName = template.getVariableNameAt(i);
+ if (ATTRS.equals(varName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static void removeVariablesWhichHasNoSegment(TemplateImpl template) {
+ Set<String> segments = new HashSet<String>();
+ for (int i = 0; i < template.getSegmentsCount(); i++) {
+ segments.add(template.getSegmentName(i));
+ }
+ IntArrayList varsToRemove = new IntArrayList();
+ for (int i = 0; i < template.getVariableCount(); i++) {
+ String varName = template.getVariableNameAt(i);
+ if (!segments.contains(varName)) {
+ varsToRemove.add(i);
+ }
+ }
+ for (int i = 0; i < varsToRemove.size(); i++) {
+ template.removeVariable(varsToRemove.get(i));
+ }
+ }
+
+ @Nullable
+ private static Map<String, String> buildPredefinedValues(List<Pair<String, String>> attribute2value, int numberInIteration) {
+ StringBuilder result = new StringBuilder();
+ for (Iterator<Pair<String, String>> it = attribute2value.iterator(); it.hasNext();) {
+ Pair<String, String> pair = it.next();
+ String name = pair.first;
+ String value = getValue(pair, numberInIteration);
+ result.append(name).append("=\"").append(value).append('"');
+ if (it.hasNext()) {
+ result.append(' ');
+ }
+ }
+ String attributes = result.toString();
+ attributes = attributes.length() > 0 ? ' ' + attributes : null;
+ Map<String, String> predefinedValues = null;
+ if (attributes != null) {
+ predefinedValues = new HashMap<String, String>();
+ predefinedValues.put(ATTRS, attributes);
+ }
+ return predefinedValues;
+ }
+
+ private static String getValue(Pair<String, String> pair, int numberInIteration) {
+ return pair.second.replace(NUMBER_IN_ITERATION_PLACE_HOLDER, Integer.toString(numberInIteration + 1));
+ }
+
+ @Nullable
+ private static String addAttrsVar(TemplateImpl modifiedTemplate, XmlTag tag) {
+ String text = tag.getContainingFile().getText();
+ PsiElement[] children = tag.getChildren();
+ if (children.length >= 1 &&
+ children[0] instanceof XmlToken &&
+ ((XmlToken)children[0]).getTokenType() == XmlTokenType.XML_START_TAG_START) {
+ PsiElement beforeAttrs = children[0];
+ if (children.length >= 2 && children[1] instanceof XmlToken && ((XmlToken)children[1]).getTokenType() == XmlTokenType.XML_NAME) {
+ beforeAttrs = children[1];
+ }
+ TextRange range = beforeAttrs.getTextRange();
+ if (range == null) {
+ return null;
+ }
+ int offset = range.getEndOffset();
+ text = text.substring(0, offset) + " $ATTRS$" + text.substring(offset);
+ modifiedTemplate.addVariable(ATTRS, "", "", false);
+ return text;
+ }
+ return null;
+ }
+
+ private static boolean invokeTemplate(TemplateToken token,
+ final CustomTemplateCallback callback,
+ final TemplateInvokationListener listener,
+ int numberInIteration) {
+ List<Pair<String, String>> attr2value = new ArrayList<Pair<String, String>>(token.myAttribute2Value);
+ if (callback.isLiveTemplateApplicable(token.myKey)) {
+ if (token.myTemplate != null) {
+ TemplateImpl modifiedTemplate = token.myTemplate.copy();
+ XmlTag tag = XmlZenCodingTemplate.parseXmlTagInTemplate(token.myTemplate.getString(), callback.getProject());
+ assert tag != null;
+ for (Iterator<Pair<String, String>> iterator = attr2value.iterator(); iterator.hasNext();) {
+ Pair<String, String> pair = iterator.next();
+ if (tag.getAttribute(pair.first) != null) {
+ tag.setAttribute(pair.first, getValue(pair, numberInIteration));
+ iterator.remove();
+ }
+ }
+ String text = null;
+ if (!containsAttrsVar(modifiedTemplate) && attr2value.size() > 0) {
+ String textWithAttrs = addAttrsVar(modifiedTemplate, tag);
+ if (textWithAttrs != null) {
+ text = textWithAttrs;
+ }
+ else {
+ for (Iterator<Pair<String, String>> iterator = attr2value.iterator(); iterator.hasNext();) {
+ Pair<String, String> pair = iterator.next();
+ tag.setAttribute(pair.first, getValue(pair, numberInIteration));
+ iterator.remove();
+ }
+ }
+ }
+ if (text == null) {
+ text = tag.getContainingFile().getText();
+ }
+ modifiedTemplate.setString(text);
+ removeVariablesWhichHasNoSegment(modifiedTemplate);
+ Map<String, String> predefinedValues = buildPredefinedValues(attr2value, numberInIteration);
+ return callback.startTemplate(modifiedTemplate, predefinedValues, listener);
+ }
+ else {
+ Map<String, String> predefinedValues = buildPredefinedValues(attr2value, numberInIteration);
+ return callback.startTemplate(token.myKey, predefinedValues, listener);
+ }
+ }
+ else {
+ TemplateImpl template = new TemplateImpl("", "");
+ template.addTextSegment('<' + token.myKey);
+ if (attr2value.size() > 0) {
+ template.addVariable(ATTRS, "", "", false);
+ template.addVariableSegment(ATTRS);
+ }
+ template.addTextSegment(">");
+ template.addVariableSegment(TemplateImpl.END);
+ template.addTextSegment("</" + token.myKey + ">");
+ template.setToReformat(true);
+ Map<String, String> predefinedValues = buildPredefinedValues(attr2value, numberInIteration);
+ return callback.startTemplate(template, predefinedValues, listener);
+ }
+ }
+
+ private static void fail() {
+ LOG.error("Input string was checked incorrectly during isApplicable() invokation");
+ }
+}
--- /dev/null
+/*
+ * Copyright 2000-2010 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.codeInsight.template.zencoding;
+
+import com.intellij.application.options.editor.WebEditorOptions;
+import com.intellij.codeInsight.template.CustomLiveTemplate;
+import com.intellij.codeInsight.template.CustomTemplateCallback;
+import com.intellij.codeInsight.template.TemplateInvokationListener;
+import com.intellij.codeInsight.template.impl.TemplateImpl;
+import com.intellij.lang.xml.XMLLanguage;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Pair;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiFileFactory;
+import com.intellij.psi.xml.*;
+import com.intellij.util.containers.HashSet;
+import org.apache.xerces.util.XML11Char;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+
+/**
+ * @author Eugene.Kudelevsky
+ */
+public class XmlZenCodingTemplate implements CustomLiveTemplate {
+ static final char MARKER = '$';
+ private static final String OPERATIONS = ">+*";
+ private static final String SELECTORS = ".#[";
+ private static final String ID = "id";
+ private static final String CLASS = "class";
+
+ private static int parseNonNegativeInt(@NotNull String s) {
+ try {
+ return Integer.parseInt(s);
+ }
+ catch (Throwable ignored) {
+ }
+ return -1;
+ }
+
+ private static String getPrefix(@NotNull String templateKey) {
+ for (int i = 0, n = templateKey.length(); i < n; i++) {
+ char c = templateKey.charAt(i);
+ if (SELECTORS.indexOf(c) >= 0) {
+ return templateKey.substring(0, i);
+ }
+ }
+ return templateKey;
+ }
+
+ @Nullable
+ private static Pair<String, String> parseAttrNameAndValue(@NotNull String text) {
+ int eqIndex = text.indexOf('=');
+ if (eqIndex > 0) {
+ return new Pair<String, String>(text.substring(0, eqIndex), text.substring(eqIndex + 1));
+ }
+ return null;
+ }
+
+ @Nullable
+ private static TemplateToken parseSelectors(@NotNull String text) {
+ String templateKey = null;
+ List<Pair<String, String>> attributes = new ArrayList<Pair<String, String>>();
+ Set<String> definedAttrs = new HashSet<String>();
+ final List<String> classes = new ArrayList<String>();
+ StringBuilder builder = new StringBuilder();
+ char lastDelim = 0;
+ text += MARKER;
+ int classAttrPosition = -1;
+ for (int i = 0, n = text.length(); i < n; i++) {
+ char c = text.charAt(i);
+ if (c == '#' || c == '.' || c == '[' || c == ']' || i == n - 1) {
+ if (c != ']') {
+ switch (lastDelim) {
+ case 0:
+ templateKey = builder.toString();
+ break;
+ case '#':
+ if (!definedAttrs.add(ID)) {
+ return null;
+ }
+ attributes.add(new Pair<String, String>(ID, builder.toString()));
+ break;
+ case '.':
+ if (builder.length() <= 0) {
+ return null;
+ }
+ if (classAttrPosition < 0) {
+ classAttrPosition = attributes.size();
+ }
+ classes.add(builder.toString());
+ break;
+ case ']':
+ if (builder.length() > 0) {
+ return null;
+ }
+ break;
+ default:
+ return null;
+ }
+ }
+ else if (lastDelim != '[') {
+ return null;
+ }
+ else {
+ Pair<String, String> pair = parseAttrNameAndValue(builder.toString());
+ if (pair == null || !definedAttrs.add(pair.first)) {
+ return null;
+ }
+ attributes.add(pair);
+ }
+ lastDelim = c;
+ builder = new StringBuilder();
+ }
+ else {
+ builder.append(c);
+ }
+ }
+ if (classes.size() > 0) {
+ if (definedAttrs.contains(CLASS)) {
+ return null;
+ }
+ StringBuilder classesAttrValue = new StringBuilder();
+ for (int i = 0; i < classes.size(); i++) {
+ classesAttrValue.append(classes.get(i));
+ if (i < classes.size() - 1) {
+ classesAttrValue.append(' ');
+ }
+ }
+ assert classAttrPosition >= 0;
+ attributes.add(classAttrPosition, new Pair<String, String>(CLASS, classesAttrValue.toString()));
+ }
+ return new TemplateToken(templateKey, attributes);
+ }
+
+ private static boolean isXML11ValidQName(String str) {
+ final int colon = str.indexOf(':');
+ if (colon == 0 || colon == str.length() - 1) {
+ return false;
+ }
+ if (colon > 0) {
+ final String prefix = str.substring(0, colon);
+ final String localPart = str.substring(colon + 1);
+ return XML11Char.isXML11ValidNCName(prefix) && XML11Char.isXML11ValidNCName(localPart);
+ }
+ return XML11Char.isXML11ValidNCName(str);
+ }
+
+ private static boolean generateTemplateAndAddToToken(TemplateToken token, CustomTemplateCallback callback) {
+ TemplateImpl template = callback.findApplicableTemplate(token.myKey);
+ assert template != null;
+ XmlTag tag = parseXmlTagInTemplate(template.getString(), callback.getProject());
+ if (tag == null) {
+ return false;
+ }
+ token.myTemplate = template;
+ return true;
+ }
+
+ @Nullable
+ private static List<Token> parse(@NotNull String text, @NotNull CustomTemplateCallback callback) {
+ text += MARKER;
+ StringBuilder templateKeyBuilder = new StringBuilder();
+ List<Token> result = new ArrayList<Token>();
+ for (int i = 0, n = text.length(); i < n; i++) {
+ char c = text.charAt(i);
+ if (i == n - 1 || (i < n - 2 && OPERATIONS.indexOf(c) >= 0)) {
+ String key = templateKeyBuilder.toString();
+ templateKeyBuilder = new StringBuilder();
+ int num = parseNonNegativeInt(key);
+ if (num > 0) {
+ result.add(new NumberToken(num));
+ }
+ else {
+ if (key.length() == 0) {
+ return null;
+ }
+ String prefix = getPrefix(key);
+ boolean applicable = callback.isLiveTemplateApplicable(prefix);
+ if (!applicable && !isXML11ValidQName(prefix)) {
+ return null;
+ }
+ TemplateToken token = parseSelectors(key);
+ if (token == null) {
+ return null;
+ }
+ if (applicable && token.myAttribute2Value.size() > 0) {
+ assert prefix.equals(token.myKey);
+ if (!generateTemplateAndAddToToken(token, callback)) {
+ return null;
+ }
+ }
+ result.add(token);
+ }
+ result.add(i < n - 1 ? new OperationToken(c) : new MarkerToken());
+ }
+ else if (!Character.isWhitespace(c)) {
+ templateKeyBuilder.append(c);
+ }
+ else {
+ return null;
+ }
+ }
+ return result;
+ }
+
+ private static boolean check(@NotNull Collection<Token> tokens) {
+ State state = State.WORD;
+ for (Token token : tokens) {
+ if (token instanceof MarkerToken) {
+ break;
+ }
+ switch (state) {
+ case OPERATION:
+ if (token instanceof OperationToken) {
+ state = ((OperationToken)token).mySign == '*' ? State.NUMBER : State.WORD;
+ }
+ else {
+ return false;
+ }
+ break;
+ case WORD:
+ if (token instanceof TemplateToken) {
+ state = State.OPERATION;
+ }
+ else {
+ return false;
+ }
+ break;
+ case NUMBER:
+ if (token instanceof NumberToken) {
+ state = State.AFTER_NUMBER;
+ }
+ else {
+ return false;
+ }
+ break;
+ case AFTER_NUMBER:
+ if (token instanceof OperationToken && ((OperationToken)token).mySign != '*') {
+ state = State.WORD;
+ }
+ else {
+ return false;
+ }
+ break;
+ }
+ }
+ return state == State.OPERATION || state == State.AFTER_NUMBER;
+ }
+
+ private static String computeKey(Editor editor, int startOffset) {
+ int offset = editor.getCaretModel().getOffset();
+ String s = editor.getDocument().getCharsSequence().subSequence(startOffset, offset).toString();
+ int index = 0;
+ while (index < s.length() && Character.isWhitespace(s.charAt(index))) {
+ index++;
+ }
+ String key = s.substring(index);
+ int lastWhitespaceIndex = -1;
+ for (int i = 0; i < key.length(); i++) {
+ if (Character.isWhitespace(key.charAt(i))) {
+ lastWhitespaceIndex = i;
+ }
+ }
+ if (lastWhitespaceIndex >= 0 && lastWhitespaceIndex < key.length() - 1) {
+ return key.substring(lastWhitespaceIndex + 1);
+ }
+ return key;
+ }
+
+ public String computeTemplateKey(@NotNull CustomTemplateCallback callback) {
+ Editor editor = callback.getEditor();
+ int offset = callback.getOffset();
+ PsiElement element = callback.getFile().findElementAt(offset > 0 ? offset - 1 : offset);
+ int line = editor.getCaretModel().getLogicalPosition().line;
+ int lineStart = editor.getDocument().getLineStartOffset(line);
+ int parentStart;
+ do {
+ parentStart = element != null ? element.getTextRange().getStartOffset() : 0;
+ int startOffset = parentStart > lineStart ? parentStart : lineStart;
+ String key = computeKey(editor, startOffset);
+ List<Token> tokens = parse(key, callback);
+ if (tokens != null && check(tokens)) {
+ if (tokens.size() == 2) {
+ Token token = tokens.get(0);
+ if (token instanceof TemplateToken) {
+ if (key.equals(((TemplateToken)token).myKey) && callback.isLiveTemplateApplicable(key)) {
+ // do not activate only live template
+ return null;
+ }
+ }
+ }
+ return key;
+ }
+ if (element != null) {
+ element = element.getParent();
+ }
+ }
+ while (element != null && parentStart > lineStart);
+ return null;
+ }
+
+ public boolean isApplicable(PsiFile file, int offset, boolean selection) {
+ WebEditorOptions webEditorOptions = WebEditorOptions.getInstance();
+ if (!webEditorOptions.isZenCodingEnabled()) {
+ return false;
+ }
+ if (file.getLanguage() instanceof XMLLanguage) {
+ PsiElement element = file.findElementAt(offset > 0 ? offset - 1 : offset);
+ if (element == null || element.getLanguage() instanceof XMLLanguage) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void expand(String key, @NotNull CustomTemplateCallback callback, @Nullable TemplateInvokationListener listener) {
+ List<Token> tokens = parse(key, callback);
+ assert tokens != null;
+ XmlZenCodingInterpreter interpreter = new XmlZenCodingInterpreter(tokens, callback, State.WORD, listener);
+ interpreter.invoke(0);
+ }
+
+ public void wrap(String selection, @NotNull CustomTemplateCallback callback, @Nullable TemplateInvokationListener listener) {
+ }
+
+ @Nullable
+ static XmlTag parseXmlTagInTemplate(String templateString, Project project) {
+ XmlFile xmlFile = (XmlFile)PsiFileFactory.getInstance(project).createFileFromText("dummy.xml", templateString);
+ XmlDocument document = xmlFile.getDocument();
+ return document == null ? null : document.getRootTag();
+ }
+}
\ No newline at end of file