support of non XHTML tags as ">" operands
[fedora-idea.git] / xml / impl / src / com / intellij / codeInsight / template / XmlCustomLiveTemplate.java
blob8ff09eb9d10b8ed764f186d45d9ddf33f0bbc9e9
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.Document;
22 import com.intellij.openapi.editor.Editor;
23 import com.intellij.openapi.editor.ScrollType;
24 import com.intellij.psi.xml.XmlFile;
25 import com.intellij.util.containers.HashMap;
26 import org.jetbrains.annotations.NotNull;
27 import org.jetbrains.annotations.Nullable;
29 import java.util.ArrayList;
30 import java.util.Collection;
31 import java.util.List;
32 import java.util.Map;
34 /**
35 * @author Eugene.Kudelevsky
37 public class XmlCustomLiveTemplate implements CustomLiveTemplate {
38 private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.template.XmlCustomLiveTemplate");
40 private static final String ATTRS = "ATTRS";
42 private static final String POSSIBLE_OPERATIONS = ">+*";
43 private static final String HTML_SELECTORS = ".#";
44 private static final char MARKER = '$';
46 private static enum MyState {
47 OPERATION, WORD, AFTER_NUMBER, NUMBER
50 private static class MyToken {
53 private static class MyMarkerToken extends MyToken {
56 private static class MyTemplateToken extends MyToken {
57 final String myKey;
59 MyTemplateToken(String key) {
60 myKey = key;
64 private static class MyNumberToken extends MyToken {
65 final int myNumber;
67 MyNumberToken(int number) {
68 myNumber = number;
72 private static class MyOperationToken extends MyToken {
73 final char mySign;
75 MyOperationToken(char sign) {
76 mySign = sign;
80 private static boolean isTemplateKeyPart(char c) {
81 return !Character.isWhitespace(c) && POSSIBLE_OPERATIONS.indexOf(c) < 0;
84 private static int parseNonNegativeInt(@NotNull String s) {
85 try {
86 return Integer.parseInt(s);
88 catch (Throwable ignored) {
90 return -1;
93 private static String getPrefix(@NotNull String templateKey) {
94 for (int i = 0, n = templateKey.length(); i < n; i++) {
95 char c = templateKey.charAt(i);
96 if (HTML_SELECTORS.indexOf(c) >= 0) {
97 return templateKey.substring(0, i);
100 return templateKey;
103 @Nullable
104 private static List<MyToken> parse(@NotNull String text, @NotNull CustomTemplateCallback callback) {
105 text += MARKER;
106 StringBuilder templateKeyBuilder = new StringBuilder();
107 List<MyToken> result = new ArrayList<MyToken>();
108 for (int i = 0, n = text.length(); i < n; i++) {
109 char c = text.charAt(i);
110 if (i == n - 1 || POSSIBLE_OPERATIONS.indexOf(c) >= 0) {
111 String key = templateKeyBuilder.toString();
112 templateKeyBuilder = new StringBuilder();
113 int num = parseNonNegativeInt(key);
114 if (num > 0) {
115 result.add(new MyNumberToken(num));
117 else {
118 if (key.length() == 0) {
119 return null;
121 String prefix = getPrefix(key);
122 if (callback.isLiveTemplateApplicable(prefix)) {
123 if (!prefix.equals(key) && !callback.isTemplateContainsVars(prefix, ATTRS)) {
124 return null;
127 else if (prefix.indexOf('<') >= 0) {
128 return null;
130 result.add(new MyTemplateToken(key));
132 result.add(i < n - 1 ? new MyOperationToken(c) : new MyMarkerToken());
134 else if (isTemplateKeyPart(c)) {
135 templateKeyBuilder.append(c);
137 else {
138 return null;
141 return result;
144 private static boolean check(@NotNull Collection<MyToken> tokens) {
145 MyState state = MyState.WORD;
146 for (MyToken token : tokens) {
147 if (token instanceof MyMarkerToken) {
148 break;
150 switch (state) {
151 case OPERATION:
152 if (token instanceof MyOperationToken) {
153 state = ((MyOperationToken)token).mySign == '*' ? MyState.NUMBER : MyState.WORD;
155 else {
156 return false;
158 break;
159 case WORD:
160 if (token instanceof MyTemplateToken) {
161 state = MyState.OPERATION;
163 else {
164 return false;
166 break;
167 case NUMBER:
168 if (token instanceof MyNumberToken) {
169 state = MyState.AFTER_NUMBER;
171 else {
172 return false;
174 break;
175 case AFTER_NUMBER:
176 if (token instanceof MyOperationToken && ((MyOperationToken)token).mySign != '*') {
177 state = MyState.WORD;
179 else {
180 return false;
182 break;
185 return state == MyState.OPERATION || state == MyState.AFTER_NUMBER;
188 public boolean isApplicable(@NotNull String key, @NotNull CustomTemplateCallback callback) {
189 if (callback.getFile() instanceof XmlFile) {
190 List<MyToken> tokens = parse(key, callback);
191 if (tokens != null) {
192 return check(tokens);
195 return false;
198 public void execute(@NotNull String key, @NotNull CustomTemplateCallback callback, @Nullable TemplateInvokationListener listener) {
199 List<MyToken> tokens = parse(key, callback);
200 assert tokens != null;
201 MyInterpreter interpreter = new MyInterpreter(tokens, callback, MyState.WORD, listener);
202 interpreter.invoke(0);
205 private static void fail() {
206 LOG.error("Input string was checked incorrectly during isApplicable() invokation");
209 @NotNull
210 private static String buildAttributesString(@Nullable String id, @NotNull List<String> classes) {
211 StringBuilder result = new StringBuilder();
212 if (id != null) {
213 result.append("id=\"").append(id).append('"');
214 if (classes.size() > 0) {
215 result.append(' ');
218 if (classes.size() > 0) {
219 result.append("class=\"");
220 for (int i = 0; i < classes.size(); i++) {
221 result.append(classes.get(i));
222 if (i < classes.size() - 1) {
223 result.append(' ');
226 result.append('"');
228 return result.toString();
231 private static boolean invokeTemplate(String key, final CustomTemplateCallback callback, final TemplateInvokationListener listener) {
232 if (callback.getFile().getFileType() instanceof HtmlFileType) {
233 String templateKey = null;
234 String id = null;
235 final List<String> classes = new ArrayList<String>();
236 StringBuilder builder = new StringBuilder();
237 char lastDelim = 0;
238 key += MARKER;
239 for (int i = 0, n = key.length(); i < n; i++) {
240 char c = key.charAt(i);
241 if (c == '#' || c == '.' || i == n - 1) {
242 switch (lastDelim) {
243 case 0:
244 templateKey = builder.toString();
245 break;
246 case '#':
247 id = builder.toString();
248 break;
249 case '.':
250 if (builder.length() > 0) {
251 classes.add(builder.toString());
253 break;
255 lastDelim = c;
256 builder = new StringBuilder();
258 else {
259 builder.append(c);
262 String attributes = buildAttributesString(id, classes);
263 return startTemplate(templateKey, callback, listener, attributes.length() > 0 ? ' ' + attributes : null);
265 return startTemplate(key, callback, listener, null);
268 private static boolean startTemplate(String key,
269 CustomTemplateCallback callback,
270 TemplateInvokationListener listener,
271 @Nullable String attributes) {
272 Map<String, String> predefinedValues = null;
273 if (attributes != null) {
274 predefinedValues = new HashMap<String, String>();
275 predefinedValues.put(ATTRS, attributes);
277 if (callback.isLiveTemplateApplicable(key)) {
278 return callback.startTemplate(key, predefinedValues, listener);
280 else {
281 TemplateImpl template = new TemplateImpl("", "");
282 template.addTextSegment('<' + key);
283 if (attributes != null) {
284 template.addVariable(ATTRS, "", "", false);
285 template.addVariableSegment(ATTRS);
287 template.addTextSegment(">");
288 template.addVariableSegment(TemplateImpl.END);
289 template.addTextSegment("</" + key + ">");
290 template.setToReformat(true);
291 return callback.startTemplate(template, predefinedValues, listener);
295 private static boolean hasClosingTag(CharSequence text, CharSequence tagName, int offset, int rightBound) {
296 if (offset + 1 < text.length() && text.charAt(offset) == '<' && text.charAt(offset + 1) == '/') {
297 CharSequence closingTagName = parseTagName(text, offset + 2, rightBound);
298 if (tagName.equals(closingTagName)) {
299 return true;
302 return false;
305 @Nullable
306 private static CharSequence getPrecedingTagName(CharSequence text, int index, int leftBound) {
307 int j = index - 1;
308 while (j >= leftBound && Character.isWhitespace(text.charAt(j))) {
309 j--;
311 if (j < leftBound || text.charAt(j) != '>') {
312 return null;
314 while (j >= leftBound && text.charAt(j) != '<') {
315 j--;
317 if (j < 0) {
318 return null;
320 return parseTagName(text, j + 1, index);
323 @Nullable
324 private static CharSequence parseTagName(CharSequence text, int index, int rightBound) {
325 int j = index;
326 if (rightBound > text.length()) {
327 rightBound = text.length();
329 while (j < rightBound && !Character.isWhitespace(text.charAt(j)) && text.charAt(j) != '>') {
330 j++;
332 if (j >= text.length()) {
333 return null;
335 return text.subSequence(index, j);
338 private class MyInterpreter {
339 private final List<MyToken> myTokens;
340 private final CustomTemplateCallback myCallback;
341 private final TemplateInvokationListener myListener;
342 private MyState myState;
343 private int myEndOffset = -1;
345 private MyInterpreter(List<MyToken> tokens,
346 CustomTemplateCallback callback,
347 MyState initialState,
348 TemplateInvokationListener listener) {
349 myTokens = tokens;
350 myCallback = callback;
351 myListener = listener;
352 myState = initialState;
355 private void fixEndOffset() {
356 if (myEndOffset < 0) {
357 myEndOffset = myCallback.getOffset();
361 private void finish(boolean inSeparateEvent) {
362 Editor editor = myCallback.getEditor();
363 if (myEndOffset >= 0) {
364 editor.getCaretModel().moveToOffset(myEndOffset);
366 editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
367 if (myListener != null) {
368 myListener.finished(inSeparateEvent);
372 private void gotoChild(Object templateBoundsKey) {
373 int startOfTemplate = myCallback.getStartOfTemplate(templateBoundsKey);
374 int endOfTemplate = myCallback.getEndOfTemplate(templateBoundsKey);
375 Editor editor = myCallback.getEditor();
376 int offset = myCallback.getOffset();
377 Document document = myCallback.getEditor().getDocument();
378 CharSequence text = document.getCharsSequence();
379 CharSequence tagName = getPrecedingTagName(text, offset, startOfTemplate);
380 if (tagName != null) {
381 if (!hasClosingTag(text, tagName, offset, endOfTemplate)) {
382 document.insertString(offset, "</" + tagName + '>');
385 else if (offset != endOfTemplate) {
386 tagName = getPrecedingTagName(text, endOfTemplate, startOfTemplate);
387 if (tagName != null) {
388 fixEndOffset();
389 document.insertString(endOfTemplate, "</" + tagName + '>');
390 editor.getCaretModel().moveToOffset(endOfTemplate);
395 public boolean invoke(int startIndex) {
396 final int n = myTokens.size();
397 String templateKey = null;
398 int number = -1;
399 for (int i = startIndex; i < n; i++) {
400 final int finalI = i;
401 MyToken token = myTokens.get(i);
402 switch (myState) {
403 case OPERATION:
404 if (templateKey != null) {
405 if (token instanceof MyMarkerToken || token instanceof MyOperationToken) {
406 final char sign = token instanceof MyOperationToken ? ((MyOperationToken)token).mySign : MARKER;
407 if (sign == MARKER || sign == '+') {
408 final Object key = new Object();
409 myCallback.fixStartOfTemplate(key);
410 TemplateInvokationListener listener = new TemplateInvokationListener() {
411 public void finished(boolean inSeparateEvent) {
412 myState = MyState.WORD;
413 fixEndOffset();
414 if (sign == '+') {
415 myCallback.gotoEndOfTemplate(key);
417 if (inSeparateEvent) {
418 invoke(finalI + 1);
422 if (!invokeTemplate(templateKey, myCallback, listener)) {
423 return false;
425 templateKey = null;
427 else if (sign == '>') {
428 if (!startTemplateAndGotoChild(templateKey, finalI)) {
429 return false;
431 templateKey = null;
433 else if (sign == '*') {
434 myState = MyState.NUMBER;
437 else {
438 fail();
441 break;
442 case WORD:
443 if (token instanceof MyTemplateToken) {
444 templateKey = ((MyTemplateToken)token).myKey;
445 myState = MyState.OPERATION;
447 else {
448 fail();
450 break;
451 case NUMBER:
452 if (token instanceof MyNumberToken) {
453 number = ((MyNumberToken)token).myNumber;
454 myState = MyState.AFTER_NUMBER;
456 else {
457 fail();
459 break;
460 case AFTER_NUMBER:
461 if (token instanceof MyMarkerToken || token instanceof MyOperationToken) {
462 char sign = token instanceof MyOperationToken ? ((MyOperationToken)token).mySign : MARKER;
463 if (sign == MARKER || sign == '+') {
464 if (!invokeTemplateSeveralTimes(templateKey, number, finalI)) {
465 return false;
467 templateKey = null;
469 else if (number > 1) {
470 return invokeTemplateAndProcessTail(templateKey, i + 1, number);
472 else {
473 assert number == 1;
474 if (!startTemplateAndGotoChild(templateKey, finalI)) {
475 return false;
477 templateKey = null;
479 myState = MyState.WORD;
481 else {
482 fail();
484 break;
487 finish(startIndex == n);
488 return true;
491 private boolean startTemplateAndGotoChild(String templateKey, final int index) {
492 final Object key = new Object();
493 myCallback.fixStartOfTemplate(key);
494 TemplateInvokationListener listener = new TemplateInvokationListener() {
495 public void finished(boolean inSeparateEvent) {
496 myState = MyState.WORD;
497 gotoChild(key);
498 if (inSeparateEvent) {
499 invoke(index + 1);
503 if (!invokeTemplate(templateKey, myCallback, listener)) {
504 return false;
506 return true;
509 private boolean invokeTemplateSeveralTimes(final String templateKey, final int count, final int index) {
510 final Object key = new Object();
511 myCallback.fixStartOfTemplate(key);
512 for (int i = 0; i < count; i++) {
513 final int finalI = i;
514 TemplateInvokationListener listener = new TemplateInvokationListener() {
515 public void finished(boolean inSeparateEvent) {
516 myState = MyState.WORD;
517 fixEndOffset();
518 myCallback.gotoEndOfTemplate(key);
519 if (inSeparateEvent) {
520 int newCount = count - finalI - 1;
521 if (newCount > 0) {
522 invokeTemplateSeveralTimes(templateKey, newCount, index);
524 else {
525 invoke(index + 1);
530 if (!invokeTemplate(templateKey, myCallback, listener)) {
531 return false;
534 return true;
537 private boolean invokeTemplateAndProcessTail(final String templateKey, final int tailStart, final int count) {
538 final Object key = new Object();
539 myCallback.fixStartOfTemplate(key);
540 for (int i = 0; i < count; i++) {
541 final int finalI = i;
542 final boolean[] flag = new boolean[]{false};
543 TemplateInvokationListener listener = new TemplateInvokationListener() {
544 public void finished(boolean inSeparateEvent) {
545 gotoChild(key);
546 MyInterpreter interpreter = new MyInterpreter(myTokens, myCallback, MyState.WORD, new TemplateInvokationListener() {
547 public void finished(boolean inSeparateEvent) {
548 fixEndOffset();
549 myCallback.gotoEndOfTemplate(key);
550 if (inSeparateEvent) {
551 invokeTemplateAndProcessTail(templateKey, tailStart, count - finalI - 1);
555 if (interpreter.invoke(tailStart)) {
556 if (inSeparateEvent) {
557 invokeTemplateAndProcessTail(templateKey, tailStart, count - finalI - 1);
560 else {
561 flag[0] = true;
565 if (!invokeTemplate(templateKey, myCallback, listener) || flag[0]) {
566 return false;
569 finish(count == 0);
570 return true;