package org.jetbrains.android.refactoring; import com.intellij.lang.Language; import com.intellij.lang.xml.XMLLanguage; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.actionSystem.LangDataKeys; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.SelectionModel; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiComment; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import com.intellij.psi.xml.XmlText; import com.intellij.refactoring.RefactoringActionHandler; import com.intellij.refactoring.actions.BaseRefactoringAction; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * @author Eugene.Kudelevsky */ abstract class AndroidBaseXmlRefactoringAction extends BaseRefactoringAction { @Override protected boolean isAvailableOnElementInEditorAndFile(@NotNull PsiElement element, @NotNull Editor editor, @NotNull PsiFile file, @NotNull DataContext context) { final XmlTag[] tags = getXmlTagsFromExternalContext(context); if (tags.length > 0) { return AndroidFacet.getInstance(tags[0]) != null && isEnabledForTags(tags); } final TextRange range = getNonEmptySelectionRange(editor); if (range != null) { final Pair<PsiElement, PsiElement> psiRange = getExtractableRange( file, range.getStartOffset(), range.getEndOffset()); return psiRange != null && isEnabledForPsiRange(psiRange.getFirst(), psiRange.getSecond()); } if (element == null || AndroidFacet.getInstance(element) == null || PsiTreeUtil.getParentOfType(element, XmlText.class) != null) { return false; } final XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class); return tag != null && isEnabledForTags(new XmlTag[]{tag}); } @Nullable private static TextRange getNonEmptySelectionRange(Editor editor) { if (editor != null) { final SelectionModel model = editor.getSelectionModel(); if (model.hasSelection()) { final int start = model.getSelectionStart(); final int end = model.getSelectionEnd(); if (start < end) { return TextRange.create(start, end); } } } return null; } @Nullable private static Pair<PsiElement, PsiElement> getExtractableRange(PsiFile file, int start, int end) { PsiElement startElement = file.findElementAt(start); PsiElement parent = startElement != null ? startElement.getParent() : null; while (parent != null && !(parent instanceof PsiFile) && parent.getTextRange().getStartOffset() == startElement.getTextRange().getStartOffset()) { startElement = parent; parent = parent.getParent(); } PsiElement endElement = file.findElementAt(end - 1); parent = endElement != null ? endElement.getParent() : null; while (parent != null && !(parent instanceof PsiFile) && parent.getTextRange().getEndOffset() == endElement.getTextRange().getEndOffset()) { endElement = parent; parent = parent.getParent(); } if (startElement == null || endElement == null) { return null; } final PsiElement commonParent = startElement.getParent(); if (commonParent == null || !(commonParent instanceof XmlTag) || commonParent != endElement.getParent()) { return null; } PsiElement e = startElement; boolean containTag = false; while (e != null) { if (!(e instanceof XmlText) && !(e instanceof XmlTag) && !(e instanceof PsiWhiteSpace) && !(e instanceof PsiComment)) { return null; } if (e instanceof XmlTag) { containTag = true; } if (e == endElement) { break; } e = e.getNextSibling(); } return e != null && containTag ? Pair.create(startElement, endElement) : null; } @Override protected boolean isEnabledOnElements(@NotNull PsiElement[] elements) { if (elements.length == 0) { return false; } final PsiElement element = elements[0]; if (AndroidFacet.getInstance(element) == null) { return false; } final XmlTag[] tags = new XmlTag[elements.length]; for (int i = 0; i < tags.length; i++) { if (!(elements[i] instanceof XmlTag)) { return false; } tags[i] = (XmlTag)elements[i]; } return isEnabledForTags(tags); } protected abstract boolean isEnabledForTags(@NotNull XmlTag[] tags); protected boolean isEnabledForPsiRange(@NotNull PsiElement from, @Nullable PsiElement to) { return false; } @Override protected boolean isAvailableForLanguage(Language language) { return language == XMLLanguage.INSTANCE; } @Override protected boolean isAvailableForFile(PsiFile file) { return file instanceof XmlFile && AndroidFacet.getInstance(file) != null && isMyFile(file); } protected abstract boolean isMyFile(PsiFile file); @Override public void update(AnActionEvent e) { final DataContext context = e.getDataContext(); final DataContext patchedContext = new DataContext() { @Override public Object getData(@NonNls String dataId) { final Object data = context.getData(dataId); if (data != null) { return data; } if (CommonDataKeys.PSI_ELEMENT.is(dataId)) { final XmlTag[] tags = getXmlTagsFromExternalContext(context); return tags.length == 1 ? tags[0] : null; } else if (LangDataKeys.PSI_ELEMENT_ARRAY.is(dataId)) { return getXmlTagsFromExternalContext(context); } return null; } }; super.update(new AnActionEvent(e.getInputEvent(), patchedContext, e.getPlace(), e.getPresentation(), e.getActionManager(), e.getModifiers())); } protected abstract void doRefactorForTags(@NotNull Project project, @NotNull XmlTag[] tags); protected void doRefactorForPsiRange(@NotNull Project project, @NotNull PsiFile file, @NotNull PsiElement from, @NotNull PsiElement to) { } @Override protected RefactoringActionHandler getHandler(@NotNull DataContext dataContext) { final XmlTag[] componentTags = getXmlTagsFromExternalContext(dataContext); return new MyHandler(componentTags); } @Override protected boolean isAvailableInEditorOnly() { return false; } @NotNull protected static XmlTag[] getXmlTagsFromExternalContext(DataContext dataContext) { if (dataContext == null) { return XmlTag.EMPTY; } for (AndroidRefactoringContextProvider provider : AndroidRefactoringContextProvider.EP_NAME.getExtensions()) { final XmlTag[] componentTags = provider.getComponentTags(dataContext); if (componentTags.length > 0) { return componentTags; } } return XmlTag.EMPTY; } private class MyHandler implements RefactoringActionHandler { private final XmlTag[] myTagsFromExternalContext; private MyHandler(@NotNull XmlTag[] tagsFromExternalContext) { myTagsFromExternalContext = tagsFromExternalContext; } @Override public void invoke(@NotNull Project project, Editor editor, PsiFile file, DataContext dataContext) { if (myTagsFromExternalContext.length > 0) { doRefactorForTags(project, myTagsFromExternalContext); return; } final TextRange range = getNonEmptySelectionRange(editor); if (range != null) { final Pair<PsiElement, PsiElement> psiRange = getExtractableRange( file, range.getStartOffset(), range.getEndOffset()); if (psiRange != null) { doRefactorForPsiRange(project, file, psiRange.getFirst(), psiRange.getSecond()); } return; } final PsiElement element = getElementAtCaret(editor, file); if (element == null) { return; } final XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class); if (tag == null) { return; } doRefactorForTags(project, new XmlTag[]{tag}); } @Override public void invoke(@NotNull Project project, @NotNull PsiElement[] elements, DataContext dataContext) { if (myTagsFromExternalContext.length > 0) { doRefactorForTags(project, myTagsFromExternalContext); return; } if (elements.length != 1) { return; } final PsiElement element = elements[0]; if (!(element instanceof XmlTag)) { return; } doRefactorForTags(project, new XmlTag[]{(XmlTag)element}); } } }