IDEA-80865 extract as include refactoring

This commit is contained in:
Eugene Kudelevsky
2012-08-17 14:45:05 +04:00
parent 2838eefe6f
commit 58e83106fe
36 changed files with 890 additions and 62 deletions
@@ -9,30 +9,39 @@ import com.intellij.psi.xml.XmlTag;
import org.jetbrains.android.refactoring.AndroidRefactoringContextProvider;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* @author Eugene.Kudelevsky
*/
public class AndroidDesignerRefactoringContextProvider implements AndroidRefactoringContextProvider {
@NotNull
@Override
public XmlTag getComponentTag(@NotNull DataContext dataContext) {
public XmlTag[] getComponentTags(@NotNull DataContext dataContext) {
final EditableArea area = EditableArea.DATA_KEY.getData(dataContext);
if (area == null) {
return null;
return XmlTag.EMPTY;
}
final List<RadComponent> selection = area.getSelection();
if (selection.size() != 1) {
return null;
if (selection.size() == 0) {
return XmlTag.EMPTY;
}
final RadComponent component = selection.get(0);
final List<XmlTag> tags = new ArrayList<XmlTag>(selection.size());
if (!(component instanceof RadViewComponent)) {
return null;
for (RadComponent component : selection) {
if (!(component instanceof RadViewComponent)) {
return XmlTag.EMPTY;
}
final XmlTag tag = ((RadViewComponent)component).getTag();
if (tag == null || tag.equals(EmptyXmlTag.INSTANCE)) {
return XmlTag.EMPTY;
}
tags.add(tag);
}
final XmlTag tag = ((RadViewComponent)component).getTag();
return tag != null && !tag.equals(EmptyXmlTag.INSTANCE) ? tag : null;
return tags.toArray(new XmlTag[tags.size()]);
}
}
@@ -60,6 +60,7 @@ import com.intellij.util.ThrowableConsumer;
import com.intellij.util.ThrowableRunnable;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.maven.AndroidMavenUtil;
import org.jetbrains.android.refactoring.AndroidExtractAsIncludeAction;
import org.jetbrains.android.refactoring.AndroidExtractStyleAction;
import org.jetbrains.android.refactoring.AndroidInlineStyleReferenceAction;
import org.jetbrains.android.sdk.AndroidPlatform;
@@ -163,10 +164,13 @@ public final class AndroidDesignerEditorPanel extends DesignerEditorPanel {
final ActionManager manager = ActionManager.getInstance();
AnAction action = manager.getAction(AndroidExtractStyleAction.ACTION_ID);
group.add(new AndroidRefactoringActionWrapper("_Extract style", action));
group.add(new AndroidRefactoringActionWrapper("_Extract Style", action));
action = manager.getAction(AndroidInlineStyleReferenceAction.ACTION_ID);
group.add(new AndroidRefactoringActionWrapper("_Inline style", action));
group.add(new AndroidRefactoringActionWrapper("_Inline Style", action));
action = manager.getAction(AndroidExtractAsIncludeAction.ACTION_ID);
group.add(new AndroidRefactoringActionWrapper("E_xtract Layout", action));
return group;
}
@@ -34,8 +34,7 @@ import com.intellij.openapi.util.Disposer;
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.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.ui.*;
import com.intellij.ui.components.JBTabbedPane;
@@ -246,10 +245,10 @@ public class ResourceDialog extends DialogWrapper implements TreeSelectionListen
private void createNewResourceFile(ResourceType resourceType) {
AndroidFacet facet = AndroidFacet.getInstance(myModule);
PsiElement[] elements = CreateResourceFileAction.createFileResource(facet, resourceType, null, true);
XmlFile newFile = CreateResourceFileAction.createFileResource(facet, resourceType, null, true);
if (elements.length == 1) {
String name = ((PsiFile)elements[0]).getName();
if (newFile != null) {
String name = newFile.getName();
int index = name.lastIndexOf('.');
if (index != -1) {
name = name.substring(0, index);
@@ -425,4 +425,5 @@ android.extract.style.title=Extract Android Style
android.inline.style.title=Inline Android Style
android.inline.style.command.name=Inline Style ''{0}''
android.inline.style.inline.all.text=Inline &all references and remove the style
android.inline.style.inline.this.text=Inline &this usage and keep the style
android.inline.style.inline.this.text=Inline &this usage and keep the style
android.extract.as.include.title=Extract Android Layout
+5 -1
View File
@@ -82,10 +82,14 @@
icon="/icons/attachDebugger.png">
<add-to-group group-id="ToolbarRunGroup" anchor="after" relative-to-action="RunnerActions"/>
</action>
<action id="AndroidExtractStyleAction" class="org.jetbrains.android.refactoring.AndroidExtractStyleAction" text="Extract St_yle..."
<action id="AndroidExtractStyleAction" class="org.jetbrains.android.refactoring.AndroidExtractStyleAction" text="St_yle..."
description="Pull out style-related attributes from layout and extract them as a new style">
<add-to-group group-id="IntroduceActionsGroup"/>
</action>
<action id="AndroidExtractAsIncludeAction" class="org.jetbrains.android.refactoring.AndroidExtractAsIncludeAction"
text="_Layout..." description="Extract one or more views into a separate layout">
<add-to-group group-id="IntroduceActionsGroup"/>
</action>
<action id="AndroidInlineStyleReferenceAction" class="org.jetbrains.android.refactoring.AndroidInlineStyleReferenceAction"
text="Inline _Style..." description="Inlines Android style">
<add-to-group group-id="RefactoringMenu"/>
@@ -37,6 +37,7 @@ import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiManager;
import com.intellij.psi.xml.XmlFile;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.util.AndroidBundle;
import org.jetbrains.android.util.AndroidResourceUtil;
@@ -91,11 +92,24 @@ public class CreateResourceFileAction extends CreateElementActionBase {
});
}
@Nullable
public static XmlFile createFileResource(@NotNull AndroidFacet facet,
@NotNull final ResourceType resType,
@Nullable String resName,
boolean chooseResName) {
final PsiElement[] elements = doCreateFileResource(facet, resType, resName, chooseResName);
if (elements.length == 0) {
return null;
}
assert elements.length == 1 && elements[0] instanceof XmlFile;
return (XmlFile)elements[0];
}
@NotNull
public static PsiElement[] createFileResource(@NotNull AndroidFacet facet,
@NotNull final ResourceType resType,
@Nullable String resName,
boolean chooseResName) {
private static PsiElement[] doCreateFileResource(@NotNull AndroidFacet facet,
@NotNull final ResourceType resType,
@Nullable String resName,
boolean chooseResName) {
final CreateResourceFileAction action = getInstance();
final String subdirName;
@@ -14,9 +14,9 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.xml.XmlFile;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.android.actions.CreateResourceFileAction;
import org.jetbrains.android.actions.CreateTypedResourceFileAction;
@@ -73,9 +73,9 @@ public class CreateFileResourceQuickFix implements LocalQuickFix, IntentionActio
@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
final PsiElement[] createdElements =
final XmlFile newFile =
CreateResourceFileAction.createFileResource(myFacet, myResourceType, myResourceName + ".xml", myChooseResName);
if (createdElements.length > 0) {
if (newFile != null) {
UndoUtil.markPsiFileForUndo(myFile);
}
}
@@ -16,8 +16,13 @@ import org.jetbrains.annotations.Nullable;
public abstract class AndroidBaseLayoutRefactoringAction extends AndroidBaseXmlRefactoringAction {
@Override
protected boolean isEnabled(@NotNull XmlTag tag) {
return getLayoutViewElement(tag) != null;
protected boolean isEnabledForTags(@NotNull XmlTag[] tags) {
for (XmlTag tag : tags) {
if (getLayoutViewElement(tag) == null) {
return false;
}
}
return true;
}
@Override
@@ -6,9 +6,14 @@ import com.intellij.openapi.actionSystem.AnActionEvent;
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;
@@ -26,27 +31,124 @@ import org.jetbrains.annotations.Nullable;
abstract class AndroidBaseXmlRefactoringAction extends BaseRefactoringAction {
@Override
protected boolean isAvailableOnElementInEditorAndFile(PsiElement element, Editor editor, PsiFile file, 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 && isEnabled(tag);
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(PsiElement[] elements) {
if (elements.length != 1) {
if (elements.length == 0) {
return false;
}
final PsiElement element = elements[0];
return element instanceof XmlTag &&
AndroidFacet.getInstance(element) != null &&
isEnabled((XmlTag)element);
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 isEnabled(@NotNull XmlTag tag);
protected abstract boolean isEnabledForTags(@NotNull XmlTag[] tags);
protected boolean isEnabledForPsiRange(@NotNull PsiElement from, @Nullable PsiElement to) {
return false;
}
@Override
protected boolean isAvailableForLanguage(Language language) {
@@ -74,7 +176,11 @@ abstract class AndroidBaseXmlRefactoringAction extends BaseRefactoringAction {
return data;
}
if (LangDataKeys.PSI_ELEMENT.is(dataId)) {
return getXmlTagFromExternalContext(context);
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;
}
@@ -83,12 +189,15 @@ abstract class AndroidBaseXmlRefactoringAction extends BaseRefactoringAction {
e.getActionManager(), e.getModifiers()));
}
protected abstract void doRefactor(@NotNull Project project, @NotNull XmlTag tag);
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(DataContext dataContext) {
final XmlTag componentTag = getXmlTagFromExternalContext(dataContext);
return new MyHandler(componentTag);
final XmlTag[] componentTags = getXmlTagsFromExternalContext(dataContext);
return new MyHandler(componentTags);
}
@Override
@@ -96,35 +205,46 @@ abstract class AndroidBaseXmlRefactoringAction extends BaseRefactoringAction {
return false;
}
@Nullable
protected static XmlTag getXmlTagFromExternalContext(DataContext dataContext) {
@NotNull
protected static XmlTag[] getXmlTagsFromExternalContext(DataContext dataContext) {
if (dataContext == null) {
return null;
return XmlTag.EMPTY;
}
for (AndroidRefactoringContextProvider provider : AndroidRefactoringContextProvider.EP_NAME.getExtensions()) {
final XmlTag componentTag = provider.getComponentTag(dataContext);
final XmlTag[] componentTags = provider.getComponentTags(dataContext);
if (componentTag != null) {
return componentTag;
if (componentTags.length > 0) {
return componentTags;
}
}
return null;
return XmlTag.EMPTY;
}
private class MyHandler implements RefactoringActionHandler {
private final XmlTag myTagFromExternalContext;
private final XmlTag[] myTagsFromExternalContext;
private MyHandler(@Nullable XmlTag tagFromExternalContext) {
myTagFromExternalContext = tagFromExternalContext;
private MyHandler(@NotNull XmlTag[] tagsFromExternalContext) {
myTagsFromExternalContext = tagsFromExternalContext;
}
@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file, DataContext dataContext) {
if (myTagFromExternalContext != null) {
doRefactor(project, myTagFromExternalContext);
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;
@@ -133,13 +253,13 @@ abstract class AndroidBaseXmlRefactoringAction extends BaseRefactoringAction {
if (tag == null) {
return;
}
doRefactor(project, tag);
doRefactorForTags(project, new XmlTag[]{tag});
}
@Override
public void invoke(@NotNull Project project, @NotNull PsiElement[] elements, DataContext dataContext) {
if (myTagFromExternalContext != null) {
doRefactor(project, myTagFromExternalContext);
if (myTagsFromExternalContext.length > 0) {
doRefactorForTags(project, myTagsFromExternalContext);
return;
}
if (elements.length != 1) {
@@ -149,7 +269,7 @@ abstract class AndroidBaseXmlRefactoringAction extends BaseRefactoringAction {
if (!(element instanceof XmlTag)) {
return;
}
doRefactor(project, (XmlTag)element);
doRefactorForTags(project, new XmlTag[]{(XmlTag)element});
}
}
}
@@ -0,0 +1,343 @@
package org.jetbrains.android.refactoring;
import com.android.resources.ResourceType;
import com.intellij.openapi.command.UndoConfirmationPolicy;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.InputValidatorEx;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.io.FileUtil;
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.IncorrectOperationException;
import com.intellij.util.containers.HashSet;
import com.intellij.util.xml.DomElement;
import com.intellij.util.xml.DomManager;
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.AndroidBundle;
import org.jetbrains.android.util.AndroidCommonUtils;
import org.jetbrains.android.util.AndroidUtils;
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;
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 title = AndroidBundle.message("android.extract.as.include.title");
final String fileName;
if (myTestConfig != null) {
fileName = myTestConfig.myLayoutFileName;
}
else {
fileName = Messages.showInputDialog(project, "Enter new layout file name", title, Messages.getQuestionIcon(),
null, new MyInputValidatorEx(dir));
}
if (fileName == null) {
return;
}
final String fileName1 = addXmlExtensionIfNecessary(fileName);
new WriteCommandAction.Simple(project, "Extract '" + fileName1 + "' layout", file) {
@Override
protected void run() throws Throwable {
doRefactor(project, file, from, to, dir, parentTag, fileName1, tagsInRange.size() > 1);
}
@Override
protected UndoConfirmationPolicy getUndoConfirmationPolicy() {
return UndoConfirmationPolicy.REQUEST_CONFIRMATION;
}
}.execute();
}
private static void doRefactor(Project project,
PsiFile file,
PsiElement from,
PsiElement to,
PsiDirectory dir,
XmlTag parentTag,
String fileName,
boolean wrapWithMerge) {
final String textToExtract = file.getText().substring(from.getTextRange().getStartOffset(),
to.getTextRange().getEndOffset());
final XmlFile newFile;
try {
final PsiFile f = dir.createFile(fileName);
assert f instanceof XmlFile;
newFile = (XmlFile)f;
}
catch (Exception e) {
AndroidUtils.reportError(project, e.getClass().getName() + ": " + e.getMessage(),
AndroidBundle.message("android.extract.as.include.title"));
return;
}
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);
}
}
}
final String resourceName = AndroidCommonUtils.getResourceName(ResourceType.LAYOUT.getName(), fileName);
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 String addXmlExtensionIfNecessary(@NotNull String inputString) {
final String ext = FileUtil.getExtension(inputString);
return "xml".equals(ext) ? inputString : inputString + ".xml";
}
@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;
}
}
private static class MyInputValidatorEx implements InputValidatorEx {
private final PsiDirectory myDirectory;
public MyInputValidatorEx(@NotNull PsiDirectory directory) {
myDirectory = directory;
}
@Nullable
@Override
public String getErrorText(String inputString) {
if (inputString.length() == 0) {
return null;
}
final String fileName = addXmlExtensionIfNecessary(inputString);
try {
myDirectory.checkCreateFile(fileName);
}
catch (IncorrectOperationException e) {
return e.getMessage();
}
return null;
}
@Override
public boolean checkInput(String inputString) {
return inputString.length() > 0;
}
@Override
public boolean canClose(String inputString) {
return checkInput(inputString) && getErrorText(inputString) == null;
}
}
}
@@ -56,8 +56,8 @@ public class AndroidExtractStyleAction extends AndroidBaseLayoutRefactoringActio
myTestConfig = testConfig;
}
protected boolean isEnabled(@NotNull XmlTag tag) {
return doIsEnabled(tag);
protected boolean isEnabledForTags(@NotNull XmlTag[] tags) {
return tags.length == 1 && doIsEnabled(tags[0]);
}
public static boolean doIsEnabled(@NotNull XmlTag tag) {
@@ -222,7 +222,10 @@ public class AndroidExtractStyleAction extends AndroidBaseLayoutRefactoringActio
}
@Override
protected void doRefactor(@NotNull Project project, @NotNull XmlTag tag) {
protected void doRefactorForTags(@NotNull Project project, @NotNull XmlTag[] tags) {
assert tags.length == 1;
final XmlTag tag = tags[0];
final Module module = ModuleUtilCore.findModuleForPsiElement(tag);
assert module != null;
doExtractStyle(module, tag, true, myTestConfig);
@@ -39,7 +39,10 @@ public class AndroidInlineStyleReferenceAction extends AndroidBaseXmlRefactoring
}
@Override
protected void doRefactor(@NotNull Project project, @NotNull final XmlTag tag) {
protected void doRefactorForTags(@NotNull Project project, @NotNull final XmlTag[] tags) {
assert tags.length == 1;
final XmlTag tag = tags[0];
final PsiFile file = tag.getContainingFile();
if (file == null) {
return;
@@ -83,8 +86,8 @@ public class AndroidInlineStyleReferenceAction extends AndroidBaseXmlRefactoring
}
@Override
protected boolean isEnabled(@NotNull XmlTag tag) {
return AndroidInlineUtil.getUsageData(tag) != null;
protected boolean isEnabledForTags(@NotNull XmlTag[] tags) {
return tags.length == 1 && AndroidInlineUtil.getUsageData(tags[0]) != null;
}
@Override
@@ -4,7 +4,6 @@ import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.psi.xml.XmlTag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* @author Eugene.Kudelevsky
@@ -13,6 +12,6 @@ public interface AndroidRefactoringContextProvider {
ExtensionPointName<AndroidRefactoringContextProvider> EP_NAME =
ExtensionPointName.create("org.jetbrains.android.refactoringContextProvider");
@Nullable
XmlTag getComponentTag(@NotNull DataContext dataContext);
@NotNull
XmlTag[] getComponentTags(@NotNull DataContext dataContext);
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextVi<caret>ew
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MyActivity"
/>
</LinearLayout>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:andr1="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/p1.p2"
andr1:orientation="vertical"
andr1:layout_width="fill_parent"
andr1:layout_height="fill_parent">
<TextView
andr1:lay<caret>out_width="fill_parent"
andr1:layout_height="wrap_content"
andr1:text="Hello World, MyActivity"
app:customAttribute="Hello"
/>
</LinearLayout>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:andr1="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/p1.p2"
andr1:orientation="vertical"
andr1:layout_width="fill_parent"
andr1:layout_height="fill_parent">
<include layout="@layout/extracted"/>
</LinearLayout>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:app="http://schemas.android.com/apk/res/p1.p2" xmlns:andr1="http://schemas.android.com/apk/res/android"
andr1:layout_width="fill_parent"
andr1:layout_height="wrap_content"
andr1:text="Hello World, MyActivity"
app:customAttribute="Hello"
/>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<include layout="@layout/extracted"/>
</LinearLayout>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MyActivity"
/>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<selection><TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MyActivity"
/></selection>
</LinearLayout>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<include layout="@layout/extracted"/>
</LinearLayout>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MyActivity"
/>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<T<selection>extView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MyActivity"
/></selection>
</LinearLayout>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<Linea<caret>rLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MyActivity"
/>
</LinearLayout>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<selection><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MyActivity"
/>
</LinearLayout></selection>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<selection><TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MyActivity"
/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MyActivity"
/></selection>
</LinearLayout>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<include layout="@layout/extracted"/>
</LinearLayout>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MyActivity"
/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MyActivity"
/>
</merge>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<selection><!-- comment1 -->
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MyActivity"
/>
<!-- comment2 -->
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MyActivity"
/></selection>
</LinearLayout>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<include layout="@layout/extracted"/>
</LinearLayout>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<!-- comment1 -->
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MyActivity"
/>
<!-- comment2 -->
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MyActivity"
/>
</merge>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<!-- co<selection>mment1 -->
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MyActivity"
/>
<!-- comment2 -->
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Hello World, MyActivity"
/></selection>
</LinearLayout>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:andr1="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/p1.p2"
andr1:orientation="vertical"
andr1:layout_width="fill_parent"
andr1:layout_height="fill_parent">
<selection><TextView
andr1:layout_width="fill_parent"
andr1:layout_height="wrap_content"
andr1:text="Hello World, MyActivity"
/>
<TextView xmlns:andr2="http://schemas.android.com/apk/res/android"
andr2:layout_width="fill_parent"
andr2:layout_height="wrap_content"
andr2:text="Hello World, MyActivity"/></selection>
</LinearLayout>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:andr1="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/p1.p2"
andr1:orientation="vertical"
andr1:layout_width="fill_parent"
andr1:layout_height="fill_parent">
<include layout="@layout/extracted"/>
</LinearLayout>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:andr1="http://schemas.android.com/apk/res/android">
<TextView
andr1:layout_width="fill_parent"
andr1:layout_height="wrap_content"
andr1:text="Hello World, MyActivity"
/>
<TextView xmlns:andr2="http://schemas.android.com/apk/res/android"
andr2:layout_width="fill_parent"
andr2:layout_height="wrap_content"
andr2:text="Hello World, MyActivity"/>
</merge>
@@ -0,0 +1,72 @@
package org.jetbrains.android.refactoring;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.android.AndroidTestCase;
/**
* @author Eugene.Kudelevsky
*/
public class AndroidExtractAsIncludeTest extends AndroidTestCase {
private static final String BASE_PATH = "refactoring/extractAsInclude/";
public void test1() throws Exception {
doTest();
}
public void test2() throws Exception {
doTest();
}
public void test3() throws Exception {
doTestDisabled();
}
public void test4() throws Exception {
doTestDisabled();
}
public void test5() throws Exception {
doTestDisabled();
}
public void test6() throws Exception {
doTest();
}
public void test7() throws Exception {
doTest();
}
public void test8() throws Exception {
doTestDisabled();
}
public void test9() throws Exception {
doTest();
}
public void test10() throws Exception {
doTest();
}
private void doTest() {
final String testName = getTestName(true);
final VirtualFile f = myFixture.copyFileToProject(BASE_PATH + testName + ".xml", "res/layout/test.xml");
myFixture.configureFromExistingVirtualFile(f);
final String extractedFileName = "extracted.xml";
myFixture.testAction(new AndroidExtractAsIncludeAction(new AndroidExtractAsIncludeAction.MyTestConfig(extractedFileName)));
myFixture.checkResultByFile(BASE_PATH + testName + "_after.xml", true);
myFixture.checkResultByFile("res/layout/" + extractedFileName, BASE_PATH + testName + "_extracted.xml", true);
}
private void doTestDisabled() {
final String testName = getTestName(true);
final VirtualFile f = myFixture.copyFileToProject(BASE_PATH + testName + ".xml", "res/layout/test.xml");
myFixture.configureFromExistingVirtualFile(f);
final Presentation p =
myFixture.testAction(new AndroidExtractAsIncludeAction(new AndroidExtractAsIncludeAction.MyTestConfig("extracted.xml")));
assertTrue(p.isVisible());
assertFalse(p.isEnabled());
}
}