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
;
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
{
59 MyTemplateToken(String key
) {
64 private static class MyNumberToken
extends MyToken
{
67 MyNumberToken(int number
) {
72 private static class MyOperationToken
extends MyToken
{
75 MyOperationToken(char 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
) {
86 return Integer
.parseInt(s
);
88 catch (Throwable ignored
) {
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
);
104 private static List
<MyToken
> parse(@NotNull String text
, @NotNull CustomTemplateCallback callback
) {
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
);
115 result
.add(new MyNumberToken(num
));
118 if (key
.length() == 0) {
121 String prefix
= getPrefix(key
);
122 if (callback
.isLiveTemplateApplicable(prefix
)) {
123 if (!prefix
.equals(key
) && !callback
.isTemplateContainsVars(prefix
, ATTRS
)) {
127 else if (prefix
.indexOf('<') >= 0) {
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
);
144 private static boolean check(@NotNull Collection
<MyToken
> tokens
) {
145 MyState state
= MyState
.WORD
;
146 for (MyToken token
: tokens
) {
147 if (token
instanceof MyMarkerToken
) {
152 if (token
instanceof MyOperationToken
) {
153 state
= ((MyOperationToken
)token
).mySign
== '*' ? MyState
.NUMBER
: MyState
.WORD
;
160 if (token
instanceof MyTemplateToken
) {
161 state
= MyState
.OPERATION
;
168 if (token
instanceof MyNumberToken
) {
169 state
= MyState
.AFTER_NUMBER
;
176 if (token
instanceof MyOperationToken
&& ((MyOperationToken
)token
).mySign
!= '*') {
177 state
= MyState
.WORD
;
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
);
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");
210 private static String
buildAttributesString(@Nullable String id
, @NotNull List
<String
> classes
) {
211 StringBuilder result
= new StringBuilder();
213 result
.append("id=\"").append(id
).append('"');
214 if (classes
.size() > 0) {
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) {
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;
235 final List
<String
> classes
= new ArrayList
<String
>();
236 StringBuilder builder
= new StringBuilder();
239 for (int i
= 0, n
= key
.length(); i
< n
; i
++) {
240 char c
= key
.charAt(i
);
241 if (c
== '#' || c
== '.' || i
== n
- 1) {
244 templateKey
= builder
.toString();
247 id
= builder
.toString();
250 if (builder
.length() > 0) {
251 classes
.add(builder
.toString());
256 builder
= new StringBuilder();
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
);
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
)) {
306 private static CharSequence
getPrecedingTagName(CharSequence text
, int index
, int leftBound
) {
308 while (j
>= leftBound
&& Character
.isWhitespace(text
.charAt(j
))) {
311 if (j
< leftBound
|| text
.charAt(j
) != '>') {
314 while (j
>= leftBound
&& text
.charAt(j
) != '<') {
320 return parseTagName(text
, j
+ 1, index
);
324 private static CharSequence
parseTagName(CharSequence text
, int index
, int rightBound
) {
326 if (rightBound
> text
.length()) {
327 rightBound
= text
.length();
329 while (j
< rightBound
&& !Character
.isWhitespace(text
.charAt(j
)) && text
.charAt(j
) != '>') {
332 if (j
>= text
.length()) {
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
) {
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) {
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;
399 for (int i
= startIndex
; i
< n
; i
++) {
400 final int finalI
= i
;
401 MyToken token
= myTokens
.get(i
);
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
;
415 myCallback
.gotoEndOfTemplate(key
);
417 if (inSeparateEvent
) {
422 if (!invokeTemplate(templateKey
, myCallback
, listener
)) {
427 else if (sign
== '>') {
428 if (!startTemplateAndGotoChild(templateKey
, finalI
)) {
433 else if (sign
== '*') {
434 myState
= MyState
.NUMBER
;
443 if (token
instanceof MyTemplateToken
) {
444 templateKey
= ((MyTemplateToken
)token
).myKey
;
445 myState
= MyState
.OPERATION
;
452 if (token
instanceof MyNumberToken
) {
453 number
= ((MyNumberToken
)token
).myNumber
;
454 myState
= MyState
.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
)) {
469 else if (number
> 1) {
470 return invokeTemplateAndProcessTail(templateKey
, i
+ 1, number
);
474 if (!startTemplateAndGotoChild(templateKey
, finalI
)) {
479 myState
= MyState
.WORD
;
487 finish(startIndex
== n
);
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
;
498 if (inSeparateEvent
) {
503 if (!invokeTemplate(templateKey
, myCallback
, listener
)) {
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
;
518 myCallback
.gotoEndOfTemplate(key
);
519 if (inSeparateEvent
) {
520 int newCount
= count
- finalI
- 1;
522 invokeTemplateSeveralTimes(templateKey
, newCount
, index
);
530 if (!invokeTemplate(templateKey
, myCallback
, listener
)) {
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
) {
546 MyInterpreter interpreter
= new MyInterpreter(myTokens
, myCallback
, MyState
.WORD
, new TemplateInvokationListener() {
547 public void finished(boolean inSeparateEvent
) {
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);
565 if (!invokeTemplate(templateKey
, myCallback
, listener
) || flag
[0]) {