| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.codeInsight.template.postfix.templates; |
| |
| import com.google.common.collect.Sets; |
| import com.intellij.codeInsight.template.CustomLiveTemplateBase; |
| import com.intellij.codeInsight.template.CustomTemplateCallback; |
| import com.intellij.codeInsight.template.impl.CustomLiveTemplateLookupElement; |
| import com.intellij.codeInsight.template.impl.TemplateSettings; |
| import com.intellij.codeInsight.template.postfix.completion.PostfixTemplateLookupElement; |
| import com.intellij.codeInsight.template.postfix.settings.PostfixTemplatesSettings; |
| import com.intellij.diagnostic.AttachmentFactory; |
| import com.intellij.featureStatistics.FeatureUsageTracker; |
| import com.intellij.lang.Language; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.command.CommandProcessor; |
| import com.intellij.openapi.command.undo.UndoConstants; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.editor.Editor; |
| import com.intellij.openapi.util.Condition; |
| import com.intellij.openapi.util.Conditions; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.PsiDocumentManager; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.PsiFileFactory; |
| import com.intellij.psi.util.PsiUtilCore; |
| import com.intellij.util.containers.ContainerUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.Collection; |
| import java.util.Set; |
| |
| public class PostfixLiveTemplate extends CustomLiveTemplateBase { |
| public static final String POSTFIX_TEMPLATE_ID = "POSTFIX_TEMPLATE_ID"; |
| private static final Logger LOG = Logger.getInstance(PostfixLiveTemplate.class); |
| |
| @NotNull |
| public Set<String> getAllTemplateKeys(PsiFile file, int offset) { |
| Set<String> keys = Sets.newHashSet(); |
| Language language = PsiUtilCore.getLanguageAtOffset(file, offset); |
| for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(language)) { |
| keys.addAll(getKeys(provider)); |
| } |
| return keys; |
| } |
| |
| @Nullable |
| private static String computeTemplateKeyWithoutContextChecking(@NotNull PostfixTemplateProvider provider, |
| @NotNull CharSequence documentContent, |
| int currentOffset) { |
| int startOffset = currentOffset; |
| if (documentContent.length() < startOffset) { |
| return null; |
| } |
| |
| while (startOffset > 0) { |
| char currentChar = documentContent.charAt(startOffset - 1); |
| if (!Character.isJavaIdentifierPart(currentChar)) { |
| if (!provider.isTerminalSymbol(currentChar)) { |
| return null; |
| } |
| startOffset--; |
| break; |
| } |
| startOffset--; |
| } |
| return String.valueOf(documentContent.subSequence(startOffset, currentOffset)); |
| } |
| |
| @Nullable |
| @Override |
| public String computeTemplateKey(@NotNull CustomTemplateCallback callback) { |
| Editor editor = callback.getEditor(); |
| CharSequence charsSequence = editor.getDocument().getCharsSequence(); |
| int offset = editor.getCaretModel().getOffset(); |
| for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(getLanguage(callback))) { |
| String key = computeTemplateKeyWithoutContextChecking(provider, charsSequence, offset); |
| if (key != null && isApplicableTemplate(provider, key, callback.getFile(), editor)) { |
| return key; |
| } |
| } |
| return null; |
| } |
| |
| @Nullable |
| @Override |
| public String computeTemplateKeyWithoutContextChecking(@NotNull CustomTemplateCallback callback) { |
| Editor editor = callback.getEditor(); |
| int currentOffset = editor.getCaretModel().getOffset(); |
| for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(getLanguage(callback))) { |
| String key = computeTemplateKeyWithoutContextChecking(provider, editor.getDocument().getCharsSequence(), currentOffset); |
| if (key != null) return key; |
| } |
| return null; |
| } |
| |
| @Override |
| public boolean supportsMultiCaret() { |
| return false; |
| } |
| |
| @Override |
| public void expand(@NotNull final String key, @NotNull final CustomTemplateCallback callback) { |
| ApplicationManager.getApplication().assertIsDispatchThread(); |
| FeatureUsageTracker.getInstance().triggerFeatureUsed("editing.completion.postfix"); |
| |
| Editor editor = callback.getEditor(); |
| for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(getLanguage(callback))) { |
| PostfixTemplate postfixTemplate = getTemplate(provider, key); |
| if (postfixTemplate != null) { |
| final PsiFile file = callback.getContext().getContainingFile(); |
| if (isApplicableTemplate(provider, key, file, editor)) { |
| int offset = deleteTemplateKey(file, editor, key); |
| try { |
| provider.preExpand(file, editor); |
| PsiElement context = CustomTemplateCallback.getContext(file, positiveOffset(offset)); |
| expandTemplate(postfixTemplate, editor, context); |
| } |
| finally { |
| provider.afterExpand(file, editor); |
| } |
| } |
| // don't care about errors in multiCaret mode |
| else if (editor.getCaretModel().getAllCarets().size() == 1) { |
| LOG.error("Template not found by key: " + key + "; offset = " + callback.getOffset(), |
| AttachmentFactory.createAttachment(callback.getFile().getVirtualFile())); |
| } |
| return; |
| } |
| } |
| |
| // don't care about errors in multiCaret mode |
| if (editor.getCaretModel().getAllCarets().size() == 1) { |
| LOG.error("Template not found by key: " + key + "; offset = " + callback.getOffset(), |
| AttachmentFactory.createAttachment(callback.getFile().getVirtualFile())); |
| } |
| } |
| |
| @Override |
| public boolean isApplicable(PsiFile file, int offset, boolean wrapping) { |
| PostfixTemplatesSettings settings = PostfixTemplatesSettings.getInstance(); |
| if (wrapping || file == null || settings == null || !settings.isPostfixTemplatesEnabled()) { |
| return false; |
| } |
| Language language = PsiUtilCore.getLanguageAtOffset(file, offset); |
| for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(language)) { |
| if (StringUtil.isNotEmpty(computeTemplateKeyWithoutContextChecking(provider, file.getText(), offset + 1))) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean supportsWrapping() { |
| return false; |
| } |
| |
| @Override |
| public void wrap(@NotNull String selection, @NotNull CustomTemplateCallback callback) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @NotNull |
| @Override |
| public String getTitle() { |
| return "Postfix"; |
| } |
| |
| @Override |
| public char getShortcut() { |
| PostfixTemplatesSettings settings = PostfixTemplatesSettings.getInstance(); |
| return settings != null ? (char)settings.getShortcut() : TemplateSettings.TAB_CHAR; |
| } |
| |
| @Override |
| public boolean hasCompletionItem(@NotNull PsiFile file, int offset) { |
| return true; |
| } |
| |
| @NotNull |
| @Override |
| public Collection<? extends CustomLiveTemplateLookupElement> getLookupElements(@NotNull PsiFile file, |
| @NotNull Editor editor, |
| int offset) { |
| Collection<CustomLiveTemplateLookupElement> result = ContainerUtil.newHashSet(); |
| CustomTemplateCallback callback = new CustomTemplateCallback(editor, file); |
| for (PostfixTemplateProvider provider : LanguagePostfixTemplate.LANG_EP.allForLanguage(getLanguage(callback))) { |
| String key = computeTemplateKeyWithoutContextChecking(callback); |
| if (key != null && editor.getCaretModel().getCaretCount() == 1) { |
| Condition<PostfixTemplate> isApplicationTemplateFunction = createIsApplicationTemplateFunction(provider, key, file, editor); |
| for (PostfixTemplate postfixTemplate : provider.getTemplates()) { |
| if (isApplicationTemplateFunction.value(postfixTemplate)) { |
| result.add(new PostfixTemplateLookupElement(this, postfixTemplate, postfixTemplate.getKey(), provider, false)); |
| } |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| private static void expandTemplate(@NotNull final PostfixTemplate template, |
| @NotNull final Editor editor, |
| @NotNull final PsiElement context) { |
| ApplicationManager.getApplication().runWriteAction(new Runnable() { |
| @Override |
| public void run() { |
| CommandProcessor.getInstance().executeCommand(context.getProject(), new Runnable() { |
| public void run() { |
| template.expand(context, editor); |
| } |
| }, "Expand postfix template", POSTFIX_TEMPLATE_ID); |
| } |
| }); |
| } |
| |
| |
| private static int deleteTemplateKey(@NotNull final PsiFile file, @NotNull final Editor editor, @NotNull final String key) { |
| ApplicationManager.getApplication().assertIsDispatchThread(); |
| |
| final int currentOffset = editor.getCaretModel().getOffset(); |
| final int newOffset = currentOffset - key.length(); |
| ApplicationManager.getApplication().runWriteAction(new Runnable() { |
| @Override |
| public void run() { |
| CommandProcessor.getInstance().runUndoTransparentAction(new Runnable() { |
| public void run() { |
| Document document = editor.getDocument(); |
| document.deleteString(newOffset, currentOffset); |
| editor.getCaretModel().moveToOffset(newOffset); |
| PsiDocumentManager.getInstance(file.getProject()).commitDocument(document); |
| } |
| }); |
| } |
| }); |
| return newOffset; |
| } |
| |
| private static Condition<PostfixTemplate> createIsApplicationTemplateFunction(@NotNull final PostfixTemplateProvider provider, |
| @NotNull String key, |
| @NotNull PsiFile file, |
| @NotNull Editor editor) { |
| int currentOffset = editor.getCaretModel().getOffset(); |
| final int newOffset = currentOffset - key.length(); |
| CharSequence fileContent = editor.getDocument().getCharsSequence(); |
| StringBuilder fileContentWithoutKey = new StringBuilder(); |
| fileContentWithoutKey.append(fileContent.subSequence(0, newOffset)); |
| fileContentWithoutKey.append(fileContent.subSequence(currentOffset, fileContent.length())); |
| PsiFile copyFile = copyFile(file, fileContentWithoutKey); |
| Document copyDocument = copyFile.getViewProvider().getDocument(); |
| if (copyDocument == null) { |
| return Conditions.alwaysFalse(); |
| } |
| |
| copyFile = provider.preCheck(copyFile, editor, newOffset); |
| copyDocument = copyFile.getViewProvider().getDocument(); |
| if (copyDocument == null) { |
| return Conditions.alwaysFalse(); |
| } |
| |
| final PsiElement context = CustomTemplateCallback.getContext(copyFile, positiveOffset(newOffset)); |
| final Document finalCopyDocument = copyDocument; |
| return new Condition<PostfixTemplate>() { |
| @Override |
| public boolean value(PostfixTemplate template) { |
| return template != null && template.isEnabled(provider) && template.isApplicable(context, finalCopyDocument, newOffset); |
| } |
| }; |
| } |
| |
| @NotNull |
| public static PsiFile copyFile(@NotNull PsiFile file, @NotNull StringBuilder fileContentWithoutKey) { |
| final PsiFileFactory psiFileFactory = PsiFileFactory.getInstance(file.getProject()); |
| PsiFile copy = psiFileFactory.createFileFromText(file.getName(), file.getFileType(), fileContentWithoutKey); |
| VirtualFile vFile = copy.getVirtualFile(); |
| if (vFile != null) { |
| vFile.putUserData(UndoConstants.DONT_RECORD_UNDO, Boolean.TRUE); |
| } |
| return copy; |
| } |
| |
| public static boolean isApplicableTemplate(@NotNull PostfixTemplateProvider provider, |
| @NotNull String key, |
| @NotNull PsiFile file, |
| @NotNull Editor editor) { |
| return createIsApplicationTemplateFunction(provider, key, file, editor).value(getTemplate(provider, key)); |
| } |
| |
| @NotNull |
| private static Set<String> getKeys(@NotNull PostfixTemplateProvider provider) { |
| Set<String> result = ContainerUtil.newHashSet(); |
| for (PostfixTemplate template : provider.getTemplates()) { |
| result.add(template.getKey()); |
| } |
| |
| return result; |
| } |
| |
| @Nullable |
| private static PostfixTemplate getTemplate(@NotNull PostfixTemplateProvider provider, @Nullable String key) { |
| for (PostfixTemplate template : provider.getTemplates()) { |
| if (template.getKey().equals(key)) { |
| return template; |
| } |
| } |
| return null; |
| } |
| |
| private static Language getLanguage(@NotNull CustomTemplateCallback callback) { |
| return callback.getContext().getLanguage(); |
| } |
| |
| private static int positiveOffset(int offset) { |
| return offset > 0 ? offset - 1 : offset; |
| } |
| } |