blob: 61544faf2c81a4f841dd2ff69a838434dffc0cfb [file] [log] [blame]
/*
* 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;
}
}