26f37bf59d6b311bee9a1286bfe53c57541d7a3b
[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     if (shortcutChar == templateSettings.getDefaultShortcutChar()) {
245       for (final CustomLiveTemplate customLiveTemplate : CustomLiveTemplate.EP_NAME.getExtensions()) {
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               callback.getEditor().getDocument().deleteString(offsetBeforeKey, caretOffset);
255               callback.fixInitialState();
256               customLiveTemplate.expand(key, callback, new TemplateInvokationListener() {
257                 public void finished(boolean inSeparateEvent) {
258                   callback.finish();
259                 }
260               });
261               return true;
262             }
263           }
264         }
265       }
266     }
267     return startNonCustomTemplates(template2argument, editor, processor);
268   }
269
270   private static int getArgumentOffset(int caretOffset, String argument, CharSequence text) {
271     int argumentOffset = caretOffset - argument.length();
272     if (argumentOffset > 0 && text.charAt(argumentOffset - 1) == ' ') {
273       if (argumentOffset - 2 >= 0 && Character.isJavaIdentifierPart(text.charAt(argumentOffset - 2))) {
274         argumentOffset--;
275       }
276     }
277     return argumentOffset;
278   }
279
280   private static int getTemplateStart(TemplateImpl template, String argument, int caretOffset, CharSequence text) {
281     int templateStart;
282     if (argument == null) {
283       templateStart = caretOffset - template.getKey().length();
284     }
285     else {
286       int argOffset = getArgumentOffset(caretOffset, argument, text);
287       templateStart = argOffset - template.getKey().length();
288     }
289     return templateStart;
290   }
291
292   private Map<TemplateImpl, String> findMatchingTemplates(final PsiFile file,
293                                                           Editor editor,
294                                                           char shortcutChar,
295                                                           TemplateSettings templateSettings) {
296     final Document document = editor.getDocument();
297     CharSequence text = document.getCharsSequence();
298     final int caretOffset = editor.getCaretModel().getOffset();
299
300     List<TemplateImpl> candidatesWithoutArgument = findMatchingTemplates(text, caretOffset, shortcutChar, templateSettings, false);
301
302     int argumentOffset = passArgumentBack(text, caretOffset);
303     String argument = null;
304     if (argumentOffset >= 0) {
305       argument = text.subSequence(argumentOffset, caretOffset).toString();
306       if (argumentOffset > 0 && text.charAt(argumentOffset - 1) == ' ') {
307         if (argumentOffset - 2 >= 0 && Character.isJavaIdentifierPart(text.charAt(argumentOffset - 2))) {
308           argumentOffset--;
309         }
310       }
311     }
312     List<TemplateImpl> candidatesWithArgument = findMatchingTemplates(text, argumentOffset, shortcutChar, templateSettings, true);
313
314     if (candidatesWithArgument.isEmpty() && candidatesWithoutArgument.isEmpty()) {
315       return null;
316     }
317
318     CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
319       public void run() {
320         PsiDocumentManager.getInstance(myProject).commitDocument(document);
321       }
322     }, "", null);
323
324     candidatesWithoutArgument = filterApplicableCandidates(file, caretOffset, candidatesWithoutArgument);
325     candidatesWithArgument = filterApplicableCandidates(file, argumentOffset, candidatesWithArgument);
326     Map<TemplateImpl, String> candidate2Argument = new HashMap<TemplateImpl, String>();
327     addToMap(candidate2Argument, candidatesWithoutArgument, null);
328     addToMap(candidate2Argument, candidatesWithArgument, argument);
329     return candidate2Argument;
330   }
331
332   private boolean startNonCustomTemplates(Map<TemplateImpl, String> template2argument,
333                                           Editor editor,
334                                           PairProcessor<String, String> processor) {
335     final int caretOffset = editor.getCaretModel().getOffset();
336     final Document document = editor.getDocument();
337     CharSequence text = document.getCharsSequence();
338
339     if (template2argument == null || template2argument.size() == 0) {
340       return false;
341     }
342     if (!FileDocumentManager.getInstance().requestWriting(editor.getDocument(), myProject)) {
343       return false;
344     }
345
346     if (template2argument.size() == 1) {
347       TemplateImpl template = template2argument.keySet().iterator().next();
348       String argument = template2argument.get(template);
349       int templateStart = getTemplateStart(template, argument, caretOffset, text);
350       startTemplateWithPrefix(editor, template, templateStart, processor, argument);
351     }
352     else {
353       ListTemplatesHandler.showTemplatesLookup(myProject, editor, template2argument);
354     }
355     return true;
356   }
357
358   public static List<TemplateImpl> findMatchingTemplates(CharSequence text,
359                                                          int caretOffset,
360                                                          char shortcutChar,
361                                                          TemplateSettings settings,
362                                                          boolean hasArgument) {
363     String key;
364     List<TemplateImpl> candidates = Collections.emptyList();
365     for (int i = settings.getMaxKeyLength(); i >= 1; i--) {
366       int wordStart = caretOffset - i;
367       if (wordStart < 0) {
368         continue;
369       }
370       key = text.subSequence(wordStart, caretOffset).toString();
371       if (Character.isJavaIdentifierStart(key.charAt(0))) {
372         if (wordStart > 0 && Character.isJavaIdentifierPart(text.charAt(wordStart - 1))) {
373           continue;
374         }
375       }
376
377       candidates = settings.collectMatchingCandidates(key, shortcutChar, hasArgument);
378       if (!candidates.isEmpty()) break;
379     }
380     return candidates;
381   }
382
383   public void startTemplateWithPrefix(final Editor editor,
384                                       final TemplateImpl template,
385                                       @Nullable final PairProcessor<String, String> processor,
386                                       @Nullable String argument) {
387     final int caretOffset = editor.getCaretModel().getOffset();
388     String key = template.getKey();
389     int startOffset = caretOffset - key.length();
390     if (argument != null) {
391       if (!isDelimiter(key.charAt(key.length() - 1))) {
392         // pass space
393         startOffset--;
394       }
395       startOffset -= argument.length();
396     }
397     startTemplateWithPrefix(editor, template, startOffset, processor, argument);
398   }
399
400   public void startTemplateWithPrefix(final Editor editor,
401                                       final TemplateImpl template,
402                                       final int templateStart,
403                                       @Nullable final PairProcessor<String, String> processor,
404                                       @Nullable final String argument) {
405     final int caretOffset = editor.getCaretModel().getOffset();
406     final TemplateState templateState = initTemplateState(editor);
407     CommandProcessor commandProcessor = CommandProcessor.getInstance();
408     commandProcessor.executeCommand(myProject, new Runnable() {
409       public void run() {
410         editor.getDocument().deleteString(templateStart, caretOffset);
411         editor.getCaretModel().moveToOffset(templateStart);
412         editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
413         editor.getSelectionModel().removeSelection();
414         Map<String, String> predefinedVarValues = null;
415         if (argument != null) {
416           predefinedVarValues = new HashMap<String, String>();
417           predefinedVarValues.put(TemplateImpl.ARG, argument);
418         }
419         templateState.start(template, processor, predefinedVarValues);
420       }
421     }, CodeInsightBundle.message("insert.code.template.command"), null);
422   }
423
424   public static List<TemplateImpl> filterApplicableCandidates(PsiFile file, int caretOffset, List<TemplateImpl> candidates) {
425     List<TemplateImpl> result = new ArrayList<TemplateImpl>();
426     for (TemplateImpl candidate : candidates) {
427       if (isApplicable(file, caretOffset - candidate.getKey().length(), candidate)) {
428         result.add(candidate);
429       }
430     }
431     return result;
432   }
433
434   public TemplateContextType getContextType(@NotNull PsiFile file, int offset) {
435     final TemplateContextType[] typeCollection = getAllContextTypes();
436     LinkedList<TemplateContextType> userDefinedExtensionsFirst = new LinkedList<TemplateContextType>();
437     for (TemplateContextType contextType : typeCollection) {
438       if (contextType.getClass().getName().startsWith("com.intellij.codeInsight.template")) {
439         userDefinedExtensionsFirst.addLast(contextType);
440       }
441       else {
442         userDefinedExtensionsFirst.addFirst(contextType);
443       }
444     }
445     for (TemplateContextType contextType : userDefinedExtensionsFirst) {
446       if (contextType.isInContext(file, offset)) {
447         return contextType;
448       }
449     }
450     assert false : "OtherContextType should match any context";
451     return null;
452   }
453
454   public static TemplateContextType[] getAllContextTypes() {
455     return Extensions.getExtensions(TemplateContextType.EP_NAME);
456   }
457
458   @NotNull
459   public String getComponentName() {
460     return "TemplateManager";
461   }
462
463   @Nullable
464   public Template getActiveTemplate(@NotNull Editor editor) {
465     final TemplateState templateState = getTemplateState(editor);
466     return templateState != null ? templateState.getTemplate() : null;
467   }
468
469   public static boolean isApplicable(PsiFile file, int offset, TemplateImpl template) {
470     TemplateManager instance = getInstance(file.getProject());
471     TemplateContext context = template.getTemplateContext();
472     if (context.isEnabled(instance.getContextType(file, offset))) {
473       return true;
474     }
475     Language baseLanguage = file.getViewProvider().getBaseLanguage();
476     if (baseLanguage != file.getLanguage()) {
477       PsiFile basePsi = file.getViewProvider().getPsi(baseLanguage);
478       if (basePsi != null && context.isEnabled(instance.getContextType(basePsi, offset))) {
479         return true;
480       }
481     }
482
483     // if we have, for example, a Ruby fragment in RHTML selected with its exact bounds, the file language and the base
484     // language will be ERb, so we won't match HTML templates for it. but they're actually valid
485     if (offset > 0) {
486       final Language prevLanguage = PsiUtilBase.getLanguageAtOffset(file, offset - 1);
487       final PsiFile prevPsi = file.getViewProvider().getPsi(prevLanguage);
488       if (prevPsi != null && context.isEnabled(instance.getContextType(prevPsi, offset - 1))) {
489         return true;
490       }
491     }
492
493     return false;
494   }
495 }