blob: dd4b2f490ff969398da74e80056432925c68a396 [file] [log] [blame]
package org.jetbrains.android.refactoring;
import com.android.SdkConstants;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.tools.idea.rendering.IncludeReference;
import com.android.tools.idea.rendering.ResourceHelper;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.UndoConfirmationPolicy;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.containers.HashSet;
import com.intellij.util.xml.DomElement;
import com.intellij.util.xml.DomManager;
import org.jetbrains.android.actions.CreateResourceFileAction;
import org.jetbrains.android.dom.layout.Include;
import org.jetbrains.android.dom.layout.LayoutViewElement;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.util.AndroidCommonUtils;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* @author Eugene.Kudelevsky
*/
public class AndroidExtractAsIncludeAction extends AndroidBaseLayoutRefactoringAction {
@NonNls public static final String ACTION_ID = "AndroidExtractAsIncludeAction";
private final MyTestConfig myTestConfig;
@SuppressWarnings("UnusedDeclaration")
public AndroidExtractAsIncludeAction() {
myTestConfig = null;
}
@TestOnly
public AndroidExtractAsIncludeAction(@Nullable MyTestConfig testConfig) {
myTestConfig = testConfig;
}
@Override
protected void doRefactorForTags(@NotNull Project project, @NotNull XmlTag[] tags) {
if (tags.length == 0) {
return;
}
final PsiFile file = tags[0].getContainingFile();
if (file == null) {
return;
}
XmlTag startTag = null;
XmlTag endTag = null;
int startOffset = Integer.MAX_VALUE;
int endOffset = -1;
for (XmlTag tag : tags) {
final TextRange range = tag.getTextRange();
final int start = range.getStartOffset();
if (start < startOffset) {
startOffset = start;
startTag = tag;
}
final int end = range.getEndOffset();
if (end > endOffset) {
endOffset = end;
endTag = tag;
}
}
assert startTag != null && endTag != null;
doRefactorForPsiRange(project, file, startTag, endTag);
}
@Override
protected boolean isEnabledForTags(@NotNull XmlTag[] tags) {
if (tags.length == 0) {
return false;
}
final DomManager domManager = DomManager.getDomManager(tags[0].getProject());
boolean containsViewElement = false;
for (XmlTag tag : tags) {
final DomElement domElement = domManager.getDomElement(tag);
if (!isSuitableDomElement(domElement)) {
return false;
}
if (domElement instanceof LayoutViewElement) {
containsViewElement = true;
}
}
if (!containsViewElement) {
return false;
}
final PsiElement parent = tags[0].getParent();
if (!(parent instanceof XmlTag) || parent.getContainingFile() == null) {
return false;
}
for (int i = 1; i < tags.length; i++) {
if (tags[i].getParent() != parent) {
return false;
}
}
return true;
}
@Override
protected void doRefactorForPsiRange(@NotNull final Project project, @NotNull final PsiFile file, @NotNull final PsiElement from,
@NotNull final PsiElement to) {
final PsiDirectory dir = file.getContainingDirectory();
if (dir == null) {
return;
}
final AndroidFacet facet = AndroidFacet.getInstance(from);
assert facet != null;
final XmlTag parentTag = PsiTreeUtil.getParentOfType(from, XmlTag.class);
assert parentTag != null;
final List<XmlTag> tagsInRange = collectAllTags(from, to);
assert tagsInRange.size() > 0 : "there is no tag inside the range";
final String fileName = myTestConfig != null ? myTestConfig.myLayoutFileName : null;
final String dirName = dir.getName();
final FolderConfiguration config = dirName.length() > 0
? FolderConfiguration.getConfig(dirName.split(SdkConstants.RES_QUALIFIER_SEP))
: null;
final String title = "Extract Android Layout";
CommandProcessor.getInstance().executeCommand(project, new Runnable() {
@Override
public void run() {
final XmlFile newFile =
CreateResourceFileAction.createFileResource(facet, ResourceFolderType.LAYOUT, fileName, "temp_root", config, true, title);
if (newFile != null) {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
doRefactor(facet, file, newFile, from, to, parentTag, tagsInRange.size() > 1);
}
});
}
}
}, title, null, UndoConfirmationPolicy.REQUEST_CONFIRMATION);
}
private static void doRefactor(AndroidFacet facet,
PsiFile file,
XmlFile newFile,
PsiElement from,
PsiElement to,
XmlTag parentTag,
boolean wrapWithMerge) {
final Project project = facet.getModule().getProject();
final String textToExtract = file.getText().substring(from.getTextRange().getStartOffset(),
to.getTextRange().getEndOffset());
final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project);
final Document document = documentManager.getDocument(newFile);
assert document != null;
document.setText("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
(wrapWithMerge ? "<merge>\n" + textToExtract + "\n</merge>" : textToExtract));
documentManager.commitDocument(document);
final Set<String> unknownPrefixes = new HashSet<String>();
newFile.accept(new XmlRecursiveElementVisitor() {
@Override
public void visitXmlTag(XmlTag tag) {
super.visitXmlTag(tag);
final String prefix = tag.getNamespacePrefix();
if (!unknownPrefixes.contains(prefix) && tag.getNamespace().length() == 0) {
unknownPrefixes.add(prefix);
}
}
@Override
public void visitXmlAttribute(XmlAttribute attribute) {
final String prefix = attribute.getNamespacePrefix();
if (!unknownPrefixes.contains(prefix) && attribute.getNamespace().length() == 0) {
unknownPrefixes.add(prefix);
}
}
});
final XmlTag rootTag = newFile.getRootTag();
assert rootTag != null;
final XmlElementFactory elementFactory = XmlElementFactory.getInstance(project);
final XmlAttribute[] attributes = rootTag.getAttributes();
final XmlAttribute firstAttribute = attributes.length > 0 ? attributes[0] : null;
for (String prefix : unknownPrefixes) {
final String namespace = parentTag.getNamespaceByPrefix(prefix);
final String xmlNsAttrName = "xmlns:" + prefix;
if (namespace.length() > 0 && rootTag.getAttribute(xmlNsAttrName) == null) {
final XmlAttribute xmlnsAttr = elementFactory.createXmlAttribute(xmlNsAttrName, namespace);
if (firstAttribute != null) {
rootTag.addBefore(xmlnsAttr, firstAttribute);
}
else {
rootTag.add(xmlnsAttr);
}
}
}
String includingLayout = SdkConstants.LAYOUT_RESOURCE_PREFIX + ResourceHelper.getResourceName(file);
IncludeReference.setIncludingLayout(project, newFile, includingLayout);
final String resourceName = AndroidCommonUtils.getResourceName(ResourceType.LAYOUT.getName(), newFile.getName());
final XmlTag includeTag = elementFactory.createTagFromText("<include layout=\"@layout/" + resourceName + "\"/>");
parentTag.addAfter(includeTag, to);
parentTag.deleteChildRange(from, to);
final CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(project);
codeStyleManager.reformat(newFile);
}
@NotNull
private static List<XmlTag> collectAllTags(PsiElement from, PsiElement to) {
final List<XmlTag> result = new ArrayList<XmlTag>();
PsiElement e = from;
while (e != null) {
if (e instanceof XmlTag) {
result.add((XmlTag)e);
}
if (e == to) {
break;
}
e = e.getNextSibling();
}
assert e != null : "invalid range";
return result;
}
@Override
protected boolean isEnabledForPsiRange(@NotNull PsiElement from, @Nullable PsiElement to) {
final DomManager domManager = DomManager.getDomManager(from.getProject());
PsiElement e = from;
boolean containsViewElement = false;
while (e != null) {
if (e instanceof XmlTag) {
final DomElement domElement = domManager.getDomElement((XmlTag)e);
if (!isSuitableDomElement(domElement)) {
return false;
}
if (domElement instanceof LayoutViewElement) {
containsViewElement = true;
}
}
if (e == to) {
break;
}
e = e.getNextSibling();
}
return containsViewElement;
}
private static boolean isSuitableDomElement(DomElement element) {
return element instanceof LayoutViewElement ||
element instanceof Include;
}
static class MyTestConfig {
private final String myLayoutFileName;
MyTestConfig(@NotNull String layoutFileName) {
myLayoutFileName = layoutFileName;
}
}
}