Xml custom template
authorEugene Kudelevsky <Eugene.Kudelevsky@jetbrains.com>
Tue, 9 Feb 2010 17:40:41 +0000 (20:40 +0300)
committerEugene Kudelevsky <Eugene.Kudelevsky@jetbrains.com>
Tue, 9 Feb 2010 17:40:41 +0000 (20:40 +0300)
platform/platform-resources/src/META-INF/XmlPlugin.xml
xml/impl/src/com/intellij/codeInsight/template/XmlCustomLiveTemplate.java [new file with mode: 0644]

index 1ad2878f27c190d0e3f68b1c8b82118651da01e2..4142e22f93c152e301489c19df6e0a7695f01f6f 100644 (file)
                                         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.XmlCustomLiveTemplate"/>
     <productivityFeaturesProvider implementation="com.intellij.featureStatistics.XmlProductivityFeatureProvider"/>
 
     <idIndexer filetype="XML" implementationClass="com.intellij.psi.impl.cache.impl.idCache.XmlIdIndexer"/>
diff --git a/xml/impl/src/com/intellij/codeInsight/template/XmlCustomLiveTemplate.java b/xml/impl/src/com/intellij/codeInsight/template/XmlCustomLiveTemplate.java
new file mode 100644 (file)
index 0000000..41f4ccb
--- /dev/null
@@ -0,0 +1,459 @@
+/*
+ * 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.codeInsight.template.impl.TemplateImpl;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.ScrollType;
+import com.intellij.psi.xml.XmlFile;
+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 class XmlCustomLiveTemplate implements CustomLiveTemplate {
+  private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.template.XmlCustomLiveTemplate");
+
+  private static final String POSSIBLE_OPERATIONS = ">+*";
+  private static final char MARKER = '$';
+
+  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;
+
+    MyTemplateToken(String key) {
+      myKey = key;
+    }
+  }
+
+  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 boolean isTemplateKeyPart(char c) {
+    return !Character.isWhitespace(c) && POSSIBLE_OPERATIONS.indexOf(c) < 0;
+  }
+
+  private static int parseNonNegativeInt(@NotNull String s) {
+    try {
+      return Integer.parseInt(s);
+    }
+    catch (Throwable ignored) {
+    }
+    return -1;
+  }
+
+  @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 || POSSIBLE_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;
+          }
+          if (!callback.isLiveTemplateApplicable(key) && key.indexOf('<') >= 0) {
+            return null;
+          }
+          result.add(new MyTemplateToken(key));
+        }
+        result.add(i < n - 1 ? new MyOperationToken(c) : new MyMarkerToken());
+      }
+      else if (isTemplateKeyPart(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;
+  }
+
+  public boolean isApplicable(@NotNull String key, @NotNull CustomTemplateCallback callback) {
+    if (callback.getFile() instanceof XmlFile) {
+      List<MyToken> tokens = parse(key, callback);
+      if (tokens != null) {
+        return check(tokens);
+      }
+    }
+    return false;
+  }
+
+  public void execute(@NotNull String key, @NotNull CustomTemplateCallback callback, @Nullable TemplateInvokationListener listener) {
+    List<MyToken> tokens = parse(key, callback);
+    assert tokens != null;
+    MyInterpreter interpreter = new MyInterpreter(tokens, 0, callback, MyState.WORD, listener);
+    interpreter.iter();
+  }
+
+  private static void fail() {
+    LOG.error("Input string was checked incorrectly during isApplicable() invokation");
+  }
+
+  private static boolean invokeTemplate(String key, CustomTemplateCallback callback, TemplateInvokationListener listener) {
+    if (callback.isLiveTemplateApplicable(key)) {
+      return callback.startTemplate(key, listener);
+    }
+    else {
+      TemplateImpl template = new TemplateImpl("", "");
+      template.addTextSegment('<' + key + '>');
+      template.addVariableSegment(TemplateImpl.END);
+      template.addTextSegment("</" + key + ">");
+      return callback.startTemplate(template, listener);
+    }
+  }
+
+  private class MyInterpreter extends Iteration {
+    private final List<MyToken> myTokens;
+    private final CustomTemplateCallback myCallback;
+    private final TemplateInvokationListener myListener;
+    private MyState myState;
+    private int myEndOffset = -1;
+
+    private MyInterpreter(List<MyToken> tokens,
+                          int startIndex,
+                          CustomTemplateCallback callback,
+                          MyState initialState,
+                          TemplateInvokationListener listener) {
+      super(startIndex, tokens.size(), null);
+      myTokens = tokens;
+      myCallback = callback;
+      myListener = listener;
+      myState = initialState;
+    }
+
+    private void fixEndOffset() {
+      if (myEndOffset < 0) {
+        myEndOffset = getOffset();
+      }
+    }
+
+    private int getOffset() {
+      return myCallback.getEditor().getCaretModel().getOffset();
+    }
+
+    private int getTextLength() {
+      return myCallback.getEditor().getDocument().getCharsSequence().length();
+    }
+
+    private void moveCaret(int delta) {
+      myCallback.getEditor().getCaretModel().moveToOffset(delta);
+    }
+
+    private void finish(boolean inSeparateEvent) {
+      Editor editor = myCallback.getEditor();
+      if (myEndOffset >= 0) {
+        editor.getCaretModel().moveToOffset(myEndOffset);
+      }
+      editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
+      if (myListener != null) {
+        myListener.finished(inSeparateEvent, true);
+      }
+    }
+
+    @Override
+    protected void next() {
+      if (myIndex == myMaxIndex - 1) {
+        finish(true);
+      }
+      super.next();
+    }
+
+    @Override
+    protected void iter() {
+      String templateKey = null;
+      int number = -1;
+      for (; myIndex < myMaxIndex; myIndex++) {
+        MyToken token = myTokens.get(myIndex);
+        switch (myState) {
+          case OPERATION:
+            if (templateKey != null) {
+              if (token instanceof MyMarkerToken || token instanceof MyOperationToken) {
+                final char sign = token instanceof MyOperationToken ? ((MyOperationToken)token).mySign : MARKER;
+                if (sign == MARKER || sign == '+') {
+                  final int offsetBefore = getOffset();
+                  final int lengthBefore = getTextLength();
+                  TemplateInvokationListener listener = new TemplateInvokationListener() {
+                    public void finished(boolean inSeparateEvent, boolean success) {
+                      myState = MyState.WORD;
+                      fixEndOffset();
+                      if (sign == '+') {
+                        moveCaret(offsetBefore + getTextLength() - lengthBefore);
+                      }
+                      if (inSeparateEvent) {
+                        next();
+                      }
+                    }
+                  };
+                  if (!invokeTemplate(templateKey, myCallback, listener)) {
+                    return;
+                  }
+                  templateKey = null;
+                }
+                else if (sign == '>') {
+                  if (!startTemplate(templateKey)) {
+                    return;
+                  }
+                  templateKey = null;
+                }
+                else if (sign == '*') {
+                  myState = MyState.NUMBER;
+                }
+              }
+              else {
+                fail();
+              }
+            }
+            break;
+          case WORD:
+            if (token instanceof MyTemplateToken) {
+              templateKey = ((MyTemplateToken)token).myKey;
+              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 == '+') {
+                ConsecutiveTemplateInvokation iteration = new ConsecutiveTemplateInvokation(templateKey, number);
+                iteration.iter();
+                if (!iteration.isFinished()) {
+                  return;
+                }
+                templateKey = null;
+              }
+              else if (number > 1) {
+                ConsecutiveTemplateWithTailInvokation iteration =
+                  new ConsecutiveTemplateWithTailInvokation(templateKey, myTokens, myIndex + 1, number);
+                iteration.iter();
+                if (iteration.isFinished()) {
+                  myIndex = myMaxIndex;
+                  finish(false);
+                }
+                return;
+              }
+              else {
+                assert number == 1;
+                if (!startTemplate(templateKey)) {
+                  return;
+                }
+                templateKey = null;
+              }
+              myState = MyState.WORD;
+            }
+            else {
+              fail();
+            }
+            break;
+        }
+      }
+      finish(false);
+    }
+
+    private boolean startTemplate(String templateKey) {
+      TemplateInvokationListener listener = new TemplateInvokationListener() {
+        public void finished(boolean inSeparateEvent, boolean success) {
+          myState = MyState.WORD;
+          if (inSeparateEvent) {
+            next();
+          }
+        }
+      };
+      if (!invokeTemplate(templateKey, myCallback, listener)) {
+        return false;
+      }
+      return true;
+    }
+
+    private class ConsecutiveTemplateInvokation extends Iteration {
+      private final String myTemplateKey;
+
+      public ConsecutiveTemplateInvokation(String templateKey, int count) {
+        super(0, count, MyInterpreter.this);
+        myTemplateKey = templateKey;
+      }
+
+      @Override
+      protected void iter() {
+        final int offsetBefore = getOffset();
+        final int lengthBefore = getTextLength();
+        for (; myIndex < myMaxIndex; myIndex++) {
+          TemplateInvokationListener listener = new TemplateInvokationListener() {
+            public void finished(boolean inSeparateEvent, boolean success) {
+              myState = MyState.WORD;
+              fixEndOffset();
+              moveCaret(offsetBefore + getTextLength() - lengthBefore);
+              if (inSeparateEvent) {
+                next();
+              }
+            }
+          };
+          if (!invokeTemplate(myTemplateKey, myCallback, listener)) {
+            return;
+          }
+        }
+      }
+    }
+
+    private class ConsecutiveTemplateWithTailInvokation extends Iteration {
+      private final String myTemplateKey;
+      private final List<MyToken> myTokens;
+      private final int myTailStart;
+
+      public ConsecutiveTemplateWithTailInvokation(String templateKey, List<MyToken> tokens, int tailStart, int count) {
+        super(0, count, null);
+        assert count > 1;
+        assert tailStart < tokens.size();
+        myTemplateKey = templateKey;
+        myTailStart = tailStart;
+        myTokens = tokens;
+      }
+
+      @Override
+      protected void next() {
+        if (myIndex == myMaxIndex - 1) {
+          finish(true);
+        }
+        super.next();
+      }
+
+      @Override
+      protected void iter() {
+        final int offsetBefore = getOffset();
+        final int lengthBefore = getTextLength();
+        for (; myIndex < myMaxIndex; myIndex++) {
+          final boolean[] flag = new boolean[]{false};
+          TemplateInvokationListener listener = new TemplateInvokationListener() {
+            public void finished(boolean inSeparateEvent, boolean success) {
+              MyInterpreter interpreter =
+                new MyInterpreter(myTokens, myTailStart, myCallback, MyState.WORD, new TemplateInvokationListener() {
+                  public void finished(boolean inSeparateEvent, boolean success) {
+                    fixEndOffset();
+                    moveCaret(offsetBefore + getTextLength() - lengthBefore);
+                    if (inSeparateEvent) {
+                      next();
+                    }
+                  }
+                });
+              interpreter.iter();
+              if (inSeparateEvent && interpreter.isFinished()) {
+                next();
+              }
+              if (!interpreter.isFinished()) {
+                flag[0] = true;
+              }
+            }
+          };
+          if (!invokeTemplate(myTemplateKey, myCallback, listener) || flag[0]) {
+            return;
+          }
+        }
+      }
+    }
+  }
+}