820259939c5228f4f0f85f07b5c76e188a988fd2
[fedora-idea.git] / xml / impl / src / com / intellij / codeInsight / template / XmlCustomLiveTemplate.java
blob820259939c5228f4f0f85f07b5c76e188a988fd2
1 /*
2 * Copyright 2000-2010 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.intellij.codeInsight.template;
18 import com.intellij.codeInsight.template.impl.TemplateImpl;
19 import com.intellij.ide.highlighter.HtmlFileType;
20 import com.intellij.openapi.diagnostic.Logger;
21 import com.intellij.openapi.editor.Editor;
22 import com.intellij.openapi.editor.ScrollType;
23 import com.intellij.psi.xml.XmlFile;
24 import com.intellij.util.containers.HashMap;
25 import org.jetbrains.annotations.NotNull;
26 import org.jetbrains.annotations.Nullable;
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.List;
31 import java.util.Map;
33 /**
34 * @author Eugene.Kudelevsky
36 public class XmlCustomLiveTemplate implements CustomLiveTemplate {
37 private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.template.XmlCustomLiveTemplate");
39 private static final String ATTRS = "ATTRS";
41 private static final String POSSIBLE_OPERATIONS = ">+*";
42 private static final String HTML_SELECTORS = ".#";
43 private static final char MARKER = '$';
45 private static enum MyState {
46 OPERATION, WORD, AFTER_NUMBER, NUMBER
49 private static class MyToken {
52 private static class MyMarkerToken extends MyToken {
55 private static class MyTemplateToken extends MyToken {
56 final String myKey;
58 MyTemplateToken(String key) {
59 myKey = key;
63 private static class MyNumberToken extends MyToken {
64 final int myNumber;
66 MyNumberToken(int number) {
67 myNumber = number;
71 private static class MyOperationToken extends MyToken {
72 final char mySign;
74 MyOperationToken(char sign) {
75 mySign = sign;
79 private static boolean isTemplateKeyPart(char c) {
80 return !Character.isWhitespace(c) && POSSIBLE_OPERATIONS.indexOf(c) < 0;
83 private static int parseNonNegativeInt(@NotNull String s) {
84 try {
85 return Integer.parseInt(s);
87 catch (Throwable ignored) {
89 return -1;
92 private static String getPrefix(@NotNull String templateKey) {
93 for (int i = 0, n = templateKey.length(); i < n; i++) {
94 char c = templateKey.charAt(i);
95 if (HTML_SELECTORS.indexOf(c) >= 0) {
96 return templateKey.substring(0, i);
99 return templateKey;
102 @Nullable
103 private static List<MyToken> parse(@NotNull String text, @NotNull CustomTemplateCallback callback) {
104 text += MARKER;
105 StringBuilder templateKeyBuilder = new StringBuilder();
106 List<MyToken> result = new ArrayList<MyToken>();
107 for (int i = 0, n = text.length(); i < n; i++) {
108 char c = text.charAt(i);
109 if (i == n - 1 || POSSIBLE_OPERATIONS.indexOf(c) >= 0) {
110 String key = templateKeyBuilder.toString();
111 templateKeyBuilder = new StringBuilder();
112 int num = parseNonNegativeInt(key);
113 if (num > 0) {
114 result.add(new MyNumberToken(num));
116 else {
117 if (key.length() == 0) {
118 return null;
120 String prefix = getPrefix(key);
121 if (callback.isLiveTemplateApplicable(prefix)) {
122 if (!prefix.equals(key) && !callback.isTemplateContainsVars(prefix, ATTRS)) {
123 return null;
126 else if (prefix.indexOf('<') >= 0) {
127 return null;
129 result.add(new MyTemplateToken(key));
131 result.add(i < n - 1 ? new MyOperationToken(c) : new MyMarkerToken());
133 else if (isTemplateKeyPart(c)) {
134 templateKeyBuilder.append(c);
136 else {
137 return null;
140 return result;
143 private static boolean check(@NotNull Collection<MyToken> tokens) {
144 MyState state = MyState.WORD;
145 for (MyToken token : tokens) {
146 if (token instanceof MyMarkerToken) {
147 break;
149 switch (state) {
150 case OPERATION:
151 if (token instanceof MyOperationToken) {
152 state = ((MyOperationToken)token).mySign == '*' ? MyState.NUMBER : MyState.WORD;
154 else {
155 return false;
157 break;
158 case WORD:
159 if (token instanceof MyTemplateToken) {
160 state = MyState.OPERATION;
162 else {
163 return false;
165 break;
166 case NUMBER:
167 if (token instanceof MyNumberToken) {
168 state = MyState.AFTER_NUMBER;
170 else {
171 return false;
173 break;
174 case AFTER_NUMBER:
175 if (token instanceof MyOperationToken && ((MyOperationToken)token).mySign != '*') {
176 state = MyState.WORD;
178 else {
179 return false;
181 break;
184 return state == MyState.OPERATION || state == MyState.AFTER_NUMBER;
187 public boolean isApplicable(@NotNull String key, @NotNull CustomTemplateCallback callback) {
188 if (callback.getFile() instanceof XmlFile) {
189 List<MyToken> tokens = parse(key, callback);
190 if (tokens != null) {
191 return check(tokens);
194 return false;
197 public void execute(@NotNull String key, @NotNull CustomTemplateCallback callback, @Nullable TemplateInvokationListener listener) {
198 List<MyToken> tokens = parse(key, callback);
199 assert tokens != null;
200 MyInterpreter interpreter = new MyInterpreter(tokens, callback, MyState.WORD, listener);
201 interpreter.invoke(0);
204 private static void fail() {
205 LOG.error("Input string was checked incorrectly during isApplicable() invokation");
208 @NotNull
209 private static String buildAttributesString(@Nullable String id, @NotNull List<String> classes) {
210 StringBuilder result = new StringBuilder();
211 if (id != null) {
212 result.append("id=\"").append(id).append('"');
213 if (classes.size() > 0) {
214 result.append(' ');
217 if (classes.size() > 0) {
218 result.append("class=\"");
219 for (int i = 0; i < classes.size(); i++) {
220 result.append(classes.get(i));
221 if (i < classes.size() - 1) {
222 result.append(' ');
225 result.append('"');
227 return result.toString();
230 private static boolean invokeTemplate(String key, final CustomTemplateCallback callback, final TemplateInvokationListener listener) {
231 if (callback.getFile().getFileType() instanceof HtmlFileType) {
232 String templateKey = null;
233 String id = null;
234 final List<String> classes = new ArrayList<String>();
235 StringBuilder builder = new StringBuilder();
236 char lastDelim = 0;
237 key += MARKER;
238 for (int i = 0, n = key.length(); i < n; i++) {
239 char c = key.charAt(i);
240 if (c == '#' || c == '.' || i == n - 1) {
241 switch (lastDelim) {
242 case 0:
243 templateKey = builder.toString();
244 break;
245 case '#':
246 id = builder.toString();
247 break;
248 case '.':
249 if (builder.length() > 0) {
250 classes.add(builder.toString());
252 break;
254 lastDelim = c;
255 builder = new StringBuilder();
257 else {
258 builder.append(c);
261 String attributes = buildAttributesString(id, classes);
262 return startTemplate(templateKey, callback, listener, attributes.length() > 0 ? ' ' + attributes : null);
264 return startTemplate(key, callback, listener, null);
267 private static boolean startTemplate(String key,
268 CustomTemplateCallback callback,
269 TemplateInvokationListener listener,
270 @Nullable String attributes) {
271 Map<String, String> predefinedValues = null;
272 if (attributes != null) {
273 predefinedValues = new HashMap<String, String>();
274 predefinedValues.put(ATTRS, attributes);
276 if (callback.isLiveTemplateApplicable(key)) {
277 return callback.startTemplate(key, predefinedValues, listener);
279 else {
280 TemplateImpl template = new TemplateImpl("", "");
281 template.addTextSegment('<' + key);
282 if (attributes != null) {
283 template.addVariable(ATTRS, "", "", false);
284 template.addVariableSegment(ATTRS);
286 template.addTextSegment(">");
287 template.addVariableSegment(TemplateImpl.END);
288 template.addTextSegment("</" + key + ">");
289 template.setToReformat(true);
290 return callback.startTemplate(template, predefinedValues, listener);
294 private class MyInterpreter {
295 private final List<MyToken> myTokens;
296 private final CustomTemplateCallback myCallback;
297 private final TemplateInvokationListener myListener;
298 private MyState myState;
299 private int myEndOffset = -1;
301 private MyInterpreter(List<MyToken> tokens,
302 CustomTemplateCallback callback,
303 MyState initialState,
304 TemplateInvokationListener listener) {
305 myTokens = tokens;
306 myCallback = callback;
307 myListener = listener;
308 myState = initialState;
311 private void fixEndOffset() {
312 if (myEndOffset < 0) {
313 myEndOffset = getOffset();
317 private int getOffset() {
318 return myCallback.getEditor().getCaretModel().getOffset();
321 private void finish(boolean inSeparateEvent) {
322 Editor editor = myCallback.getEditor();
323 if (myEndOffset >= 0) {
324 editor.getCaretModel().moveToOffset(myEndOffset);
326 editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
327 if (myListener != null) {
328 myListener.finished(inSeparateEvent);
332 public boolean invoke(int startIndex) {
333 final int n = myTokens.size();
334 String templateKey = null;
335 int number = -1;
336 for (int i = startIndex; i < n; i++) {
337 final int finalI = i;
338 MyToken token = myTokens.get(i);
339 switch (myState) {
340 case OPERATION:
341 if (templateKey != null) {
342 if (token instanceof MyMarkerToken || token instanceof MyOperationToken) {
343 final char sign = token instanceof MyOperationToken ? ((MyOperationToken)token).mySign : MARKER;
344 if (sign == MARKER || sign == '+') {
345 final Object key = new Object();
346 myCallback.fixStartOfTemplate(key);
347 TemplateInvokationListener listener = new TemplateInvokationListener() {
348 public void finished(boolean inSeparateEvent) {
349 myState = MyState.WORD;
350 fixEndOffset();
351 if (sign == '+') {
352 myCallback.gotoEndOfTemplate(key);
354 if (inSeparateEvent) {
355 invoke(finalI + 1);
359 if (!invokeTemplate(templateKey, myCallback, listener)) {
360 return false;
362 templateKey = null;
364 else if (sign == '>') {
365 if (!startTemplate(templateKey, finalI)) {
366 return false;
368 templateKey = null;
370 else if (sign == '*') {
371 myState = MyState.NUMBER;
374 else {
375 fail();
378 break;
379 case WORD:
380 if (token instanceof MyTemplateToken) {
381 templateKey = ((MyTemplateToken)token).myKey;
382 myState = MyState.OPERATION;
384 else {
385 fail();
387 break;
388 case NUMBER:
389 if (token instanceof MyNumberToken) {
390 number = ((MyNumberToken)token).myNumber;
391 myState = MyState.AFTER_NUMBER;
393 else {
394 fail();
396 break;
397 case AFTER_NUMBER:
398 if (token instanceof MyMarkerToken || token instanceof MyOperationToken) {
399 char sign = token instanceof MyOperationToken ? ((MyOperationToken)token).mySign : MARKER;
400 if (sign == MARKER || sign == '+') {
401 if (!invokeTemplateSeveralTimes(templateKey, number, finalI)) {
402 return false;
404 templateKey = null;
406 else if (number > 1) {
407 return invokeTemplateAndProcessTail(templateKey, i + 1, number);
409 else {
410 assert number == 1;
411 if (!startTemplate(templateKey, finalI)) {
412 return false;
414 templateKey = null;
416 myState = MyState.WORD;
418 else {
419 fail();
421 break;
424 finish(startIndex == n);
425 return true;
428 private boolean startTemplate(String templateKey, final int index) {
429 TemplateInvokationListener listener = new TemplateInvokationListener() {
430 public void finished(boolean inSeparateEvent) {
431 myState = MyState.WORD;
432 if (inSeparateEvent) {
433 invoke(index + 1);
437 if (!invokeTemplate(templateKey, myCallback, listener)) {
438 return false;
440 return true;
443 private boolean invokeTemplateSeveralTimes(final String templateKey, final int count, final int index) {
444 final Object key = new Object();
445 myCallback.fixStartOfTemplate(key);
446 for (int i = 0; i < count; i++) {
447 final int finalI = i;
448 TemplateInvokationListener listener = new TemplateInvokationListener() {
449 public void finished(boolean inSeparateEvent) {
450 myState = MyState.WORD;
451 fixEndOffset();
452 myCallback.gotoEndOfTemplate(key);
453 if (inSeparateEvent) {
454 int newCount = count - finalI - 1;
455 if (newCount > 0) {
456 invokeTemplateSeveralTimes(templateKey, newCount, index);
458 else {
459 invoke(index + 1);
464 if (!invokeTemplate(templateKey, myCallback, listener)) {
465 return false;
468 return true;
471 private boolean invokeTemplateAndProcessTail(final String templateKey, final int tailStart, final int count) {
472 final Object key = new Object();
473 myCallback.fixStartOfTemplate(key);
474 for (int i = 0; i < count; i++) {
475 final int finalI = i;
476 final boolean[] flag = new boolean[]{false};
477 TemplateInvokationListener listener = new TemplateInvokationListener() {
478 public void finished(boolean inSeparateEvent) {
479 MyInterpreter interpreter = new MyInterpreter(myTokens, myCallback, MyState.WORD, new TemplateInvokationListener() {
480 public void finished(boolean inSeparateEvent) {
481 fixEndOffset();
482 myCallback.gotoEndOfTemplate(key);
483 if (inSeparateEvent) {
484 invokeTemplateAndProcessTail(templateKey, tailStart, count - finalI - 1);
488 if (interpreter.invoke(tailStart)) {
489 if (inSeparateEvent) {
490 invokeTemplateAndProcessTail(templateKey, tailStart, count - finalI - 1);
493 else {
494 flag[0] = true;
498 if (!invokeTemplate(templateKey, myCallback, listener) || flag[0]) {
499 return false;
502 finish(count == 0);
503 return true;