refactoring, support css zen-coding selectors
[idea/community.git] / xml / impl / src / com / intellij / codeInsight / template / zencoding / ZenCodingTemplate.java
1 /*
2  * Copyright 2000-2010 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.codeInsight.template.zencoding;
17
18 import com.intellij.application.options.editor.WebEditorOptions;
19 import com.intellij.codeInsight.CodeInsightBundle;
20 import com.intellij.codeInsight.template.CustomLiveTemplate;
21 import com.intellij.codeInsight.template.CustomTemplateCallback;
22 import com.intellij.codeInsight.template.TemplateInvokationListener;
23 import com.intellij.lang.injection.InjectedLanguageManager;
24 import com.intellij.openapi.application.ApplicationManager;
25 import com.intellij.openapi.command.CommandProcessor;
26 import com.intellij.openapi.editor.Editor;
27 import com.intellij.openapi.editor.EditorModificationUtil;
28 import com.intellij.openapi.ui.InputValidatorEx;
29 import com.intellij.openapi.ui.Messages;
30 import com.intellij.psi.PsiDocumentManager;
31 import com.intellij.psi.PsiElement;
32 import com.intellij.psi.PsiFile;
33 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
34 import com.intellij.xml.XmlBundle;
35 import org.jetbrains.annotations.NotNull;
36 import org.jetbrains.annotations.Nullable;
37
38 import java.util.ArrayList;
39 import java.util.Collection;
40 import java.util.List;
41
42 /**
43  * @author Eugene.Kudelevsky
44  */
45 public abstract class ZenCodingTemplate implements CustomLiveTemplate {
46   static final char MARKER = '$';
47   private static final String OPERATIONS = ">+*";
48
49   private static int parseNonNegativeInt(@NotNull String s) {
50     try {
51       return Integer.parseInt(s);
52     }
53     catch (Throwable ignored) {
54     }
55     return -1;
56   }
57
58   @Nullable
59   private List<Token> parse(@NotNull String text, @NotNull CustomTemplateCallback callback) {
60     text += MARKER;
61     StringBuilder templateKeyBuilder = new StringBuilder();
62     List<Token> result = new ArrayList<Token>();
63     for (int i = 0, n = text.length(); i < n; i++) {
64       char c = text.charAt(i);
65       if (i == n - 1 || (i < n - 2 && OPERATIONS.indexOf(c) >= 0)) {
66         String key = templateKeyBuilder.toString();
67         templateKeyBuilder = new StringBuilder();
68         int num = parseNonNegativeInt(key);
69         if (num > 0) {
70           result.add(new NumberToken(num));
71         }
72         else {
73           if (key.length() == 0) {
74             return null;
75           }
76           TemplateToken token = parseTemplateKey(key, callback);
77           if (token == null) return null;
78           result.add(token);
79         }
80         result.add(i < n - 1 ? new OperationToken(c) : new MarkerToken());
81       }
82       else if (!Character.isWhitespace(c)) {
83         templateKeyBuilder.append(c);
84       }
85       else {
86         return null;
87       }
88     }
89     return result;
90   }
91
92   @Nullable
93   protected abstract TemplateToken parseTemplateKey(String key, CustomTemplateCallback callback);
94
95   private static boolean check(@NotNull Collection<Token> tokens) {
96     State state = State.WORD;
97     for (Token token : tokens) {
98       if (token instanceof MarkerToken) {
99         break;
100       }
101       switch (state) {
102         case OPERATION:
103           if (token instanceof OperationToken) {
104             state = ((OperationToken)token).mySign == '*' ? State.NUMBER : State.WORD;
105           }
106           else {
107             return false;
108           }
109           break;
110         case WORD:
111           if (token instanceof TemplateToken) {
112             state = State.OPERATION;
113           }
114           else {
115             return false;
116           }
117           break;
118         case NUMBER:
119           if (token instanceof NumberToken) {
120             state = State.AFTER_NUMBER;
121           }
122           else {
123             return false;
124           }
125           break;
126         case AFTER_NUMBER:
127           if (token instanceof OperationToken && ((OperationToken)token).mySign != '*') {
128             state = State.WORD;
129           }
130           else {
131             return false;
132           }
133           break;
134       }
135     }
136     return state == State.OPERATION || state == State.AFTER_NUMBER;
137   }
138
139   protected static String computeKey(Editor editor, int startOffset) {
140     int offset = editor.getCaretModel().getOffset();
141     String s = editor.getDocument().getCharsSequence().subSequence(startOffset, offset).toString();
142     int index = 0;
143     while (index < s.length() && Character.isWhitespace(s.charAt(index))) {
144       index++;
145     }
146     String key = s.substring(index);
147     int lastWhitespaceIndex = -1;
148     for (int i = 0; i < key.length(); i++) {
149       if (Character.isWhitespace(key.charAt(i))) {
150         lastWhitespaceIndex = i;
151       }
152     }
153     if (lastWhitespaceIndex >= 0 && lastWhitespaceIndex < key.length() - 1) {
154       return key.substring(lastWhitespaceIndex + 1);
155     }
156     return key;
157   }
158
159   protected boolean checkTemplateKey(String key, CustomTemplateCallback callback) {
160     List<Token> tokens = parse(key, callback);
161     if (tokens != null && check(tokens)) {
162       // !! required if Zen Coding if invoked by TemplateManagerImpl action
163       /*if (tokens.size() == 2) {
164         Token token = tokens.get(0);
165         if (token instanceof TemplateToken) {
166           if (key.equals(((TemplateToken)token).myKey) && callback.isLiveTemplateApplicable(key)) {
167             // do not activate only live template
168             return null;
169           }
170         }
171       }*/
172       return true;
173     }
174     return false;
175   }
176
177   public void expand(String key, @NotNull CustomTemplateCallback callback) {
178     expand(key, callback, null);
179   }
180
181   private void expand(String key,
182                       @NotNull CustomTemplateCallback callback,
183                       String surroundedText) {
184     List<Token> tokens = parse(key, callback);
185     assert tokens != null;
186     if (surroundedText == null) {
187       if (tokens.size() == 2) {
188         Token token = tokens.get(0);
189         if (token instanceof TemplateToken) {
190           if (key.equals(((TemplateToken)token).myKey) && callback.findApplicableTemplates(key).size() > 1) {
191             callback.startTemplate();
192             return;
193           }
194         }
195       }
196       callback.deleteTemplateKey(key);
197     }
198     XmlZenCodingInterpreter.interpret(tokens, 0, callback, State.WORD, surroundedText);
199   }
200
201   public void wrap(final String selection,
202                    @NotNull final CustomTemplateCallback callback,
203                    @Nullable final TemplateInvokationListener listener) {
204     InputValidatorEx validator = new InputValidatorEx() {
205       public String getErrorText(String inputString) {
206         if (!checkTemplateKey(inputString, callback)) {
207           return XmlBundle.message("zen.coding.incorrect.abbreviation.error");
208         }
209         return null;
210       }
211
212       public boolean checkInput(String inputString) {
213         return getErrorText(inputString) == null;
214       }
215
216       public boolean canClose(String inputString) {
217         return checkInput(inputString);
218       }
219     };
220     final String abbreviation = Messages
221       .showInputDialog(callback.getProject(), XmlBundle.message("zen.coding.enter.abbreviation.dialog.label"),
222                        XmlBundle.message("zen.coding.title"), Messages.getQuestionIcon(), "", validator);
223     if (abbreviation != null) {
224       doWrap(selection, abbreviation, callback, listener);
225     }
226   }
227
228   public boolean isApplicable(PsiFile file, int offset) {
229     WebEditorOptions webEditorOptions = WebEditorOptions.getInstance();
230     if (!webEditorOptions.isZenCodingEnabled()) {
231       return false;
232     }
233     if (file == null) {
234       return false;
235     }
236     PsiDocumentManager.getInstance(file.getProject()).commitAllDocuments();
237     PsiElement element = null;
238     if (!InjectedLanguageManager.getInstance(file.getProject()).isInjectedFragment(file)) {
239       element = InjectedLanguageUtil.findInjectedElementNoCommit(file, offset);
240     }
241     if (element == null) {
242       element = file.findElementAt(offset > 0 ? offset - 1 : offset);
243       if (element == null) {
244         element = file;
245       }
246     }
247     return isApplicable(element);
248   }
249
250   protected abstract boolean isApplicable(@NotNull PsiElement element);
251
252   protected void doWrap(final String selection,
253                         final String abbreviation,
254                         final CustomTemplateCallback callback,
255                         final TemplateInvokationListener listener) {
256     ApplicationManager.getApplication().runWriteAction(new Runnable() {
257       public void run() {
258         CommandProcessor.getInstance().executeCommand(callback.getProject(), new Runnable() {
259           public void run() {
260             EditorModificationUtil.deleteSelectedText(callback.getEditor());
261             PsiDocumentManager.getInstance(callback.getProject()).commitAllDocuments();
262             callback.fixInitialState();
263             expand(abbreviation, callback, selection);
264             if (listener != null) {
265               listener.finished();
266             }
267           }
268         }, CodeInsightBundle.message("insert.code.template.command"), null);
269       }
270     });
271   }
272
273   @NotNull
274   public String getTitle() {
275     return XmlBundle.message("zen.coding.title");
276   }
277
278   public char getShortcut() {
279     return (char)WebEditorOptions.getInstance().getZenCodingExpandShortcut();
280   }
281 }