renaming; show popup, if it is simple live template invokation by zen-coding shortcut...
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / template / impl / TemplateManagerImpl.java
1 /*
2  * Copyright 2000-2009 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
17 package com.intellij.codeInsight.template.impl;
18
19 import com.intellij.codeInsight.CodeInsightBundle;
20 import com.intellij.codeInsight.template.*;
21 import com.intellij.lang.Language;
22 import com.intellij.openapi.Disposable;
23 import com.intellij.openapi.application.ApplicationManager;
24 import com.intellij.openapi.command.CommandProcessor;
25 import com.intellij.openapi.components.ProjectComponent;
26 import com.intellij.openapi.editor.*;
27 import com.intellij.openapi.editor.event.EditorFactoryAdapter;
28 import com.intellij.openapi.editor.event.EditorFactoryEvent;
29 import com.intellij.openapi.editor.event.EditorFactoryListener;
30 import com.intellij.openapi.extensions.Extensions;
31 import com.intellij.openapi.fileEditor.FileDocumentManager;
32 import com.intellij.openapi.project.Project;
33 import com.intellij.openapi.util.Disposer;
34 import com.intellij.openapi.util.Key;
35 import com.intellij.psi.PsiDocumentManager;
36 import com.intellij.psi.PsiFile;
37 import com.intellij.psi.util.PsiUtilBase;
38 import com.intellij.util.PairProcessor;
39 import com.intellij.util.containers.HashMap;
40 import org.jetbrains.annotations.NotNull;
41 import org.jetbrains.annotations.Nullable;
42
43 import java.util.*;
44
45 public class TemplateManagerImpl extends TemplateManager implements ProjectComponent {
46   protected Project myProject;
47   private boolean myTemplateTesting;
48   private final List<Disposable> myDisposables = new ArrayList<Disposable>();
49
50   private static final Key<TemplateState> TEMPLATE_STATE_KEY = Key.create("TEMPLATE_STATE_KEY");
51
52   public TemplateManagerImpl(Project project) {
53     myProject = project;
54   }
55
56   public void disposeComponent() {
57     for (Disposable disposable : myDisposables) {
58       disposable.dispose();
59     }
60     myDisposables.clear();
61   }
62
63   public void initComponent() {
64   }
65
66   public void projectClosed() {
67   }
68
69   public void projectOpened() {
70     final EditorFactoryListener myEditorFactoryListener = new EditorFactoryAdapter() {
71       public void editorReleased(EditorFactoryEvent event) {
72         Editor editor = event.getEditor();
73         if (editor.getProject() != null && editor.getProject() != myProject) return;
74         TemplateState tState = getTemplateState(editor);
75         if (tState != null) {
76           disposeState(tState);
77         }
78         editor.putUserData(TEMPLATE_STATE_KEY, null);
79       }
80     };
81     EditorFactory.getInstance().addEditorFactoryListener(myEditorFactoryListener);
82     Disposer.register(myProject, new Disposable() {
83       public void dispose() {
84         EditorFactory.getInstance().removeEditorFactoryListener(myEditorFactoryListener);
85       }
86     });
87   }
88
89   public void setTemplateTesting(final boolean templateTesting) {
90     myTemplateTesting = templateTesting;
91   }
92
93   private void disposeState(final TemplateState tState) {
94     tState.dispose();
95     myDisposables.remove(tState);
96   }
97
98   public Template createTemplate(@NotNull String key, String group) {
99     return new TemplateImpl(key, group);
100   }
101
102   public Template createTemplate(@NotNull String key, String group, String text) {
103     return new TemplateImpl(key, text, group);
104   }
105
106   public static TemplateState getTemplateState(Editor editor) {
107     return editor.getUserData(TEMPLATE_STATE_KEY);
108   }
109
110   void clearTemplateState(final Editor editor) {
111     TemplateState prevState = getTemplateState(editor);
112     if (prevState != null) {
113       disposeState(prevState);
114     }
115     editor.putUserData(TEMPLATE_STATE_KEY, null);
116   }
117
118   private TemplateState initTemplateState(final Editor editor) {
119     clearTemplateState(editor);
120     TemplateState state = new TemplateState(myProject, editor);
121     myDisposables.add(state);
122     editor.putUserData(TEMPLATE_STATE_KEY, state);
123     return state;
124   }
125
126   public boolean startTemplate(@NotNull Editor editor, char shortcutChar) {
127     return startTemplate(editor, shortcutChar, null);
128   }
129
130   public void startTemplate(@NotNull final Editor editor, @NotNull Template template) {
131     startTemplate(editor, template, null);
132   }
133
134   public void startTemplate(@NotNull Editor editor, String selectionString, @NotNull Template template) {
135     startTemplate(editor, selectionString, template, true, null, null, null);
136   }
137
138   public void startTemplate(@NotNull Editor editor,
139                             @NotNull Template template,
140                             TemplateEditingListener listener,
141                             final PairProcessor<String, String> processor) {
142     startTemplate(editor, null, template, true, listener, processor, null);
143   }
144
145   private void startTemplate(final Editor editor,
146                              final String selectionString,
147                              final Template template,
148                              boolean inSeparateCommand,
149                              TemplateEditingListener listener,
150                              final PairProcessor<String, String> processor,
151                              final Map<String, String> predefinedVarValues) {
152     final TemplateState templateState = initTemplateState(editor);
153
154     templateState.getProperties().put(ExpressionContext.SELECTION, selectionString);
155
156     if (listener != null) {
157       templateState.addTemplateStateListener(listener);
158     }
159     Runnable r = new Runnable() {
160       public void run() {
161         if (selectionString != null) {
162           ApplicationManager.getApplication().runWriteAction(new Runnable() {
163             public void run() {
164               EditorModificationUtil.deleteSelectedText(editor);
165             }
166           });
167         }
168         else {
169           editor.getSelectionModel().removeSelection();
170         }
171         templateState.start((TemplateImpl)template, processor, predefinedVarValues);
172       }
173     };
174     if (inSeparateCommand) {
175       CommandProcessor.getInstance().executeCommand(myProject, r, CodeInsightBundle.message("insert.code.template.command"), null);
176     }
177     else {
178       r.run();
179     }
180
181     if (shouldSkipInTests()) {
182       if (!templateState.isFinished()) templateState.gotoEnd();
183     }
184   }
185
186   public boolean shouldSkipInTests() {
187     return ApplicationManager.getApplication().isUnitTestMode() && !myTemplateTesting;
188   }
189
190   public void startTemplate(@NotNull final Editor editor, @NotNull final Template template, TemplateEditingListener listener) {
191     startTemplate(editor, null, template, true, listener, null, null);
192   }
193
194   public void startTemplate(@NotNull final Editor editor,
195                             @NotNull final Template template,
196                             boolean inSeparateCommand,
197                             Map<String, String> predefinedVarValues,
198                             TemplateEditingListener listener) {
199     startTemplate(editor, null, template, inSeparateCommand, listener, null, predefinedVarValues);
200   }
201
202   private static int passArgumentBack(CharSequence text, int caretOffset) {
203     int i = caretOffset - 1;
204     for (; i >= 0; i--) {
205       char c = text.charAt(i);
206       if (isDelimiter(c)) {
207         break;
208       }
209     }
210     return i + 1;
211   }
212
213   private static boolean isDelimiter(char c) {
214     return !Character.isJavaIdentifierPart(c);
215   }
216
217   private static <T, U> void addToMap(@NotNull Map<T, U> map, @NotNull Collection<? extends T> keys, U value) {
218     for (T key : keys) {
219       map.put(key, value);
220     }
221   }
222
223   private static boolean containsTemplateStartingBefore(Map<TemplateImpl, String> template2argument,
224                                                         int offset,
225                                                         int caretOffset,
226                                                         CharSequence text) {
227     for (TemplateImpl template : template2argument.keySet()) {
228       String argument = template2argument.get(template);
229       int templateStart = getTemplateStart(template, argument, caretOffset, text);
230       if (templateStart < offset) {
231         return true;
232       }
233     }
234     return false;
235   }
236
237   public boolean startTemplate(final Editor editor, char shortcutChar, final PairProcessor<String, String> processor) {
238     PsiFile file = PsiUtilBase.getPsiFileInEditor(editor, myProject);
239     if (file == null) return false;
240     TemplateSettings templateSettings = TemplateSettings.getInstance();
241
242     Map<TemplateImpl, String> template2argument = findMatchingTemplates(file, editor, shortcutChar, templateSettings);
243
244     for (final CustomLiveTemplate customLiveTemplate : CustomLiveTemplate.EP_NAME.getExtensions()) {
245       if (shortcutChar == customLiveTemplate.getShortcut()) {
246         int caretOffset = editor.getCaretModel().getOffset();
247         if (customLiveTemplate.isApplicable(file, caretOffset, false)) {
248           final CustomTemplateCallback callback = new CustomTemplateCallback(editor, file);
249           String key = customLiveTemplate.computeTemplateKey(callback);
250           if (key != null) {
251             int offsetBeforeKey = caretOffset - key.length();
252             CharSequence text = editor.getDocument().getCharsSequence();
253             if (template2argument == null || !containsTemplateStartingBefore(template2argument, offsetBeforeKey, caretOffset, text)) {
254               customLiveTemplate.expand(key, callback);
255               callback.startAllExpandedTemplates();
256               return true;
257             }
258           }
259         }
260       }
261     }
262     return startNonCustomTemplates(template2argument, editor, processor);
263   }
264
265   private static int getArgumentOffset(int caretOffset, String argument, CharSequence text) {
266     int argumentOffset = caretOffset - argument.length();
267     if (argumentOffset > 0 && text.charAt(argumentOffset - 1) == ' ') {
268       if (argumentOffset - 2 >= 0 && Character.isJavaIdentifierPart(text.charAt(argumentOffset - 2))) {
269         argumentOffset--;
270       }
271     }
272     return argumentOffset;
273   }
274
275   private static int getTemplateStart(TemplateImpl template, String argument, int caretOffset, CharSequence text) {
276     int templateStart;
277     if (argument == null) {
278       templateStart = caretOffset - template.getKey().length();
279     }
280     else {
281       int argOffset = getArgumentOffset(caretOffset, argument, text);
282       templateStart = argOffset - template.getKey().length();
283     }
284     return templateStart;
285   }
286
287   private Map<TemplateImpl, String> findMatchingTemplates(final PsiFile file,
288                                                           Editor editor,
289                                                           char shortcutChar,
290                                                           TemplateSettings templateSettings) {
291     final Document document = editor.getDocument();
292     CharSequence text = document.getCharsSequence();
293     final int caretOffset = editor.getCaretModel().getOffset();
294
295     List<TemplateImpl> candidatesWithoutArgument = findMatchingTemplates(text, caretOffset, shortcutChar, templateSettings, false);
296
297     int argumentOffset = passArgumentBack(text, caretOffset);
298     String argument = null;
299     if (argumentOffset >= 0) {
300       argument = text.subSequence(argumentOffset, caretOffset).toString();
301       if (argumentOffset > 0 && text.charAt(argumentOffset - 1) == ' ') {
302         if (argumentOffset - 2 >= 0 && Character.isJavaIdentifierPart(text.charAt(argumentOffset - 2))) {
303           argumentOffset--;
304         }
305       }
306     }
307     List<TemplateImpl> candidatesWithArgument = findMatchingTemplates(text, argumentOffset, shortcutChar, templateSettings, true);
308
309     if (candidatesWithArgument.isEmpty() && candidatesWithoutArgument.isEmpty()) {
310       return null;
311     }
312
313     CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
314       public void run() {
315         PsiDocumentManager.getInstance(myProject).commitDocument(document);
316       }
317     }, "", null);
318
319     candidatesWithoutArgument = filterApplicableCandidates(file, caretOffset, candidatesWithoutArgument);
320     candidatesWithArgument = filterApplicableCandidates(file, argumentOffset, candidatesWithArgument);
321     Map<TemplateImpl, String> candidate2Argument = new HashMap<TemplateImpl, String>();
322     addToMap(candidate2Argument, candidatesWithoutArgument, null);
323     addToMap(candidate2Argument, candidatesWithArgument, argument);
324     return candidate2Argument;
325   }
326
327   public boolean startNonCustomTemplates(Map<TemplateImpl, String> template2argument,
328                                           Editor editor,
329                                           PairProcessor<String, String> processor) {
330     final int caretOffset = editor.getCaretModel().getOffset();
331     final Document document = editor.getDocument();
332     CharSequence text = document.getCharsSequence();
333
334     if (template2argument == null || template2argument.size() == 0) {
335       return false;
336     }
337     if (!FileDocumentManager.getInstance().requestWriting(editor.getDocument(), myProject)) {
338       return false;
339     }
340
341     if (template2argument.size() == 1) {
342       TemplateImpl template = template2argument.keySet().iterator().next();
343       String argument = template2argument.get(template);
344       int templateStart = getTemplateStart(template, argument, caretOffset, text);
345       startTemplateWithPrefix(editor, template, templateStart, processor, argument);
346     }
347     else {
348       ListTemplatesHandler.showTemplatesLookup(myProject, editor, template2argument);
349     }
350     return true;
351   }
352
353   public static List<TemplateImpl> findMatchingTemplates(CharSequence text,
354                                                          int caretOffset,
355                                                          Character shortcutChar,
356                                                          TemplateSettings settings,
357                                                          boolean hasArgument) {
358     String key;
359     List<TemplateImpl> candidates = Collections.emptyList();
360     for (int i = settings.getMaxKeyLength(); i >= 1; i--) {
361       int wordStart = caretOffset - i;
362       if (wordStart < 0) {
363         continue;
364       }
365       key = text.subSequence(wordStart, caretOffset).toString();
366       if (Character.isJavaIdentifierStart(key.charAt(0))) {
367         if (wordStart > 0 && Character.isJavaIdentifierPart(text.charAt(wordStart - 1))) {
368           continue;
369         }
370       }
371
372       candidates = settings.collectMatchingCandidates(key, shortcutChar, hasArgument);
373       if (!candidates.isEmpty()) break;
374     }
375     return candidates;
376   }
377
378   public void startTemplateWithPrefix(final Editor editor,
379                                       final TemplateImpl template,
380                                       @Nullable final PairProcessor<String, String> processor,
381                                       @Nullable String argument) {
382     final int caretOffset = editor.getCaretModel().getOffset();
383     String key = template.getKey();
384     int startOffset = caretOffset - key.length();
385     if (argument != null) {
386       if (!isDelimiter(key.charAt(key.length() - 1))) {
387         // pass space
388         startOffset--;
389       }
390       startOffset -= argument.length();
391     }
392     startTemplateWithPrefix(editor, template, startOffset, processor, argument);
393   }
394
395   public void startTemplateWithPrefix(final Editor editor,
396                                       final TemplateImpl template,
397                                       final int templateStart,
398                                       @Nullable final PairProcessor<String, String> processor,
399                                       @Nullable final String argument) {
400     final int caretOffset = editor.getCaretModel().getOffset();
401     final TemplateState templateState = initTemplateState(editor);
402     CommandProcessor commandProcessor = CommandProcessor.getInstance();
403     commandProcessor.executeCommand(myProject, new Runnable() {
404       public void run() {
405         editor.getDocument().deleteString(templateStart, caretOffset);
406         editor.getCaretModel().moveToOffset(templateStart);
407         editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
408         editor.getSelectionModel().removeSelection();
409         Map<String, String> predefinedVarValues = null;
410         if (argument != null) {
411           predefinedVarValues = new HashMap<String, String>();
412           predefinedVarValues.put(TemplateImpl.ARG, argument);
413         }
414         templateState.start(template, processor, predefinedVarValues);
415       }
416     }, CodeInsightBundle.message("insert.code.template.command"), null);
417   }
418
419   public static List<TemplateImpl> filterApplicableCandidates(PsiFile file, int caretOffset, List<TemplateImpl> candidates) {
420     List<TemplateImpl> result = new ArrayList<TemplateImpl>();
421     for (TemplateImpl candidate : candidates) {
422       if (isApplicable(file, caretOffset - candidate.getKey().length(), candidate)) {
423         result.add(candidate);
424       }
425     }
426     return result;
427   }
428
429   public TemplateContextType getContextType(@NotNull PsiFile file, int offset) {
430     final TemplateContextType[] typeCollection = getAllContextTypes();
431     LinkedList<TemplateContextType> userDefinedExtensionsFirst = new LinkedList<TemplateContextType>();
432     for (TemplateContextType contextType : typeCollection) {
433       if (contextType.getClass().getName().startsWith("com.intellij.codeInsight.template")) {
434         userDefinedExtensionsFirst.addLast(contextType);
435       }
436       else {
437         userDefinedExtensionsFirst.addFirst(contextType);
438       }
439     }
440     for (TemplateContextType contextType : userDefinedExtensionsFirst) {
441       if (contextType.isInContext(file, offset)) {
442         return contextType;
443       }
444     }
445     assert false : "OtherContextType should match any context";
446     return null;
447   }
448
449   public static TemplateContextType[] getAllContextTypes() {
450     return Extensions.getExtensions(TemplateContextType.EP_NAME);
451   }
452
453   @NotNull
454   public String getComponentName() {
455     return "TemplateManager";
456   }
457
458   @Nullable
459   public Template getActiveTemplate(@NotNull Editor editor) {
460     final TemplateState templateState = getTemplateState(editor);
461     return templateState != null ? templateState.getTemplate() : null;
462   }
463
464   public static boolean isApplicable(PsiFile file, int offset, TemplateImpl template) {
465     TemplateManager instance = getInstance(file.getProject());
466     TemplateContext context = template.getTemplateContext();
467     if (context.isEnabled(instance.getContextType(file, offset))) {
468       return true;
469     }
470     Language baseLanguage = file.getViewProvider().getBaseLanguage();
471     if (baseLanguage != file.getLanguage()) {
472       PsiFile basePsi = file.getViewProvider().getPsi(baseLanguage);
473       if (basePsi != null && context.isEnabled(instance.getContextType(basePsi, offset))) {
474         return true;
475       }
476     }
477
478     // if we have, for example, a Ruby fragment in RHTML selected with its exact bounds, the file language and the base
479     // language will be ERb, so we won't match HTML templates for it. but they're actually valid
480     if (offset > 0) {
481       final Language prevLanguage = PsiUtilBase.getLanguageAtOffset(file, offset - 1);
482       final PsiFile prevPsi = file.getViewProvider().getPsi(prevLanguage);
483       if (prevPsi != null && context.isEnabled(instance.getContextType(prevPsi, offset - 1))) {
484         return true;
485       }
486     }
487
488     return false;
489   }
490 }