don't cache invalid injected editor in lookup (IDEA-146179, EA-54321 - assert: PsiFil...
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / template / impl / ListTemplatesHandler.java
1 /*
2  * Copyright 2000-2014 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.CodeInsightActionHandler;
20 import com.intellij.codeInsight.CodeInsightBundle;
21 import com.intellij.codeInsight.CodeInsightUtilBase;
22 import com.intellij.codeInsight.completion.PlainPrefixMatcher;
23 import com.intellij.codeInsight.hint.HintManager;
24 import com.intellij.codeInsight.lookup.*;
25 import com.intellij.codeInsight.lookup.impl.LookupImpl;
26 import com.intellij.codeInsight.template.CustomLiveTemplate;
27 import com.intellij.codeInsight.template.CustomLiveTemplateBase;
28 import com.intellij.codeInsight.template.CustomTemplateCallback;
29 import com.intellij.codeInsight.template.TemplateManager;
30 import com.intellij.diagnostic.AttachmentFactory;
31 import com.intellij.featureStatistics.FeatureUsageTracker;
32 import com.intellij.openapi.application.Result;
33 import com.intellij.openapi.command.WriteCommandAction;
34 import com.intellij.openapi.diagnostic.Logger;
35 import com.intellij.openapi.editor.Document;
36 import com.intellij.openapi.editor.Editor;
37 import com.intellij.openapi.editor.ex.util.EditorUtil;
38 import com.intellij.openapi.fileEditor.FileDocumentManager;
39 import com.intellij.openapi.project.Project;
40 import com.intellij.openapi.util.Pair;
41 import com.intellij.openapi.util.text.StringUtil;
42 import com.intellij.psi.PsiDocumentManager;
43 import com.intellij.psi.PsiFile;
44 import com.intellij.util.containers.ContainerUtil;
45 import com.intellij.util.containers.MultiMap;
46 import org.jetbrains.annotations.NotNull;
47 import org.jetbrains.annotations.Nullable;
48
49 import java.util.*;
50 import java.util.regex.Pattern;
51
52 public class ListTemplatesHandler implements CodeInsightActionHandler {
53
54   private static final Logger LOG = Logger.getInstance(ListTemplatesHandler.class);
55
56   @Override
57   public void invoke(@NotNull final Project project, @NotNull final Editor editor, @NotNull PsiFile file) {
58     if (!CodeInsightUtilBase.prepareEditorForWrite(editor)) return;
59     if (!FileDocumentManager.getInstance().requestWriting(editor.getDocument(), project)) {
60       return;
61     }
62     EditorUtil.fillVirtualSpaceUntilCaret(editor);
63
64     PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument());
65     int offset = editor.getCaretModel().getOffset();
66     List<TemplateImpl> applicableTemplates = TemplateManagerImpl.listApplicableTemplateWithInsertingDummyIdentifier(editor, file, false);
67
68     Map<TemplateImpl, String> matchingTemplates = filterTemplatesByPrefix(applicableTemplates, editor, offset, false, true);
69     MultiMap<String, CustomLiveTemplateLookupElement> customTemplatesLookupElements = getCustomTemplatesLookupItems(editor, file, offset);
70
71     if (matchingTemplates.isEmpty()) {
72       for (TemplateImpl template : applicableTemplates) {
73         matchingTemplates.put(template, null);
74       }
75     }
76
77     if (matchingTemplates.isEmpty() && customTemplatesLookupElements.isEmpty()) {
78       HintManager.getInstance().showErrorHint(editor, CodeInsightBundle.message("templates.no.defined"));
79       return;
80     }
81
82     showTemplatesLookup(project, editor, file, matchingTemplates, customTemplatesLookupElements);
83   }
84
85   public static Map<TemplateImpl, String> filterTemplatesByPrefix(@NotNull Collection<TemplateImpl> templates, @NotNull Editor editor,
86                                                                   int offset, boolean fullMatch, boolean searchInDescription) {
87     if (offset > editor.getDocument().getTextLength()) {
88       LOG.error("Cannot filter templates, index out of bounds. Offset: " + offset,
89                 AttachmentFactory.createAttachment(editor.getDocument()));
90     }
91     CharSequence documentText = editor.getDocument().getCharsSequence().subSequence(0, offset);
92
93     String prefixWithoutDots = computeDescriptionMatchingPrefix(editor.getDocument(), offset);
94     Pattern prefixSearchPattern = Pattern.compile(".*\\b" + prefixWithoutDots + ".*");
95
96     Map<TemplateImpl, String> matchingTemplates = new TreeMap<TemplateImpl, String>(TemplateListPanel.TEMPLATE_COMPARATOR);
97     for (TemplateImpl template : templates) {
98       String templateKey = template.getKey();
99       if (fullMatch) {
100         int startOffset = documentText.length() - templateKey.length();
101         if (startOffset <= 0 || !Character.isJavaIdentifierPart(documentText.charAt(startOffset - 1))) {
102           // after non-identifier
103           if (StringUtil.endsWith(documentText, templateKey)) {
104             matchingTemplates.put(template, templateKey);
105           }
106         }
107       }
108       else {
109         for (int i = templateKey.length(); i > 0; i--) {
110           String prefix = templateKey.substring(0, i);
111           int startOffset = documentText.length() - i;
112           if (startOffset > 0 && Character.isJavaIdentifierPart(documentText.charAt(startOffset - 1))) {
113             // after java identifier
114             continue;
115           }
116           if (StringUtil.endsWith(documentText, prefix)) {
117             matchingTemplates.put(template, prefix);
118             break;
119           }
120         }
121       }
122
123       if (searchInDescription && !matchingTemplates.containsKey(template)) {
124         String templateDescription = template.getDescription();
125         if (!prefixWithoutDots.isEmpty() && templateDescription != null && prefixSearchPattern.matcher(templateDescription).matches()) {
126           matchingTemplates.put(template, prefixWithoutDots);
127         }
128       }
129     }
130
131     return matchingTemplates;
132   }
133
134   private static void showTemplatesLookup(final Project project,
135                                           final Editor editor,
136                                           final PsiFile file,
137                                           @NotNull Map<TemplateImpl, String> matchingTemplates,
138                                           @NotNull MultiMap<String, CustomLiveTemplateLookupElement> customTemplatesLookupElements) {
139
140     LookupImpl lookup = (LookupImpl)LookupManager.getInstance(project).createLookup(editor, LookupElement.EMPTY_ARRAY, "", new TemplatesArranger());
141     for (Map.Entry<TemplateImpl, String> entry : matchingTemplates.entrySet()) {
142       TemplateImpl template = entry.getKey();
143       lookup.addItem(createTemplateElement(template), new PlainPrefixMatcher(StringUtil.notNullize(entry.getValue())));
144     }
145
146     for (Map.Entry<String, Collection<CustomLiveTemplateLookupElement>> entry : customTemplatesLookupElements.entrySet()) {
147       for (CustomLiveTemplateLookupElement lookupElement : entry.getValue()) {
148         lookup.addItem(lookupElement, new PlainPrefixMatcher(entry.getKey()));
149       }
150     }
151
152     showLookup(lookup, file);
153   }
154
155   public static MultiMap<String, CustomLiveTemplateLookupElement> getCustomTemplatesLookupItems(@NotNull Editor editor,
156                                                                                                 @NotNull PsiFile file,
157                                                                                                 int offset) {
158     final MultiMap<String, CustomLiveTemplateLookupElement> result = MultiMap.create();
159     CustomTemplateCallback customTemplateCallback = new CustomTemplateCallback(editor, file);
160     for (CustomLiveTemplate customLiveTemplate : TemplateManagerImpl.listApplicableCustomTemplates(editor, file, false)) {
161       if (customLiveTemplate instanceof CustomLiveTemplateBase) {
162         String customTemplatePrefix = ((CustomLiveTemplateBase)customLiveTemplate).computeTemplateKeyWithoutContextChecking(customTemplateCallback);
163         if (customTemplatePrefix != null) {
164           result.putValues(customTemplatePrefix, ((CustomLiveTemplateBase)customLiveTemplate).getLookupElements(file, editor, offset));
165         }
166       }
167     }
168     return result;
169   }
170
171   private static LiveTemplateLookupElement createTemplateElement(final TemplateImpl template) {
172     return new LiveTemplateLookupElementImpl(template, false) {
173       @Override
174       public Set<String> getAllLookupStrings() {
175         String description = template.getDescription();
176         if (description == null) {
177           return super.getAllLookupStrings();
178         }
179         return ContainerUtil.newHashSet(getLookupString(), description);
180       }
181     };
182   }
183
184   private static String computePrefix(TemplateImpl template, String argument) {
185     String key = template.getKey();
186     if (argument == null) {
187       return key;
188     }
189     if (key.length() > 0 && Character.isJavaIdentifierPart(key.charAt(key.length() - 1))) {
190       return key + ' ' + argument;
191     }
192     return key + argument;
193   }
194
195   public static void showTemplatesLookup(final Project project, final Editor editor, Map<TemplateImpl, String> template2Argument) {
196     final LookupImpl lookup = (LookupImpl)LookupManager.getInstance(project).createLookup(editor, LookupElement.EMPTY_ARRAY, "",
197                                                                                           new LookupArranger.DefaultArranger());
198     for (TemplateImpl template : template2Argument.keySet()) {
199       String prefix = computePrefix(template, template2Argument.get(template));
200       lookup.addItem(createTemplateElement(template), new PlainPrefixMatcher(prefix));
201     }
202
203     showLookup(lookup, template2Argument);
204   }
205
206   private static void showLookup(LookupImpl lookup, @Nullable Map<TemplateImpl, String> template2Argument) {
207     lookup.addLookupListener(new MyLookupAdapter(template2Argument));
208     lookup.refreshUi(false, true);
209     lookup.showLookup();
210   }
211
212   private static void showLookup(LookupImpl lookup, @NotNull PsiFile file) {
213     lookup.addLookupListener(new MyLookupAdapter(file));
214     lookup.refreshUi(false, true);
215     lookup.showLookup();
216   }
217
218   @Override
219   public boolean startInWriteAction() {
220     return true;
221   }
222
223   private static String computeDescriptionMatchingPrefix(Document document, int offset) {
224     CharSequence chars = document.getCharsSequence();
225     int start = offset;
226     while (true) {
227       if (start == 0) break;
228       char c = chars.charAt(start - 1);
229       if (!(Character.isJavaIdentifierPart(c))) break;
230       start--;
231     }
232     return chars.subSequence(start, offset).toString();
233   }
234
235   private static class MyLookupAdapter extends LookupAdapter {
236     private final Map<TemplateImpl, String> myTemplate2Argument;
237     private final PsiFile myFile;
238
239     public MyLookupAdapter(@Nullable Map<TemplateImpl, String> template2Argument) {
240       myTemplate2Argument = template2Argument;
241       myFile = null;
242     }
243
244     public MyLookupAdapter(@Nullable PsiFile file) {
245       myTemplate2Argument = null;
246       myFile = file;
247     }
248
249     @Override
250     public void itemSelected(final LookupEvent event) {
251       FeatureUsageTracker.getInstance().triggerFeatureUsed("codeassists.liveTemplates");
252       final LookupElement item = event.getItem();
253       final Lookup lookup = event.getLookup();
254       final Project project = lookup.getProject();
255       if (item instanceof LiveTemplateLookupElementImpl) {
256         final TemplateImpl template = ((LiveTemplateLookupElementImpl)item).getTemplate();
257         final String argument = myTemplate2Argument != null ? myTemplate2Argument.get(template) : null;
258         new WriteCommandAction(project) {
259           @Override
260           protected void run(@NotNull Result result) throws Throwable {
261             ((TemplateManagerImpl)TemplateManager.getInstance(project)).startTemplateWithPrefix(lookup.getEditor(), template, null, argument);
262           }
263         }.execute();
264       }
265       else if (item instanceof CustomLiveTemplateLookupElement) {
266         if (myFile != null) {
267           new WriteCommandAction(project) {
268             @Override
269             protected void run(@NotNull Result result) throws Throwable {
270               ((CustomLiveTemplateLookupElement)item).expandTemplate(lookup.getEditor(), myFile);
271             }
272           }.execute();
273         }
274       }
275     }
276   }
277
278   private static class TemplatesArranger extends LookupArranger {
279
280     @Override
281     public Pair<List<LookupElement>, Integer> arrangeItems(@NotNull Lookup lookup, boolean onExplicitAction) {
282       LinkedHashSet<LookupElement> result = new LinkedHashSet<LookupElement>();
283       List<LookupElement> items = getMatchingItems();
284       for (LookupElement item : items) {
285         if (item.getLookupString().startsWith(lookup.itemPattern(item))) {
286           result.add(item);
287         }
288       }
289       result.addAll(items);
290       ArrayList<LookupElement> list = new ArrayList<LookupElement>(result);
291       int selected = lookup.isSelectionTouched() ? list.indexOf(lookup.getCurrentItem()) : 0;
292       return new Pair<List<LookupElement>, Integer>(list, selected >= 0 ? selected : 0);
293     }
294
295     @Override
296     public LookupArranger createEmptyCopy() {
297       return new TemplatesArranger();
298     }
299   }
300 }