Merge branch 'set-original' of https://github.com/dzharkov/intellij-community
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / template / postfix / templates / PostfixLiveTemplate.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 package com.intellij.codeInsight.template.postfix.templates;
17
18 import com.google.common.collect.Sets;
19 import com.intellij.codeInsight.template.CustomLiveTemplateBase;
20 import com.intellij.codeInsight.template.CustomTemplateCallback;
21 import com.intellij.codeInsight.template.impl.CustomLiveTemplateLookupElement;
22 import com.intellij.codeInsight.template.impl.TemplateSettings;
23 import com.intellij.codeInsight.template.postfix.completion.PostfixTemplateLookupElement;
24 import com.intellij.codeInsight.template.postfix.settings.PostfixTemplatesSettings;
25 import com.intellij.diagnostic.AttachmentFactory;
26 import com.intellij.featureStatistics.FeatureUsageTracker;
27 import com.intellij.lang.Language;
28 import com.intellij.openapi.application.ApplicationManager;
29 import com.intellij.openapi.command.CommandProcessor;
30 import com.intellij.openapi.command.undo.UndoConstants;
31 import com.intellij.openapi.diagnostic.Logger;
32 import com.intellij.openapi.editor.Document;
33 import com.intellij.openapi.editor.Editor;
34 import com.intellij.openapi.util.Condition;
35 import com.intellij.openapi.util.Conditions;
36 import com.intellij.openapi.util.text.StringUtil;
37 import com.intellij.openapi.vfs.VirtualFile;
38 import com.intellij.psi.PsiDocumentManager;
39 import com.intellij.psi.PsiElement;
40 import com.intellij.psi.PsiFile;
41 import com.intellij.psi.PsiFileFactory;
42 import com.intellij.psi.impl.source.PsiFileImpl;
43 import com.intellij.psi.util.PsiUtilCore;
44 import com.intellij.util.containers.ContainerUtil;
45 import org.jetbrains.annotations.NotNull;
46 import org.jetbrains.annotations.Nullable;
47
48 import java.util.Collection;
49 import java.util.Set;
50
51 public class PostfixLiveTemplate extends CustomLiveTemplateBase {
52   public static final String POSTFIX_TEMPLATE_ID = "POSTFIX_TEMPLATE_ID";
53   private static final Logger LOG = Logger.getInstance(PostfixLiveTemplate.class);
54
55   @NotNull
56   public Set<String> getAllTemplateKeys(PsiFile file, int offset) {
57     Set<String> keys = Sets.newHashSet();
58     Language language = PsiUtilCore.getLanguageAtOffset(file, offset);
59     for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(language)) {
60       keys.addAll(getKeys(provider));
61     }
62     return keys;
63   }
64
65   @Nullable
66   private static String computeTemplateKeyWithoutContextChecking(@NotNull PostfixTemplateProvider provider,
67                                                                  @NotNull CharSequence documentContent,
68                                                                  int currentOffset) {
69     int startOffset = currentOffset;
70     if (documentContent.length() < startOffset) {
71       return null;
72     }
73
74     while (startOffset > 0) {
75       char currentChar = documentContent.charAt(startOffset - 1);
76       if (!Character.isJavaIdentifierPart(currentChar)) {
77         if (!provider.isTerminalSymbol(currentChar)) {
78           return null;
79         }
80         startOffset--;
81         break;
82       }
83       startOffset--;
84     }
85     return String.valueOf(documentContent.subSequence(startOffset, currentOffset));
86   }
87
88   @Nullable
89   @Override
90   public String computeTemplateKey(@NotNull CustomTemplateCallback callback) {
91     Editor editor = callback.getEditor();
92     CharSequence charsSequence = editor.getDocument().getCharsSequence();
93     int offset = editor.getCaretModel().getOffset();
94     for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(getLanguage(callback))) {
95       String key = computeTemplateKeyWithoutContextChecking(provider, charsSequence, offset);
96       if (key != null && isApplicableTemplate(provider, key, callback.getFile(), editor)) {
97         return key;
98       }
99     }
100     return null;
101   }
102
103   @Nullable
104   @Override
105   public String computeTemplateKeyWithoutContextChecking(@NotNull CustomTemplateCallback callback) {
106     Editor editor = callback.getEditor();
107     int currentOffset = editor.getCaretModel().getOffset();
108     for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(getLanguage(callback))) {
109       String key = computeTemplateKeyWithoutContextChecking(provider, editor.getDocument().getCharsSequence(), currentOffset);
110       if (key != null) return key;
111     }
112     return null;
113   }
114
115   @Override
116   public boolean supportsMultiCaret() {
117     return false;
118   }
119
120   @Override
121   public void expand(@NotNull final String key, @NotNull final CustomTemplateCallback callback) {
122     ApplicationManager.getApplication().assertIsDispatchThread();
123     FeatureUsageTracker.getInstance().triggerFeatureUsed("editing.completion.postfix");
124
125     Editor editor = callback.getEditor();
126     for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(getLanguage(callback))) {
127       PostfixTemplate postfixTemplate = getTemplate(provider, key);
128       if (postfixTemplate != null) {
129         final PsiFile file = callback.getContext().getContainingFile();
130         if (isApplicableTemplate(provider, key, file, editor)) {
131           int offset = deleteTemplateKey(file, editor, key);
132           try {
133             provider.preExpand(file, editor);
134             PsiElement context = CustomTemplateCallback.getContext(file, positiveOffset(offset));
135             expandTemplate(postfixTemplate, editor, context);
136           }
137           finally {
138             provider.afterExpand(file, editor);
139           }
140         }
141         // don't care about errors in multiCaret mode
142         else if (editor.getCaretModel().getAllCarets().size() == 1) {
143           LOG.error("Template not found by key: " + key + "; offset = " + callback.getOffset(), 
144                     AttachmentFactory.createAttachment(callback.getFile().getVirtualFile()));
145         }
146         return;
147       }
148     }
149
150     // don't care about errors in multiCaret mode
151     if (editor.getCaretModel().getAllCarets().size() == 1) {
152       LOG.error("Template not found by key: " + key + "; offset = " + callback.getOffset(), 
153                     AttachmentFactory.createAttachment(callback.getFile().getVirtualFile()));
154     }
155   }
156
157   @Override
158   public boolean isApplicable(PsiFile file, int offset, boolean wrapping) {
159     PostfixTemplatesSettings settings = PostfixTemplatesSettings.getInstance();
160     if (wrapping || file == null || settings == null || !settings.isPostfixTemplatesEnabled()) {
161       return false;
162     }
163     Language language = PsiUtilCore.getLanguageAtOffset(file, offset);
164     for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(language)) {
165       if (StringUtil.isNotEmpty(computeTemplateKeyWithoutContextChecking(provider, file.getText(), offset + 1))) {
166         return true;
167       }
168     }
169     return false;
170   }
171
172   @Override
173   public boolean supportsWrapping() {
174     return false;
175   }
176
177   @Override
178   public void wrap(@NotNull String selection, @NotNull CustomTemplateCallback callback) {
179     throw new UnsupportedOperationException();
180   }
181
182   @NotNull
183   @Override
184   public String getTitle() {
185     return "Postfix";
186   }
187
188   @Override
189   public char getShortcut() {
190     PostfixTemplatesSettings settings = PostfixTemplatesSettings.getInstance();
191     return settings != null ? (char)settings.getShortcut() : TemplateSettings.TAB_CHAR;
192   }
193
194   @Override
195   public boolean hasCompletionItem(@NotNull PsiFile file, int offset) {
196     return true;
197   }
198
199   @NotNull
200   @Override
201   public Collection<? extends CustomLiveTemplateLookupElement> getLookupElements(@NotNull PsiFile file,
202                                                                                  @NotNull Editor editor,
203                                                                                  int offset) {
204     Collection<CustomLiveTemplateLookupElement> result = ContainerUtil.newHashSet();
205     CustomTemplateCallback callback = new CustomTemplateCallback(editor, file);
206     for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(getLanguage(callback))) {
207       String key = computeTemplateKeyWithoutContextChecking(callback);
208       if (key != null && editor.getCaretModel().getCaretCount() == 1) {
209         Condition<PostfixTemplate> isApplicationTemplateFunction = createIsApplicationTemplateFunction(provider, key, file, editor);
210         for (PostfixTemplate postfixTemplate : provider.getTemplates()) {
211           if (isApplicationTemplateFunction.value(postfixTemplate)) {
212             result.add(new PostfixTemplateLookupElement(this, postfixTemplate, postfixTemplate.getKey(), provider, false));
213           }
214         }
215       }
216     }
217
218     return result;
219   }
220
221   private static void expandTemplate(@NotNull final PostfixTemplate template,
222                                      @NotNull final Editor editor,
223                                      @NotNull final PsiElement context) {
224     ApplicationManager.getApplication().runWriteAction(() -> CommandProcessor.getInstance().executeCommand(context.getProject(), () -> template.expand(context, editor), "Expand postfix template", POSTFIX_TEMPLATE_ID));
225   }
226
227
228   private static int deleteTemplateKey(@NotNull final PsiFile file, @NotNull final Editor editor, @NotNull final String key) {
229     ApplicationManager.getApplication().assertIsDispatchThread();
230
231     final int currentOffset = editor.getCaretModel().getOffset();
232     final int newOffset = currentOffset - key.length();
233     ApplicationManager.getApplication().runWriteAction(() -> CommandProcessor.getInstance().runUndoTransparentAction(() -> {
234       Document document = editor.getDocument();
235       document.deleteString(newOffset, currentOffset);
236       editor.getCaretModel().moveToOffset(newOffset);
237       PsiDocumentManager.getInstance(file.getProject()).commitDocument(document);
238     }));
239     return newOffset;
240   }
241
242   private static Condition<PostfixTemplate> createIsApplicationTemplateFunction(@NotNull final PostfixTemplateProvider provider,
243                                                                                 @NotNull String key,
244                                                                                 @NotNull PsiFile file,
245                                                                                 @NotNull Editor editor) {
246     int currentOffset = editor.getCaretModel().getOffset();
247     final int newOffset = currentOffset - key.length();
248     CharSequence fileContent = editor.getDocument().getCharsSequence();
249     StringBuilder fileContentWithoutKey = new StringBuilder();
250     fileContentWithoutKey.append(fileContent.subSequence(0, newOffset));
251     fileContentWithoutKey.append(fileContent.subSequence(currentOffset, fileContent.length()));
252     PsiFile copyFile = copyFile(file, fileContentWithoutKey);
253     Document copyDocument = copyFile.getViewProvider().getDocument();
254     if (copyDocument == null) {
255       return Conditions.alwaysFalse();
256     }
257
258     copyFile = provider.preCheck(copyFile, editor, newOffset);
259     copyDocument = copyFile.getViewProvider().getDocument();
260     if (copyDocument == null) {
261       return Conditions.alwaysFalse();
262     }
263
264     final PsiElement context = CustomTemplateCallback.getContext(copyFile, positiveOffset(newOffset));
265     final Document finalCopyDocument = copyDocument;
266     return template -> template != null && template.isEnabled(provider) && template.isApplicable(context, finalCopyDocument, newOffset);
267   }
268
269   @NotNull
270   public static PsiFile copyFile(@NotNull PsiFile file, @NotNull StringBuilder fileContentWithoutKey) {
271     final PsiFileFactory psiFileFactory = PsiFileFactory.getInstance(file.getProject());
272     PsiFile copy = psiFileFactory.createFileFromText(file.getName(), file.getFileType(), fileContentWithoutKey);
273
274     if (copy instanceof PsiFileImpl) {
275       ((PsiFileImpl) copy).setOriginalFile(file);
276     }
277
278     VirtualFile vFile = copy.getVirtualFile();
279     if (vFile != null) {
280       vFile.putUserData(UndoConstants.DONT_RECORD_UNDO, Boolean.TRUE);
281     }
282     return copy;
283   }
284
285   public static boolean isApplicableTemplate(@NotNull PostfixTemplateProvider provider,
286                                              @NotNull String key,
287                                              @NotNull PsiFile file,
288                                              @NotNull Editor editor) {
289     return createIsApplicationTemplateFunction(provider, key, file, editor).value(getTemplate(provider, key));
290   }
291
292   @NotNull
293   private static Set<String> getKeys(@NotNull PostfixTemplateProvider provider) {
294     Set<String> result = ContainerUtil.newHashSet();
295     for (PostfixTemplate template : provider.getTemplates()) {
296       result.add(template.getKey());
297     }
298
299     return result;
300   }
301
302   @Nullable
303   private static PostfixTemplate getTemplate(@NotNull PostfixTemplateProvider provider, @Nullable String key) {
304     for (PostfixTemplate template : provider.getTemplates()) {
305       if (template.getKey().equals(key)) {
306         return template;
307       }
308     }
309     return null;
310   }
311
312   private static Language getLanguage(@NotNull CustomTemplateCallback callback) {
313     return callback.getContext().getLanguage();
314   }
315
316   private static int positiveOffset(int offset) {
317     return offset > 0 ? offset - 1 : offset;
318   }
319 }