From c16276e8a415e0748059163b8ddf6a3c254877dd Mon Sep 17 00:00:00 2001 From: Denis Zhdanov Date: Thu, 28 Apr 2011 13:00:48 +0400 Subject: [PATCH] IDEA-68536 I want Live Templates to automatically add static imports (Use Case: templates for org.junit.Assert.assertXXX) 1. Corresponding template optional processor is added; 2. 'Add static import' actions are refactored in order to provide ability to execute their logic from the template processor; --- .../impl/AddOnDemandStaticImportAction.java | 65 ++++++-- .../AddSingleMemberStaticImportAction.java | 55 +++++-- .../impl/ShortenToStaticImportProcessor.java | 139 ++++++++++++++++++ .../codeInsight/template/Template.java | 28 ++++ .../template/impl/TemplateImpl.java | 7 + .../template/impl/TemplateSettings.java | 10 ++ .../src/messages/CodeInsightBundle.properties | 1 + resources/src/META-INF/IdeaPlugin.xml | 1 + 8 files changed, 276 insertions(+), 30 deletions(-) create mode 100644 java/java-impl/src/com/intellij/codeInsight/template/impl/ShortenToStaticImportProcessor.java diff --git a/java/java-impl/src/com/intellij/codeInsight/intention/impl/AddOnDemandStaticImportAction.java b/java/java-impl/src/com/intellij/codeInsight/intention/impl/AddOnDemandStaticImportAction.java index f06162932d0e..01d6988cb998 100644 --- a/java/java-impl/src/com/intellij/codeInsight/intention/impl/AddOnDemandStaticImportAction.java +++ b/java/java-impl/src/com/intellij/codeInsight/intention/impl/AddOnDemandStaticImportAction.java @@ -31,6 +31,7 @@ import com.intellij.util.IncorrectOperationException; import gnu.trove.TIntArrayList; import gnu.trove.TIntProcedure; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * @author ven @@ -43,41 +44,63 @@ public class AddOnDemandStaticImportAction extends PsiElementBaseIntentionAction return CodeInsightBundle.message("intention.add.on.demand.static.import.family"); } - public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) { - if (!PsiUtil.isLanguageLevel5OrHigher(element)) return false; + /** + * Allows to check if static import may be performed for the given element. + * + * @param element element to check + * @return target class that may be statically imported if any; null otherwise + */ + @Nullable + public static PsiClass getClassToPerformStaticImport(@NotNull PsiElement element) { + if (!PsiUtil.isLanguageLevel5OrHigher(element)) return null; if (!(element instanceof PsiIdentifier) || !(element.getParent() instanceof PsiReferenceExpression)) { - return false; + return null; } PsiReferenceExpression refExpr = (PsiReferenceExpression)element.getParent(); if (refExpr.getParent() instanceof PsiReferenceExpression && - isParameterizedReference((PsiReferenceExpression)refExpr.getParent())) return false; + isParameterizedReference((PsiReferenceExpression)refExpr.getParent())) return null; PsiElement resolved = refExpr.resolve(); if (!(resolved instanceof PsiClass)) { - return false; + return null; } PsiClass psiClass = (PsiClass)resolved; PsiFile file = refExpr.getContainingFile(); - if (!(file instanceof PsiJavaFile)) return false; + if (!(file instanceof PsiJavaFile)) return null; PsiImportList importList = ((PsiJavaFile)file).getImportList(); - if (importList == null) return false; + if (importList == null) return null; for (PsiImportStaticStatement statement : importList.getImportStaticStatements()) { PsiClass staticResolve = statement.resolveTargetClass(); - if (psiClass == staticResolve) return false; //already imported + if (psiClass == staticResolve) return null; //already imported } - String text = CodeInsightBundle.message("intention.add.on.demand.static.import.text", psiClass.getQualifiedName()); - setText(text); - return true; + + return psiClass; + } + + public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) { + PsiClass classToImport = getClassToPerformStaticImport(element); + if (classToImport != null) { + String text = CodeInsightBundle.message("intention.add.on.demand.static.import.text", classToImport.getQualifiedName()); + setText(text); + } + return classToImport != null; } - public void invoke(@NotNull final Project project, final Editor editor, PsiFile file) throws IncorrectOperationException { + public static void invoke(final Project project, PsiFile file, final Editor editor, PsiElement element) { if (!CodeInsightUtilBase.prepareFileForWrite(file)) return; - PsiElement element = file.findElementAt(editor.getCaretModel().getOffset()); + final PsiReferenceExpression refExpr = (PsiReferenceExpression)element.getParent(); final PsiClass aClass = (PsiClass)refExpr.resolve(); + if (aClass == null) { + return; + } PsiImportStaticStatement importStaticStatement = JavaPsiFacade.getInstance(file.getProject()).getElementFactory().createImportStaticStatement(aClass, "*"); - ((PsiJavaFile)file).getImportList().add(importStaticStatement); + PsiImportList importList = ((PsiJavaFile)file).getImportList(); + if (importList == null) { + return; + } + importList.add(importStaticStatement); PsiFile[] roots = file.getPsiRoots(); for (final PsiFile root : roots) { @@ -113,6 +136,9 @@ public class AddOnDemandStaticImportAction extends PsiElementBaseIntentionAction expressionToDequalifyOffsets.forEachDescending(new TIntProcedure() { public boolean execute(int offset) { PsiReferenceExpression expression = PsiTreeUtil.findElementOfClassAtOffset(root, offset, PsiReferenceExpression.class, false); + if (expression == null) { + return false; + } PsiExpression qualifierExpression = expression.getQualifierExpression(); if (qualifierExpression instanceof PsiReferenceExpression && ((PsiReferenceExpression)qualifierExpression).isReferenceTo(aClass)) { qualifierExpression.delete(); @@ -127,8 +153,17 @@ public class AddOnDemandStaticImportAction extends PsiElementBaseIntentionAction }); } } + + public void invoke(@NotNull final Project project, final Editor editor, PsiFile file) throws IncorrectOperationException { + PsiElement element = file.findElementAt(editor.getCaretModel().getOffset()); + invoke(project, file, editor, element); + } private static boolean isParameterizedReference(final PsiReferenceExpression expression) { - return expression.getParameterList() != null && expression.getParameterList().getFirstChild() != null; + if (expression.getParameterList() == null) { + return false; + } + PsiReferenceParameterList parameterList = expression.getParameterList(); + return parameterList != null && parameterList.getFirstChild() != null; } } diff --git a/java/java-impl/src/com/intellij/codeInsight/intention/impl/AddSingleMemberStaticImportAction.java b/java/java-impl/src/com/intellij/codeInsight/intention/impl/AddSingleMemberStaticImportAction.java index 74c8049add86..a76233a34c64 100644 --- a/java/java-impl/src/com/intellij/codeInsight/intention/impl/AddSingleMemberStaticImportAction.java +++ b/java/java-impl/src/com/intellij/codeInsight/intention/impl/AddSingleMemberStaticImportAction.java @@ -32,6 +32,7 @@ import com.intellij.psi.util.PsiUtil; import com.intellij.refactoring.util.RefactoringUtil; import com.intellij.util.IncorrectOperationException; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public class AddSingleMemberStaticImportAction extends PsiElementBaseIntentionAction { private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.intention.impl.AddSingleMemberStaticImportAction"); @@ -42,26 +43,32 @@ public class AddSingleMemberStaticImportAction extends PsiElementBaseIntentionAc return CodeInsightBundle.message("intention.add.single.member.static.import.family"); } - public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) { - if (!PsiUtil.isLanguageLevel5OrHigher(element)) return false; + /** + * Allows to check if it's possible to perform static import for the target element. + * + * @param element target element that is static import candidate + * @return not-null qualified name of the class which method may be statically imported if any; null otherwise + */ + @Nullable + public static String getStaticImportClass(@NotNull PsiElement element) { + if (!PsiUtil.isLanguageLevel5OrHigher(element)) return null; PsiFile file = element.getContainingFile(); if (element instanceof PsiIdentifier && element.getParent() instanceof PsiReferenceExpression && ((PsiReferenceExpression)element.getParent()).getQualifierExpression() != null) { PsiReferenceExpression refExpr = (PsiReferenceExpression)element.getParent(); - if (refExpr.getParameterList() != null && - refExpr.getParameterList().getFirstChild() != null) return false; + PsiReferenceParameterList parameterList = refExpr.getParameterList(); + if (parameterList != null && parameterList.getFirstChild() != null) return null; PsiElement resolved = refExpr.resolve(); - if (resolved instanceof PsiMember && - ((PsiModifierListOwner)resolved).hasModifierProperty(PsiModifier.STATIC)) { + if (resolved instanceof PsiMember && ((PsiModifierListOwner)resolved).hasModifierProperty(PsiModifier.STATIC)) { PsiClass aClass = ((PsiMember)resolved).getContainingClass(); if (aClass != null && !PsiTreeUtil.isAncestor(aClass, element, true)) { String qName = aClass.getQualifiedName(); if (qName != null) { qName = qName + "." +refExpr.getReferenceName(); if (file instanceof PsiJavaFile) { - if (((PsiJavaFile)file).getImportList().findSingleImportStatement(refExpr.getReferenceName()) == null) { - setText(CodeInsightBundle.message("intention.add.single.member.static.import.text", qName)); - return true; + PsiImportList importList = ((PsiJavaFile)file).getImportList(); + if (importList != null && importList.findSingleImportStatement(refExpr.getReferenceName()) == null) { + return qName; } } } @@ -69,20 +76,28 @@ public class AddSingleMemberStaticImportAction extends PsiElementBaseIntentionAc } } - return false; + return null; + } + + public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) { + String classQName = getStaticImportClass(element); + if (classQName != null) { + setText(CodeInsightBundle.message("intention.add.single.member.static.import.text", classQName)); + } + return classQName != null; } - - public void invoke(@NotNull final Project project, final Editor editor, PsiFile file) throws IncorrectOperationException { + public static void invoke(PsiFile file, PsiElement element) { if (!CodeInsightUtilBase.prepareFileForWrite(file)) return; - PsiElement element = file.findElementAt(editor.getCaretModel().getOffset()); + final PsiReferenceExpression refExpr = (PsiReferenceExpression)element.getParent(); final PsiElement resolved = refExpr.resolve(); file.accept(new JavaRecursiveElementWalkingVisitor() { @Override public void visitReferenceExpression(PsiReferenceExpression expression) { super.visitReferenceExpression(expression); - if (refExpr.getReferenceName().equals(expression.getReferenceName())) { + String referenceName = refExpr.getReferenceName(); + if (referenceName != null && referenceName.equals(expression.getReferenceName())) { PsiElement resolved = expression.resolve(); if (resolved != null) { expression.putUserData(TEMP_REFERENT_USER_DATA, resolved); @@ -91,7 +106,11 @@ public class AddSingleMemberStaticImportAction extends PsiElementBaseIntentionAc } }); - RefactoringUtil.bindToElementViaStaticImport(((PsiMember)resolved).getContainingClass(), ((PsiNamedElement)resolved).getName(), ((PsiJavaFile)file).getImportList()); + if (resolved != null) { + RefactoringUtil.bindToElementViaStaticImport( + ((PsiMember)resolved).getContainingClass(), ((PsiNamedElement)resolved).getName(), ((PsiJavaFile)file).getImportList() + ); + } file.accept(new JavaRecursiveElementVisitor() { @Override public void visitReferenceExpression(PsiReferenceExpression expression) { @@ -133,5 +152,11 @@ public class AddSingleMemberStaticImportAction extends PsiElementBaseIntentionAc super.visitReferenceExpression(expression); } }); + + } + + public void invoke(@NotNull final Project project, final Editor editor, PsiFile file) throws IncorrectOperationException { + PsiElement element = file.findElementAt(editor.getCaretModel().getOffset()); + invoke(file, element); } } diff --git a/java/java-impl/src/com/intellij/codeInsight/template/impl/ShortenToStaticImportProcessor.java b/java/java-impl/src/com/intellij/codeInsight/template/impl/ShortenToStaticImportProcessor.java new file mode 100644 index 000000000000..20050e3b1c01 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/template/impl/ShortenToStaticImportProcessor.java @@ -0,0 +1,139 @@ +/* + * Copyright 2000-2011 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.impl; + +import com.intellij.codeInsight.CodeInsightBundle; +import com.intellij.codeInsight.intention.impl.AddOnDemandStaticImportAction; +import com.intellij.codeInsight.intention.impl.AddSingleMemberStaticImportAction; +import com.intellij.codeInsight.template.Template; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.RangeMarker; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.PsiUtilBase; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Arrays.asList; + +/** + * @author Denis Zhdanov + * @since 4/27/11 3:07 PM + */ +public class ShortenToStaticImportProcessor implements TemplateOptionalProcessor { + + private static final List IMPORTERS = asList(new SingleMemberStaticImporter(), new OnDemandStaticImporter()); + + @Override + public void processText(Project project, Template template, Document document, RangeMarker templateRange, Editor editor) { + if (!template.getValue(Template.Property.USE_STATIC_IMPORT_IF_POSSIBLE)) { + return; + } + + PsiDocumentManager.getInstance(project).commitDocument(document); + final PsiFile file = PsiUtilBase.getPsiFileInEditor(editor, project); + if (file == null) { + return; + } + + List> staticImportTargets = new ArrayList>(); + for ( + PsiElement element = PsiUtilBase.getElementAtOffset(file, templateRange.getStartOffset()); + element != null && element.getTextRange().getStartOffset() < templateRange.getEndOffset(); + element = getNext(element)) + { + for (StaticImporter importer : IMPORTERS) { + if (importer.canPerform(element)) { + staticImportTargets.add(new Pair(element, importer)); + break; + } + } + } + + for (Pair pair : staticImportTargets) { + if (pair.first.isValid()) { + pair.second.perform(project, file, editor, pair.first); + } + } + } + + @Nullable + private static PsiElement getNext(@NotNull PsiElement element) { + PsiElement result = element.getNextSibling(); + for (PsiElement current = element; current != null && result == null; current = current.getParent()) { + result = current.getNextSibling(); + } + return result; + } + + @Nls + @Override + public String getOptionName() { + return CodeInsightBundle.message("dialog.edit.template.checkbox.use.static.import"); + } + + @Override + public boolean isEnabled(Template template) { + return template.getValue(Template.Property.USE_STATIC_IMPORT_IF_POSSIBLE); + } + + @Override + public void setEnabled(Template template, boolean value) { + template.setValue(Template.Property.USE_STATIC_IMPORT_IF_POSSIBLE, value); + } + + @Override + public boolean isVisible(Template template) { + return true; + } + + private interface StaticImporter { + boolean canPerform(@NotNull PsiElement element); + void perform(Project project, PsiFile file, Editor editor, PsiElement element); + } + + private static class SingleMemberStaticImporter implements StaticImporter { + @Override + public boolean canPerform(@NotNull PsiElement element) { + return AddSingleMemberStaticImportAction.getStaticImportClass(element) != null; + } + + @Override + public void perform(Project project, PsiFile file, Editor editor, PsiElement element) { + AddSingleMemberStaticImportAction.invoke(file, element); + } + } + + private static class OnDemandStaticImporter implements StaticImporter { + @Override + public boolean canPerform(@NotNull PsiElement element) { + return AddOnDemandStaticImportAction.getClassToPerformStaticImport(element) != null; + } + + @Override + public void perform(Project project, PsiFile file, Editor editor, PsiElement element) { + AddOnDemandStaticImportAction.invoke(project, file, editor, element); + } + } +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/template/Template.java b/platform/lang-impl/src/com/intellij/codeInsight/template/Template.java index 43dba460aa10..9b61f20fabe3 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/template/Template.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/template/Template.java @@ -21,7 +21,21 @@ import com.intellij.openapi.editor.RangeMarker; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; +import java.util.EnumMap; +import java.util.Map; + public abstract class Template { + + public enum Property { + USE_STATIC_IMPORT_IF_POSSIBLE + } + private static final Map DEFAULT_PROPERTIES = new EnumMap(Property.class); + static { + DEFAULT_PROPERTIES.put(Property.USE_STATIC_IMPORT_IF_POSSIBLE, false); + } + + private final Map myProperties = new EnumMap(Property.class); + public abstract void addTextSegment(@NotNull String text); public abstract void addVariableSegment(@NonNls String name); @@ -66,4 +80,18 @@ public abstract class Template { public abstract boolean isToShortenLongNames(); public abstract void setToShortenLongNames(boolean toShortenLongNames); + + public boolean getValue(@NotNull Property key) { + Boolean result = myProperties.get(key); + return result == null ? getDefaultValue(key) : result; + } + + public void setValue(@NotNull Property key, boolean value) { + myProperties.put(key, value); + } + + public static boolean getDefaultValue(@NotNull Property key) { + Boolean result = DEFAULT_PROPERTIES.get(key); + return result == null ? false : result; + } } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/template/impl/TemplateImpl.java b/platform/lang-impl/src/com/intellij/codeInsight/template/impl/TemplateImpl.java index ce7303c93d7d..91a516fea300 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/template/impl/TemplateImpl.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/template/impl/TemplateImpl.java @@ -27,6 +27,7 @@ import com.intellij.psi.tree.IElementType; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; +import javax.xml.bind.annotation.XmlElement; import java.util.*; /** @@ -183,6 +184,12 @@ public class TemplateImpl extends Template implements SchemeElement { template.myIsInline = myIsInline; template.myTemplateContext = myTemplateContext.createCopy(); template.isDeactivated = isDeactivated; + for (Property property : Property.values()) { + boolean value = getValue(property); + if (value != Template.getDefaultValue(property)) { + template.setValue(property, value); + } + } for (Variable variable : myVariables) { template.addVariable(variable.getName(), variable.getExpressionString(), variable.getDefaultValueString(), variable.isAlwaysStopAt()); } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/template/impl/TemplateSettings.java b/platform/lang-impl/src/com/intellij/codeInsight/template/impl/TemplateSettings.java index 7e7d2c7dbda4..a5b40e8b27f5 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/template/impl/TemplateSettings.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/template/impl/TemplateSettings.java @@ -86,6 +86,7 @@ public class TemplateSettings implements PersistentStateComponent, Expo private static final @NonNls String CONTEXT = "context"; private static final @NonNls String TO_REFORMAT = "toReformat"; private static final @NonNls String TO_SHORTEN_FQ_NAMES = "toShortenFQNames"; + private static final @NonNls String USE_STATIC_IMPORT = "useStaticImport"; private static final @NonNls String DEFAULT_SHORTCUT = "defaultShortcut"; private static final @NonNls String DEACTIVATED = "deactivated"; @@ -589,6 +590,10 @@ public class TemplateSettings implements PersistentStateComponent, Expo template.setToShortenLongNames(Boolean.parseBoolean(element.getAttributeValue(TO_SHORTEN_FQ_NAMES))); template.setDeactivated(Boolean.parseBoolean(element.getAttributeValue(DEACTIVATED))); + String useStaticImport = element.getAttributeValue(USE_STATIC_IMPORT); + if (useStaticImport != null) { + template.setValue(TemplateImpl.Property.USE_STATIC_IMPORT_IF_POSSIBLE, Boolean.parseBoolean(useStaticImport)); + } for (final Object o : element.getChildren(VARIABLE)) { Element e = (Element)o; @@ -627,6 +632,11 @@ public class TemplateSettings implements PersistentStateComponent, Expo } element.setAttribute(TO_REFORMAT, Boolean.toString(template.isToReformat())); element.setAttribute(TO_SHORTEN_FQ_NAMES, Boolean.toString(template.isToShortenLongNames())); + if (template.getValue(Template.Property.USE_STATIC_IMPORT_IF_POSSIBLE) + != Template.getDefaultValue(Template.Property.USE_STATIC_IMPORT_IF_POSSIBLE)) + { + element.setAttribute(USE_STATIC_IMPORT, Boolean.toString(template.getValue(Template.Property.USE_STATIC_IMPORT_IF_POSSIBLE))); + } if (template.isDeactivated()) { element.setAttribute(DEACTIVATED, Boolean.toString(true)); } diff --git a/platform/platform-resources-en/src/messages/CodeInsightBundle.properties b/platform/platform-resources-en/src/messages/CodeInsightBundle.properties index 80b26d17b51e..ba4040d332c1 100644 --- a/platform/platform-resources-en/src/messages/CodeInsightBundle.properties +++ b/platform/platform-resources-en/src/messages/CodeInsightBundle.properties @@ -295,6 +295,7 @@ dialog.edit.template.options.title=Options dialog.edit.template.label.expand.with=Expand with dialog.edit.template.checkbox.reformat.according.to.style=&Reformat according to style dialog.edit.template.checkbox.shorten.fq.names=Shorten &FQ names +dialog.edit.template.checkbox.use.static.import=Use static import if possible dialog.edit.template.context.title=Context dialog.edit.template.checkbox.java.code=&Java code dialog.edit.template.checkbox.html=&HTML diff --git a/resources/src/META-INF/IdeaPlugin.xml b/resources/src/META-INF/IdeaPlugin.xml index 58ab2328b8f3..391f7863f211 100644 --- a/resources/src/META-INF/IdeaPlugin.xml +++ b/resources/src/META-INF/IdeaPlugin.xml @@ -947,6 +947,7 @@ +