refactoring, support css zen-coding selectors
authorEugene Kudelevsky <Eugene.Kudelevsky@jetbrains.com>
Thu, 22 Apr 2010 16:49:09 +0000 (20:49 +0400)
committerEugene Kudelevsky <Eugene.Kudelevsky@jetbrains.com>
Thu, 22 Apr 2010 16:49:09 +0000 (20:49 +0400)
platform/lang-impl/src/com/intellij/codeInsight/template/CustomLiveTemplate.java
platform/lang-impl/src/com/intellij/codeInsight/template/impl/SurroundWithTemplateHandler.java
platform/lang-impl/src/com/intellij/codeInsight/template/impl/TemplateManagerImpl.java
xml/impl/src/com/intellij/codeInsight/template/zencoding/XmlZenCodingInterpreter.java
xml/impl/src/com/intellij/codeInsight/template/zencoding/XmlZenCodingTemplate.java
xml/impl/src/com/intellij/codeInsight/template/zencoding/ZenCodingTemplate.java [new file with mode: 0644]

index c3e9748382e3c7839cab2590f9293978882f07fa..f57b56b9c723e5de88e5e8ca58fcbd3845999e48 100644 (file)
@@ -29,7 +29,9 @@ public interface CustomLiveTemplate {
   @Nullable
   String computeTemplateKey(@NotNull CustomTemplateCallback callback);
 
-  boolean isApplicable(PsiFile file, int offset, boolean selection);
+  boolean isApplicable(PsiFile file, int offset);
+
+  boolean supportsWrapping();
 
   void expand(String key, @NotNull CustomTemplateCallback callback);
 
index c6de17b41d824bedcf0dd97a7690dd13e153047f..329faae575a219a726bfc334105dd8a9fee3de8d 100644 (file)
@@ -77,7 +77,7 @@ public class SurroundWithTemplateHandler implements CodeInsightActionHandler {
     List<CustomLiveTemplate> result = new ArrayList<CustomLiveTemplate>();
     int offset = editor.getCaretModel().getOffset();
     for (CustomLiveTemplate template : CustomLiveTemplate.EP_NAME.getExtensions()) {
-      if (template.isApplicable(file, offset, true)) {
+      if (template.supportsWrapping() && template.isApplicable(file, offset)) {
         result.add(template);
       }
     }
index eec5cef1d3435f05ef374c3e16cfe504faf6a329..14f0c7c45e76d6183c2f2aed968443bca5ecdd08 100644 (file)
@@ -244,7 +244,7 @@ public class TemplateManagerImpl extends TemplateManager implements ProjectCompo
     for (final CustomLiveTemplate customLiveTemplate : CustomLiveTemplate.EP_NAME.getExtensions()) {
       if (shortcutChar == customLiveTemplate.getShortcut()) {
         int caretOffset = editor.getCaretModel().getOffset();
-        if (customLiveTemplate.isApplicable(file, caretOffset, false)) {
+        if (customLiveTemplate.isApplicable(file, caretOffset)) {
           final CustomTemplateCallback callback = new CustomTemplateCallback(editor, file);
           String key = customLiveTemplate.computeTemplateKey(callback);
           if (key != null) {
index 42f4e112375ae0715e52e3716a13b730cf4a083d..2f74ada15052fae32c88a3034323e642128bb2bb 100644 (file)
@@ -121,8 +121,8 @@ class XmlZenCodingInterpreter {
         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 == '+' || (mySurroundedText == null && sign == XmlZenCodingTemplate.MARKER)) {
+              final char sign = token instanceof OperationToken ? ((OperationToken)token).mySign : ZenCodingTemplate.MARKER;
+              if (sign == '+' || (mySurroundedText == null && sign == ZenCodingTemplate.MARKER)) {
                 final Object key = new Object();
                 myCallback.fixStartOfTemplate(key);
                 invokeTemplate(templateToken, myCallback, 0);
@@ -135,7 +135,7 @@ class XmlZenCodingInterpreter {
                 }
                 templateToken = null;
               }
-              else if (sign == '>' || (mySurroundedText != null && sign == XmlZenCodingTemplate.MARKER)) {
+              else if (sign == '>' || (mySurroundedText != null && sign == ZenCodingTemplate.MARKER)) {
                 startTemplateAndGotoChild(templateToken);
                 templateToken = null;
               }
@@ -168,8 +168,8 @@ class XmlZenCodingInterpreter {
           break;
         case AFTER_NUMBER:
           if (token instanceof MarkerToken || token instanceof OperationToken) {
-            char sign = token instanceof OperationToken ? ((OperationToken)token).mySign : XmlZenCodingTemplate.MARKER;
-            if (sign == '+' || (mySurroundedText == null && sign == XmlZenCodingTemplate.MARKER)) {
+            char sign = token instanceof OperationToken ? ((OperationToken)token).mySign : ZenCodingTemplate.MARKER;
+            if (sign == '+' || (mySurroundedText == null && sign == ZenCodingTemplate.MARKER)) {
               invokeTemplateSeveralTimes(templateToken, 0, number);
               templateToken = null;
             }
index 8ea46618e0cf3e269f6e9e86d295706172773502..7f6b294d6b8a726a55c305ae007b6a2beb50192e 100644 (file)
  */
 package com.intellij.codeInsight.template.zencoding;
 
-import com.intellij.application.options.editor.WebEditorOptions;
-import com.intellij.codeInsight.CodeInsightBundle;
 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.injection.InjectedLanguageManager;
 import com.intellij.lang.xml.XMLLanguage;
-import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.command.CommandProcessor;
 import com.intellij.openapi.editor.Editor;
-import com.intellij.openapi.editor.EditorModificationUtil;
 import com.intellij.openapi.fileTypes.FileType;
 import com.intellij.openapi.fileTypes.StdFileTypes;
-import com.intellij.openapi.ui.InputValidatorEx;
-import com.intellij.openapi.ui.Messages;
 import com.intellij.openapi.util.Pair;
-import com.intellij.psi.PsiDocumentManager;
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiFile;
 import com.intellij.psi.PsiFileFactory;
-import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
 import com.intellij.psi.util.PsiTreeUtil;
 import com.intellij.psi.xml.*;
 import com.intellij.util.LocalTimeCounter;
 import com.intellij.util.containers.HashSet;
-import com.intellij.xml.XmlBundle;
 import org.apache.xerces.util.XML11Char;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
 import java.util.Set;
 
 /**
  * @author Eugene.Kudelevsky
  */
-public class XmlZenCodingTemplate implements CustomLiveTemplate {
-  static final char MARKER = '$';
-  private static final String OPERATIONS = ">+*";
+public class XmlZenCodingTemplate extends ZenCodingTemplate {
   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);
@@ -183,187 +160,40 @@ public class XmlZenCodingTemplate implements CustomLiveTemplate {
     return type == StdFileTypes.XHTML || type == StdFileTypes.JSPX || type == StdFileTypes.XML;
   }
 
+  @Override
   @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);
-          TemplateImpl template = callback.findApplicableTemplate(prefix);
-          if (template == null && !isXML11ValidQName(prefix)) {
-            return null;
-          }
-          TemplateToken token = parseSelectors(key);
-          if (token == null) {
-            return null;
-          }
-          if (template != null && (token.myAttribute2Value.size() > 0 || isTrueXml(callback))) {
-            assert prefix.equals(token.myKey);
-            token.myTemplate = template;
-            if (token.myAttribute2Value.size() > 0) {
-              XmlTag tag = parseXmlTagInTemplate(template.getString(), callback, false);
-              if (tag == null) {
-                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;
-      }
+  protected TemplateToken parseTemplateKey(String key, CustomTemplateCallback callback) {
+    String prefix = getPrefix(key);
+    TemplateImpl template = callback.findApplicableTemplate(prefix);
+    if (template == null && !isXML11ValidQName(prefix)) {
+      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) {
-    return computeKey(callback);
-  }
-
-  static String computeKey(CustomTemplateCallback callback) {
-    Editor editor = callback.getEditor();
-    PsiElement element = callback.getContext();
-    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);
-      if (checkTemplateKey(key, callback)) {
-        return key;
-      }
-      if (element != null) {
-        element = element.getParent();
-      }
+    TemplateToken token = parseSelectors(key);
+    if (token == null) {
+      return null;
     }
-    while (element != null && parentStart > lineStart);
-    return null;
-  }
-
-  private static boolean checkTemplateKey(String key, CustomTemplateCallback callback) {
-    List<Token> tokens = parse(key, callback);
-    if (tokens != null && check(tokens)) {
-      // !! required if Zen Coding if invoked by TemplateManagerImpl action
-      /*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;
-          }
+    if (template != null && (token.myAttribute2Value.size() > 0 || isTrueXml(callback))) {
+      assert prefix.equals(token.myKey);
+      token.myTemplate = template;
+      if (token.myAttribute2Value.size() > 0) {
+        XmlTag tag = parseXmlTagInTemplate(template.getString(), callback, false);
+        if (tag == null) {
+          return null;
         }
-      }*/
-      return true;
+      }
     }
-    return false;
+    return token;
   }
 
-  public boolean isApplicable(PsiFile file, int offset, boolean selection) {
-    return isApplicable(file, offset);
+  @Nullable
+  static XmlTag parseXmlTagInTemplate(String templateString, CustomTemplateCallback callback, boolean createPhysicalFile) {
+    XmlFile xmlFile = (XmlFile)PsiFileFactory.getInstance(callback.getProject())
+      .createFileFromText("dummy.xml", StdFileTypes.XML, templateString, LocalTimeCounter.currentTime(), createPhysicalFile);
+    XmlDocument document = xmlFile.getDocument();
+    return document == null ? null : document.getRootTag();
   }
 
-  private static boolean isApplicable(PsiFile file, int offset) {
-    WebEditorOptions webEditorOptions = WebEditorOptions.getInstance();
-    if (!webEditorOptions.isZenCodingEnabled()) {
-      return false;
-    }
-    if (file == null) {
-      return false;
-    }
-    PsiDocumentManager.getInstance(file.getProject()).commitAllDocuments();
-    PsiElement element = null;
-    if (!InjectedLanguageManager.getInstance(file.getProject()).isInjectedFragment(file)) {
-      element = InjectedLanguageUtil.findInjectedElementNoCommit(file, offset);
-    }
-    if (element == null) {
-      element = file.findElementAt(offset > 0 ? offset - 1 : offset);
-      if (element == null) {
-        element = file;
-      }
-    }
+  protected boolean isApplicable(@NotNull PsiElement element) {
     if (element.getLanguage() instanceof XMLLanguage) {
       if (PsiTreeUtil.getParentOfType(element, XmlAttributeValue.class) != null) {
         return false;
@@ -376,111 +206,25 @@ public class XmlZenCodingTemplate implements CustomLiveTemplate {
     return false;
   }
 
-  public void expand(String key, @NotNull CustomTemplateCallback callback) {
-    expand(key, callback, null);
-  }
-
-  private static void expand(String key,
-                             @NotNull CustomTemplateCallback callback,
-                             String surroundedText) {
-    List<Token> tokens = parse(key, callback);
-    assert tokens != null;
-    if (surroundedText == null) {
-      if (tokens.size() == 2) {
-        Token token = tokens.get(0);
-        if (token instanceof TemplateToken) {
-          if (key.equals(((TemplateToken)token).myKey) && callback.findApplicableTemplates(key).size() > 1) {
-            callback.startTemplate();
-            return;
-          }
-        }
-      }
-      callback.deleteTemplateKey(key);
-    }
-    XmlZenCodingInterpreter.interpret(tokens, 0, callback, State.WORD, surroundedText);
-  }
-
-  public void wrap(final String selection,
-                   @NotNull final CustomTemplateCallback callback,
-                   @Nullable final TemplateInvokationListener listener) {
-    InputValidatorEx validator = new InputValidatorEx() {
-      public String getErrorText(String inputString) {
-        if (!checkTemplateKey(inputString, callback)) {
-          return XmlBundle.message("zen.coding.incorrect.abbreviation.error");
-        }
-        return null;
-      }
-
-      public boolean checkInput(String inputString) {
-        return getErrorText(inputString) == null;
-      }
-
-      public boolean canClose(String inputString) {
-        return checkInput(inputString);
-      }
-    };
-    final String abbreviation = Messages
-      .showInputDialog(callback.getProject(), XmlBundle.message("zen.coding.enter.abbreviation.dialog.label"),
-                       XmlBundle.message("zen.coding.title"), Messages.getQuestionIcon(), "", validator);
-    if (abbreviation != null) {
-      doWrap(selection, abbreviation, callback, listener);
-    }
-  }
-
-  private static void doWrap(final String selection,
-                             final String abbreviation,
-                             final CustomTemplateCallback callback,
-                             final TemplateInvokationListener listener) {
-    ApplicationManager.getApplication().runWriteAction(new Runnable() {
-      public void run() {
-        CommandProcessor.getInstance().executeCommand(callback.getProject(), new Runnable() {
-          public void run() {
-            EditorModificationUtil.deleteSelectedText(callback.getEditor());
-            PsiDocumentManager.getInstance(callback.getProject()).commitAllDocuments();
-            callback.fixInitialState();
-            expand(abbreviation, callback, selection);
-            if (listener != null) {
-              listener.finished();
-            }
-          }
-        }, CodeInsightBundle.message("insert.code.template.command"), null);
-      }
-    });
-  }
-
-  @NotNull
-  public String getTitle() {
-    return XmlBundle.message("zen.coding.title");
-  }
-
-  public char getShortcut() {
-    return (char)WebEditorOptions.getInstance().getZenCodingExpandShortcut();
-  }
-
-  @Nullable
-  static XmlTag parseXmlTagInTemplate(String templateString, CustomTemplateCallback callback, boolean createPhysicalFile) {
-    XmlFile xmlFile = (XmlFile)PsiFileFactory.getInstance(callback.getProject())
-      .createFileFromText("dummy.xml", StdFileTypes.XML, templateString, LocalTimeCounter.currentTime(), createPhysicalFile);
-    XmlDocument document = xmlFile.getDocument();
-    return document == null ? null : document.getRootTag();
-  }
-
   public static boolean startZenCoding(Editor editor, PsiFile file, String abbreviation) {
     int caretAt = editor.getCaretModel().getOffset();
-    if (isApplicable(file, caretAt)) {
+    XmlZenCodingTemplate template = CustomLiveTemplate.EP_NAME.findExtension(XmlZenCodingTemplate.class);
+    if (abbreviation != null && !template.supportsWrapping()) {
+      return false;
+    }
+    if (template.isApplicable(file, caretAt)) {
       final CustomTemplateCallback callback = new CustomTemplateCallback(editor, file);
       if (abbreviation != null) {
         String selection = callback.getEditor().getSelectionModel().getSelectedText();
         assert selection != null;
         selection = selection.trim();
-        doWrap(selection, abbreviation, callback, new TemplateInvokationListener() {
+        template.doWrap(selection, abbreviation, callback, new TemplateInvokationListener() {
           public void finished() {
             callback.startAllExpandedTemplates();
           }
         });
       }
       else {
-        XmlZenCodingTemplate template = new XmlZenCodingTemplate();
         String key = template.computeTemplateKey(callback);
         if (key != null) {
           template.expand(key, callback);
@@ -494,4 +238,29 @@ public class XmlZenCodingTemplate implements CustomLiveTemplate {
     }
     return false;
   }
+
+  public String computeTemplateKey(@NotNull CustomTemplateCallback callback) {
+    Editor editor = callback.getEditor();
+    PsiElement element = callback.getContext();
+    int line = editor.getCaretModel().getLogicalPosition().line;
+    int lineStart = editor.getDocument().getLineStartOffset(line);
+    int elementStart;
+    do {
+      elementStart = element != null ? element.getTextRange().getStartOffset() : 0;
+      int startOffset = elementStart > lineStart ? elementStart : lineStart;
+      String key = computeKey(editor, startOffset);
+      if (checkTemplateKey(key, callback)) {
+        return key;
+      }
+      if (element != null) {
+        element = element.getParent();
+      }
+    }
+    while (element != null && elementStart > lineStart);
+    return null;
+  }
+
+  public boolean supportsWrapping() {
+    return true;
+  }
 }
\ No newline at end of file
diff --git a/xml/impl/src/com/intellij/codeInsight/template/zencoding/ZenCodingTemplate.java b/xml/impl/src/com/intellij/codeInsight/template/zencoding/ZenCodingTemplate.java
new file mode 100644 (file)
index 0000000..ba46de5
--- /dev/null
@@ -0,0 +1,281 @@
+/*
+ * 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.CodeInsightBundle;
+import com.intellij.codeInsight.template.CustomLiveTemplate;
+import com.intellij.codeInsight.template.CustomTemplateCallback;
+import com.intellij.codeInsight.template.TemplateInvokationListener;
+import com.intellij.lang.injection.InjectedLanguageManager;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.command.CommandProcessor;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.EditorModificationUtil;
+import com.intellij.openapi.ui.InputValidatorEx;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
+import com.intellij.xml.XmlBundle;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @author Eugene.Kudelevsky
+ */
+public abstract class ZenCodingTemplate implements CustomLiveTemplate {
+  static final char MARKER = '$';
+  private static final String OPERATIONS = ">+*";
+
+  private static int parseNonNegativeInt(@NotNull String s) {
+    try {
+      return Integer.parseInt(s);
+    }
+    catch (Throwable ignored) {
+    }
+    return -1;
+  }
+
+  @Nullable
+  private 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;
+          }
+          TemplateToken token = parseTemplateKey(key, callback);
+          if (token == null) 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;
+  }
+
+  @Nullable
+  protected abstract TemplateToken parseTemplateKey(String key, CustomTemplateCallback callback);
+
+  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;
+  }
+
+  protected 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;
+  }
+
+  protected boolean checkTemplateKey(String key, CustomTemplateCallback callback) {
+    List<Token> tokens = parse(key, callback);
+    if (tokens != null && check(tokens)) {
+      // !! required if Zen Coding if invoked by TemplateManagerImpl action
+      /*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 true;
+    }
+    return false;
+  }
+
+  public void expand(String key, @NotNull CustomTemplateCallback callback) {
+    expand(key, callback, null);
+  }
+
+  private void expand(String key,
+                      @NotNull CustomTemplateCallback callback,
+                      String surroundedText) {
+    List<Token> tokens = parse(key, callback);
+    assert tokens != null;
+    if (surroundedText == null) {
+      if (tokens.size() == 2) {
+        Token token = tokens.get(0);
+        if (token instanceof TemplateToken) {
+          if (key.equals(((TemplateToken)token).myKey) && callback.findApplicableTemplates(key).size() > 1) {
+            callback.startTemplate();
+            return;
+          }
+        }
+      }
+      callback.deleteTemplateKey(key);
+    }
+    XmlZenCodingInterpreter.interpret(tokens, 0, callback, State.WORD, surroundedText);
+  }
+
+  public void wrap(final String selection,
+                   @NotNull final CustomTemplateCallback callback,
+                   @Nullable final TemplateInvokationListener listener) {
+    InputValidatorEx validator = new InputValidatorEx() {
+      public String getErrorText(String inputString) {
+        if (!checkTemplateKey(inputString, callback)) {
+          return XmlBundle.message("zen.coding.incorrect.abbreviation.error");
+        }
+        return null;
+      }
+
+      public boolean checkInput(String inputString) {
+        return getErrorText(inputString) == null;
+      }
+
+      public boolean canClose(String inputString) {
+        return checkInput(inputString);
+      }
+    };
+    final String abbreviation = Messages
+      .showInputDialog(callback.getProject(), XmlBundle.message("zen.coding.enter.abbreviation.dialog.label"),
+                       XmlBundle.message("zen.coding.title"), Messages.getQuestionIcon(), "", validator);
+    if (abbreviation != null) {
+      doWrap(selection, abbreviation, callback, listener);
+    }
+  }
+
+  public boolean isApplicable(PsiFile file, int offset) {
+    WebEditorOptions webEditorOptions = WebEditorOptions.getInstance();
+    if (!webEditorOptions.isZenCodingEnabled()) {
+      return false;
+    }
+    if (file == null) {
+      return false;
+    }
+    PsiDocumentManager.getInstance(file.getProject()).commitAllDocuments();
+    PsiElement element = null;
+    if (!InjectedLanguageManager.getInstance(file.getProject()).isInjectedFragment(file)) {
+      element = InjectedLanguageUtil.findInjectedElementNoCommit(file, offset);
+    }
+    if (element == null) {
+      element = file.findElementAt(offset > 0 ? offset - 1 : offset);
+      if (element == null) {
+        element = file;
+      }
+    }
+    return isApplicable(element);
+  }
+
+  protected abstract boolean isApplicable(@NotNull PsiElement element);
+
+  protected void doWrap(final String selection,
+                        final String abbreviation,
+                        final CustomTemplateCallback callback,
+                        final TemplateInvokationListener listener) {
+    ApplicationManager.getApplication().runWriteAction(new Runnable() {
+      public void run() {
+        CommandProcessor.getInstance().executeCommand(callback.getProject(), new Runnable() {
+          public void run() {
+            EditorModificationUtil.deleteSelectedText(callback.getEditor());
+            PsiDocumentManager.getInstance(callback.getProject()).commitAllDocuments();
+            callback.fixInitialState();
+            expand(abbreviation, callback, selection);
+            if (listener != null) {
+              listener.finished();
+            }
+          }
+        }, CodeInsightBundle.message("insert.code.template.command"), null);
+      }
+    });
+  }
+
+  @NotNull
+  public String getTitle() {
+    return XmlBundle.message("zen.coding.title");
+  }
+
+  public char getShortcut() {
+    return (char)WebEditorOptions.getInstance().getZenCodingExpandShortcut();
+  }
+}