package com.intellij.lang.javascript.generation; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.codeInsight.template.*; import com.intellij.javascript.flex.mxml.FlexCommonTypeNames; import com.intellij.javascript.flex.resolve.ActionScriptClassResolver; import com.intellij.lang.ASTNode; import com.intellij.lang.LanguageNamesValidation; import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.lang.javascript.JSTokenTypes; import com.intellij.lang.javascript.JavaScriptSupportLoader; import com.intellij.lang.javascript.dialects.JSDialectSpecificHandlersFactory; import com.intellij.lang.javascript.flex.AnnotationBackedDescriptor; import com.intellij.lang.javascript.flex.FlexBundle; import com.intellij.lang.javascript.flex.ImportUtils; import com.intellij.lang.javascript.psi.*; import com.intellij.lang.javascript.psi.ecmal4.JSAttribute; import com.intellij.lang.javascript.psi.ecmal4.JSAttributeList; import com.intellij.lang.javascript.psi.ecmal4.JSAttributeNameValuePair; import com.intellij.lang.javascript.psi.ecmal4.JSClass; import com.intellij.lang.javascript.psi.impl.JSChangeUtil; import com.intellij.lang.javascript.psi.impl.PublicInheritorFilter; import com.intellij.lang.javascript.psi.resolve.JSInheritanceUtil; import com.intellij.lang.javascript.psi.resolve.JSResolveUtil; import com.intellij.lang.javascript.ui.JSClassChooserDialog; import com.intellij.lang.javascript.validation.fixes.BaseCreateMethodsFix; import com.intellij.lang.refactoring.NamesValidator; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtil; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.Trinity; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; import com.intellij.psi.impl.source.tree.LeafPsiElement; import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlDocument; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import com.intellij.util.IncorrectOperationException; import com.intellij.xml.XmlAttributeDescriptor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.text.MessageFormat; public class ActionScriptGenerateEventHandler extends BaseJSGenerateHandler { protected String getTitleKey() { return ""; // not used in this action } protected BaseCreateMethodsFix createFix(final JSClass jsClass) { return new GenerateEventHandlerFix(jsClass); } protected boolean collectCandidatesAndShowDialog() { return false; } protected boolean canHaveEmptySelectedElements() { return true; } @Nullable public static XmlAttribute getXmlAttribute(final PsiFile psiFile, final Editor editor) { PsiElement context = null; if (psiFile instanceof JSFile) { context = InjectedLanguageManager.getInstance(psiFile.getProject()).getInjectionHost(psiFile); } else if (psiFile instanceof XmlFile) { context = psiFile.findElementAt(editor.getCaretModel().getOffset()); } return PsiTreeUtil.getParentOfType(context, XmlAttribute.class); } @Nullable public static String getEventType(final XmlAttribute xmlAttribute) { final XmlAttributeDescriptor descriptor = xmlAttribute == null ? null : xmlAttribute.getDescriptor(); final PsiElement declaration = descriptor instanceof AnnotationBackedDescriptor ? descriptor.getDeclaration() : null; final PsiElement declarationParent = declaration == null ? null : declaration.getParent(); if (declaration instanceof JSAttributeNameValuePair && (((JSAttributeNameValuePair)declaration).getName() == null || "name".equals(((JSAttributeNameValuePair)declaration).getName())) && declarationParent instanceof JSAttribute && "Event".equals(((JSAttribute)declarationParent).getName())) { return ((AnnotationBackedDescriptor)descriptor).getType(); } return null; } @Nullable public static JSCallExpression getEventListenerCallExpression(final PsiFile psiFile, final Editor editor) { if (!(psiFile instanceof JSFile)) { return null; } final PsiElement elementAtCursor = psiFile.findElementAt(editor.getCaretModel().getOffset()); final JSCallExpression callExpression = PsiTreeUtil.getParentOfType(elementAtCursor, JSCallExpression.class); if (callExpression == null || !JSResolveUtil.isEventListenerCall(callExpression)) { return null; } final JSExpression[] params = callExpression.getArguments(); if (params.length > 0 && ((params[0] instanceof JSReferenceExpression && ((JSReferenceExpression)params[0]).getQualifier() != null) || (params[0] instanceof JSLiteralExpression && ((JSLiteralExpression)params[0]).isQuotedLiteral()))) { if (params.length == 1 || params.length > 1 && isUnresolvedReference(params[1])) { return callExpression; } } return null; } private static boolean isUnresolvedReference(final JSExpression parameter) { if (parameter instanceof JSReferenceExpression) { final PsiElement referenceNameElement = ((JSReferenceExpression)parameter).getReferenceNameElement(); final ASTNode nameNode = referenceNameElement == null ? null : referenceNameElement.getNode(); if (nameNode != null && nameNode.getElementType() == JSTokenTypes.IDENTIFIER && ((JSReferenceExpression)parameter).resolve() == null) { return true; } } return false; } /** * Trinity.first is JSExpressionStatement (if it looks like ButtonEvent.CLICK), * Trinity.second is event class FQN (like "flash.events.MouseEvent"), * Trinity.third is event name (like "click") */ @Nullable public static Trinity<JSExpressionStatement, String, String> getEventConstantInfo(final PsiFile psiFile, final Editor editor) { if (!(psiFile instanceof JSFile)) { return null; } final JSClass jsClass = BaseJSGenerateHandler.findClass(psiFile, editor); if (jsClass == null || !ActionScriptEventDispatchUtils.isEventDispatcher(jsClass)) { return null; } final PsiElement elementAtCursor = psiFile.findElementAt(editor.getCaretModel().getOffset()); final JSExpressionStatement expressionStatement = PsiTreeUtil.getParentOfType(elementAtCursor, JSExpressionStatement.class); final PsiElement expressionStatementParent = expressionStatement == null ? null : expressionStatement.getParent(); final JSFunction jsFunction = PsiTreeUtil.getParentOfType(expressionStatement, JSFunction.class); final JSExpression expression = expressionStatement == null ? null : expressionStatement.getExpression(); final JSReferenceExpression refExpression = expression instanceof JSReferenceExpression ? (JSReferenceExpression)expression : null; final JSExpression qualifier = refExpression == null ? null : refExpression.getQualifier(); final PsiReference qualifierReference = qualifier == null ? null : qualifier.getReference(); final PsiElement referenceNameElement = refExpression == null ? null : refExpression.getReferenceNameElement(); JSAttributeList functionAttributes; if (jsFunction == null || ((functionAttributes = jsFunction.getAttributeList()) != null && functionAttributes.hasModifier(JSAttributeList.ModifierType.STATIC)) || qualifierReference == null || !(referenceNameElement instanceof LeafPsiElement) || (!(expressionStatementParent instanceof JSFunction) && !(expressionStatementParent instanceof JSBlockStatement)) ) { return null; } final PsiElement qualifierResolve = qualifierReference.resolve(); if (!(qualifierResolve instanceof JSClass) || !isEventClass((JSClass)qualifierResolve)) { return null; } final PsiElement expressionResolve = refExpression.resolve(); if (expressionResolve instanceof JSVariable) { final JSAttributeList varAttributes = ((JSVariable)expressionResolve).getAttributeList(); final String text = ((JSVariable)expressionResolve).getLiteralOrReferenceInitializerText(); if (varAttributes != null && varAttributes.hasModifier(JSAttributeList.ModifierType.STATIC) && varAttributes.getAccessType() == JSAttributeList.AccessType.PUBLIC && text != null && StringUtil.isQuotedString(text)) { return Trinity.create(expressionStatement, ((JSClass)qualifierResolve).getQualifiedName(), initializerToPartialMethodName(text)); } } return null; } public static boolean isEventClass(final JSClass jsClass) { final PsiElement eventClass = ActionScriptClassResolver.findClassByQNameStatic(FlexCommonTypeNames.FLASH_EVENT_FQN, jsClass); if ((eventClass instanceof JSClass) && JSInheritanceUtil.isParentClass(jsClass, (JSClass)eventClass)) { return true; } final PsiElement eventClass2 = ActionScriptClassResolver.findClassByQNameStatic(FlexCommonTypeNames.STARLING_EVENT_FQN, jsClass); if ((eventClass2 instanceof JSClass) && JSInheritanceUtil.isParentClass(jsClass, (JSClass)eventClass2)) { return true; } return false; } private static String initializerToPartialMethodName(final String initializerText) { final String unquoted = StringUtil.stripQuotesAroundValue(initializerText); final int dotIndex = unquoted.lastIndexOf('.'); return unquoted.substring(dotIndex + 1).replaceAll("[^\\p{Alnum}]", "_"); } public static class GenerateEventHandlerFix extends BaseCreateMethodsFix { private boolean inMxmlEventAttributeValue; private boolean inEventListenerCall; private PsiElement handlerCallerAnchorInArgumentList; private JSReferenceExpression myExistingUnresolvedReverence; private boolean inEventConstantExpression; private JSExpressionStatement eventConstantExpression; private String eventHandlerName; private String eventHandlerName2; private String methodBody; private String eventClassFqn; private boolean userCancelled; private static final String METHOD_NAME_PATTERN = "{0}_{1}Handler"; private final JSClass myJsClass; public GenerateEventHandlerFix(final JSClass jsClass) { super(jsClass); myJsClass = jsClass; inMxmlEventAttributeValue = false; inEventListenerCall = false; handlerCallerAnchorInArgumentList = null; eventHandlerName = "eventHandler"; eventHandlerName2 = "onEvent"; methodBody = ""; eventClassFqn = FlexCommonTypeNames.FLASH_EVENT_FQN; userCancelled = false; } // called outside of write action - required for class chooser public void beforeInvoke(@NotNull final Project project, final Editor editor, final PsiFile psiFile) { // keep consistency with CreateEventHandlerIntention.isAvailable() final XmlAttribute xmlAttribute = getXmlAttribute(psiFile, editor); final String eventType = xmlAttribute == null ? null : getEventType(xmlAttribute); if (eventType != null) { inMxmlEventAttributeValue = true; prepareForMxmlEventAttributeValue(xmlAttribute, eventType); return; } final JSCallExpression callExpression = getEventListenerCallExpression(psiFile, editor); if (callExpression != null) { inEventListenerCall = true; prepareForEventListenerCall(callExpression); return; } final Trinity<JSExpressionStatement, String, String> eventConstantInfo = getEventConstantInfo(psiFile, editor); if (eventConstantInfo != null) { inEventConstantExpression = true; eventConstantExpression = eventConstantInfo.first; eventClassFqn = eventConstantInfo.second; final String eventName = eventConstantInfo.third; eventHandlerName = eventName + "Handler"; eventHandlerName2 = "on" + (eventName.isEmpty() ? "Event" : Character.toUpperCase(eventName.charAt(0)) + eventName.substring(1)); return; } // no suitable context -> ask for event class and create handler without usage final Module module = ModuleUtil.findModuleForPsiElement(psiFile); if (module != null && !ApplicationManager.getApplication().isUnitTestMode()) { final GlobalSearchScope scope = GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module); final JSClassChooserDialog dialog = new JSClassChooserDialog(module.getProject(), FlexBundle.message("choose.event.class.title"), scope, getEventBaseClass(), new PublicInheritorFilter(module.getProject(), FlexCommonTypeNames.FLASH_EVENT_FQN, scope, false)); if (dialog.showDialog()) { final JSClass selectedClass = dialog.getSelectedClass(); if (selectedClass != null) { eventClassFqn = selectedClass.getQualifiedName(); } } else { userCancelled = true; } } } public void invoke(@NotNull final Project project, final Editor editor, final PsiFile file) throws IncorrectOperationException { if (userCancelled) return; final PsiElement referenceElement = insertEventHandlerReference(editor, file); evalAnchor(editor, file); final String eventClassShortName = StringUtil.getShortName(eventClassFqn); final String functionText = "private function " + eventHandlerName + "(event:" + eventClassShortName + "):void{" + methodBody + "\n}\n"; JSFunction addedElement = (JSFunction)doAddOneMethod(project, functionText, anchor); addedElement = (JSFunction)ImportUtils.importAndShortenReference(eventClassFqn, addedElement, true, false).second; final PsiElement templateBaseElement = referenceElement == null ? addedElement : myJsClass; final TemplateBuilderImpl templateBuilder = new TemplateBuilderImpl(templateBaseElement); final PsiElement lastElement = PsiTreeUtil.getDeepestLast(addedElement); final PsiElement prevElement = lastElement.getPrevSibling(); templateBuilder.setEndVariableBefore((prevElement != null ? prevElement : lastElement)); templateBuilder .replaceElement(addedElement.getAttributeList().findAccessTypeElement(), new MyExpression("private", "protected", "public")); templateBuilder .replaceElement(addedElement.findNameIdentifier().getPsi(), "handlerName", new MyExpression(eventHandlerName, eventHandlerName2), true); templateBuilder .replaceElement(addedElement.getParameterVariables()[0].findNameIdentifier().getPsi(), new MyExpression("event", "e")); if (referenceElement != null && referenceElement.isValid()) { templateBuilder.replaceElement(referenceElement, "handlerReference", "handlerName", false); } final Editor topEditor = InjectedLanguageUtil.getTopLevelEditor(editor); PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(topEditor.getDocument()); final int startOffset = templateBaseElement.getTextRange().getStartOffset(); final Template template = templateBuilder.buildInlineTemplate(); topEditor.getCaretModel().moveToOffset(InjectedLanguageManager.getInstance(project).injectedToHost(templateBaseElement, startOffset)); TemplateManager.getInstance(project).startTemplate(topEditor, template); } @Nullable private PsiElement insertEventHandlerReference(final Editor editor, final PsiFile psiFile) { if (inMxmlEventAttributeValue) { final XmlAttribute xmlAttribute = getXmlAttribute(psiFile, editor); if (xmlAttribute != null) { final String attributeValue = eventHandlerName + "(event)"; xmlAttribute.setValue(attributeValue); final PsiLanguageInjectionHost valueElement = (PsiLanguageInjectionHost)xmlAttribute.getValueElement(); if (valueElement != null) { final Ref<PsiElement> ref = new Ref<>(); InjectedLanguageUtil.enumerate(valueElement, (injectedPsi, places) -> { int i = injectedPsi.getText().indexOf(attributeValue); if (i != -1) { ref.set(PsiTreeUtil.findElementOfClassAtOffset(injectedPsi, i, JSReferenceExpression.class, false)); } }); return ref.get(); } } } else if (inEventListenerCall) { if (handlerCallerAnchorInArgumentList != null) { PsiElement element = JSChangeUtil.createJSTreeFromText(psiFile.getProject(), eventHandlerName, JavaScriptSupportLoader.ECMA_SCRIPT_L4).getPsi(); PsiElement created = null; if (element != null) { created = handlerCallerAnchorInArgumentList.getParent().addAfter(element, handlerCallerAnchorInArgumentList); } // comma in argument list if (handlerCallerAnchorInArgumentList.getNode().getElementType() != JSTokenTypes.COMMA) { final PsiElement psi = JSChangeUtil.createJSTreeFromText(psiFile.getProject(), "a,b").getPsi(); final JSCommaExpression commaExpression = PsiTreeUtil.getChildOfType(psi, JSCommaExpression.class); final LeafPsiElement comma = PsiTreeUtil.getChildOfType(commaExpression, LeafPsiElement.class); if (comma != null && comma.getNode().getElementType() == JSTokenTypes.COMMA) { handlerCallerAnchorInArgumentList.getParent().addAfter(comma, handlerCallerAnchorInArgumentList); } } ensureTrailingSemicolonPresent(psiFile, created); return created; } else if (myExistingUnresolvedReverence != null) { ensureTrailingSemicolonPresent(psiFile, myExistingUnresolvedReverence); return myExistingUnresolvedReverence; } } else if (inEventConstantExpression) { final String text = "addEventListener(" + eventConstantExpression.getExpression().getText() + ", " + eventHandlerName + ");"; final PsiElement element = JSChangeUtil.createJSTreeFromText(psiFile.getProject(), text, JavaScriptSupportLoader.ECMA_SCRIPT_L4).getPsi(); if (element != null) { final PsiElement addedElement = eventConstantExpression.replace(element); final JSExpression expression = ((JSExpressionStatement)addedElement).getExpression(); final JSArgumentList argumentList = PsiTreeUtil.findChildOfType(expression, JSArgumentList.class); final JSExpression[] arguments = argumentList == null ? JSExpression.EMPTY_ARRAY : argumentList.getArguments(); if (arguments.length == 2) { return arguments[1]; } } } return null; } private static void ensureTrailingSemicolonPresent(final PsiFile psiFile, final PsiElement element) { final JSCallExpression callExpression = PsiTreeUtil.getParentOfType(element, JSCallExpression.class); if (callExpression != null && JSResolveUtil.isEventListenerCall(callExpression)) { final PsiElement parent = callExpression.getParent(); if (parent instanceof JSExpressionStatement) { final PsiElement lastChild = parent.getLastChild(); if (lastChild == callExpression) { final PsiElement psi = JSChangeUtil.createJSTreeFromText(psiFile.getProject(), ";").getPsi(); final PsiElement semicolon = psi.getFirstChild(); if (semicolon != null && semicolon.getNode().getElementType() == JSTokenTypes.SEMICOLON) { parent.addAfter(semicolon, callExpression); } } } } } @Nullable private JSClass getEventBaseClass() { final PsiElement eventClass = JSDialectSpecificHandlersFactory.forElement(myJsClass).getClassResolver() .findClassByQName(FlexCommonTypeNames.FLASH_EVENT_FQN, myJsClass); if (eventClass instanceof JSClass) return (JSClass)eventClass; return null; } private void prepareForMxmlEventAttributeValue(final XmlAttribute xmlAttribute, final String eventType) { eventClassFqn = eventType; methodBody = StringUtil.notNullize(xmlAttribute.getValue()).trim(); if (methodBody.length() > 0 && !methodBody.endsWith(";") && !methodBody.endsWith("}")) methodBody += ";"; final XmlTag xmlTag = xmlAttribute.getParent(); final String eventName = xmlAttribute.getName(); final String id = xmlTag == null ? null : xmlTag.getAttributeValue("id"); if (xmlTag != null && xmlTag.getParent() instanceof XmlDocument) { eventHandlerName = eventName + "Handler"; } else if (id == null) { final String name = xmlTag == null ? "" : xmlTag.getLocalName(); final String idBase = name.isEmpty() ? "" : Character.toLowerCase(name.charAt(0)) + name.substring(1); int i = 0; do { i++; eventHandlerName = MessageFormat.format(METHOD_NAME_PATTERN, idBase + i, eventName); } while (myJsClass.findFunctionByName(eventHandlerName) != null); } else { eventHandlerName = MessageFormat.format(METHOD_NAME_PATTERN, id, eventName); } eventHandlerName2 = "on" + (eventName.isEmpty() ? "Event" : Character.toUpperCase(eventName.charAt(0)) + eventName.substring(1)); } private void prepareForEventListenerCall(final JSCallExpression callExpression) { final JSExpression[] params = callExpression.getArguments(); String eventName = "event"; if (params.length > 0) { handlerCallerAnchorInArgumentList = params[0]; PsiElement sibling = params[0]; while ((sibling = sibling.getNextSibling()) != null) { final ASTNode node = sibling.getNode(); if (node != null && node.getElementType() == JSTokenTypes.COMMA) { handlerCallerAnchorInArgumentList = sibling; if (params.length >= 2) { handlerCallerAnchorInArgumentList = null; if (isUnresolvedReference(params[1])) { myExistingUnresolvedReverence = (JSReferenceExpression)params[1]; eventHandlerName = myExistingUnresolvedReverence.getReferencedName(); } } break; } } if (params[0] instanceof JSReferenceExpression) { final JSReferenceExpression referenceExpression = (JSReferenceExpression)params[0]; final JSExpression qualifier = referenceExpression.getQualifier(); if (qualifier != null) { final PsiReference[] references = qualifier.getReferences(); PsiElement resolveResult; if (references.length == 1 && ((resolveResult = references[0].resolve()) instanceof JSClass) && isEventClass((JSClass)resolveResult)) { eventClassFqn = ((JSClass)resolveResult).getQualifiedName(); } } final PsiReference reference = referenceExpression.getReference(); final PsiElement resolved = reference == null ? null : reference.resolve(); if (resolved instanceof JSVariable && ((JSVariable)resolved).hasInitializer()) { eventName = initializerToPartialMethodName(((JSVariable)resolved).getInitializer().getText()); } } else if (params[0] instanceof JSLiteralExpression) { eventName = initializerToPartialMethodName(params[0].getText()); } } if (handlerCallerAnchorInArgumentList != null) { final JSExpression qualifier = ((JSReferenceExpression)callExpression.getMethodExpression()).getQualifier(); final NamesValidator validator = LanguageNamesValidation.INSTANCE.forLanguage(JavaScriptSupportLoader.JAVASCRIPT.getLanguage()); if (qualifier != null && validator.isIdentifier(qualifier.getText(), null)) { String qualifierText = qualifier.getText(); if (qualifierText.length() > 1 && qualifierText.charAt(0) == '_' && validator.isIdentifier(qualifierText.substring(1), null)) { qualifierText = qualifierText.substring(1); } eventHandlerName = MessageFormat.format(METHOD_NAME_PATTERN, qualifierText, eventName); } else { eventHandlerName = eventName + "Handler"; } } eventHandlerName2 = "on" + (eventName.isEmpty() ? "Event" : Character.toUpperCase(eventName.charAt(0)) + eventName.substring(1)); } private static class MyExpression extends Expression { private final TextResult myResult; private final LookupElement[] myLookupItems; public MyExpression(final String... variants) { myResult = new TextResult(variants[0]); myLookupItems = variants.length == 1 ? LookupElement.EMPTY_ARRAY : new LookupElement[variants.length]; if (variants.length > 1) { for (int i = 0; i < variants.length; i++) { myLookupItems[i] = LookupElementBuilder.create(variants[i]); } } } public Result calculateResult(ExpressionContext context) { return myResult; } public Result calculateQuickResult(ExpressionContext context) { return myResult; } public LookupElement[] calculateLookupItems(ExpressionContext context) { return myLookupItems; } } } }