diff options
author | Tor Norbye <tnorbye@google.com> | 2012-11-09 13:29:24 -0800 |
---|---|---|
committer | Tor Norbye <tnorbye@google.com> | 2012-11-26 17:30:22 -0800 |
commit | a96737ef1ee717e322c87a8ef391440b2aaf03b6 (patch) | |
tree | 602b312ed80c22e7274b75e2df9a978049b4ca5d /eclipse | |
parent | c88485c606173a591f15d7f24c4b37133ac24ac8 (diff) | |
download | sdk-a96737ef1ee717e322c87a8ef391440b2aaf03b6.zip sdk-a96737ef1ee717e322c87a8ef391440b2aaf03b6.tar.gz sdk-a96737ef1ee717e322c87a8ef391440b2aaf03b6.tar.bz2 |
Add resource renaming support
This changeset adds support for renaming resources.
There are several new hooks for initiating a resource rename:
(1) You can use the same keybinding as in Java files from XML files to
initiate refactoring; for example, place the caret somewhere in
@+id/foo or @string/bar and hit the refactoring keybinding, and a
rename resource refactoring dialog shows up.
(2) Invoking Quick Assistant in an XML file (Ctrl/Cmd 1) will offer to
rename the resource, if the caret is over a resource name.
(3) Renaming an XML or bitmap resource file, such as
res/drawable-hdpi/foo.png, will now initiate the same XML resource
naming machinery as above to update all resource references, plus
it will also update all the other versions of the same file
(e.g. in res/drawable-mdpi, res/drawable-xhdpi, etc.). Assuming an
R field exists (e.g. the project has been built), it will also
optionally update all Java field references.
(4) Invoking renaming in the layout editor (via the rename keybinding,
or via the context menu, or via the property sheet's "..." button)
will also initiate id resource refactoring. Editing the id
directly in the inline editor for the id will pop up a dialog
asking whether to update references as well, along with a "Do not
ask again" checkbox.
(5) Finally, there is a renaming participant registered which will
discover whether an R field is renamed, so if you go and rename
R.layout.foo from Java, this will also kick in all of the above
machinery - renaming layout files, updating resource references,
etc.
If the renamed resource is in a library project, the refactoring
will also look at all the including projects and offer to update
references there as well.
Finally, this CL goes and fixes a few bugs in the existing refactoring
operations; in particular, making sure that they not only look at
files in layout/ but in all folder configurations containing layout
files. It also adds refactoring unit tests.
Change-Id: Ie88511a571b414fdc5be048e781fe29a34063cbf
Diffstat (limited to 'eclipse')
24 files changed, 2964 insertions, 88 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml index 3461516..d99ece5 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml +++ b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml @@ -1146,6 +1146,25 @@ </enablement> </renameParticipant> <renameParticipant + class="com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceParticipant" + id="com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceParticipant" + name="Android Rename Resource Participant"> + <enablement> + <with variable="element"> + <or> + <instanceof value="org.eclipse.jdt.core.IField"/> + <instanceof value="org.eclipse.core.resources.IResource" /> + <instanceof value="java.lang.String" /> + </or> + </with> + <with variable="affectedNatures"> + <iterate operator="or"> + <equals value="com.android.ide.eclipse.adt.AndroidNature" /> + </iterate> + </with> + </enablement> + </renameParticipant> + <renameParticipant class="com.android.ide.eclipse.adt.internal.refactorings.core.AndroidPackageRenameParticipant" id="com.android.ide.eclipse.adt.internal.refactoring.core.AndroidPackageRenameParticipant" name="Android Rename Package Participant"> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java index 3fe89e0..83ce9ef 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java @@ -44,7 +44,6 @@ import com.android.ide.common.api.IClientRulesEngine; import com.android.ide.common.api.IDragElement; import com.android.ide.common.api.IMenuCallback; import com.android.ide.common.api.INode; -import com.android.ide.common.api.IValidator; import com.android.ide.common.api.IViewMetadata; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.RuleAction; @@ -191,32 +190,11 @@ public class BaseViewRule extends AbstractViewRule { // Ids must be set individually so open the id dialog for each // selected node (though allow cancel to break the loop) for (INode node : selectedNodes) { - // Strip off the @id prefix stuff - String oldId = node.getStringAttr(ANDROID_URI, ATTR_ID); - oldId = stripIdPrefix(ensureValidString(oldId)); - IValidator validator = mRulesEngine.getResourceValidator("id",//$NON-NLS-1$ - false /*uniqueInProject*/, - true /*uniqueInLayout*/, - false /*exists*/, - oldId); - String newId = mRulesEngine.displayInput("New Id:", oldId, validator); - if (newId != null && newId.trim().length() > 0) { - if (!newId.startsWith(NEW_ID_PREFIX)) { - newId = NEW_ID_PREFIX + stripIdPrefix(newId); - } - node.editXml("Change ID", new PropertySettingNodeHandler(ANDROID_URI, - ATTR_ID, newId)); - editedProperty(ATTR_ID); - } else if (newId != null) { - // Clear - node.editXml("Change ID", new PropertySettingNodeHandler(ANDROID_URI, - ATTR_ID, null)); - editedProperty(ATTR_ID); - } else if (newId == null) { - // Cancelled + if (!mRulesEngine.rename(node)) { break; } } + editedProperty(ATTR_ID); return; } else if (isProp) { INode firstNode = selectedNodes.get(0); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java index af42989..6e051df 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java @@ -29,6 +29,8 @@ import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper.IProjectFilter; import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; import com.android.sdklib.AndroidVersion; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.repository.PkgProps; @@ -1509,4 +1511,108 @@ public class AdtUtils { return s; } + + /** + * Looks up the {@link ResourceFolderType} corresponding to a given + * {@link ResourceType}: the folder where those resources can be found. + * <p> + * Note that {@link ResourceType#ID} is a special case: it can not just + * be defined in {@link ResourceFolderType#VALUES}, but it can also be + * defined inline via {@code @+id} in {@link ResourceFolderType#LAYOUT} and + * {@link ResourceFolderType#MENU} folders. + * + * @param type the resource type + * @return the corresponding resource folder type + */ + @NonNull + public static ResourceFolderType getFolderTypeFor(@NonNull ResourceType type) { + switch (type) { + case ANIM: + return ResourceFolderType.ANIM; + case ANIMATOR: + return ResourceFolderType.ANIMATOR; + case ARRAY: + return ResourceFolderType.VALUES; + case COLOR: + return ResourceFolderType.COLOR; + case DRAWABLE: + return ResourceFolderType.DRAWABLE; + case INTERPOLATOR: + return ResourceFolderType.INTERPOLATOR; + case LAYOUT: + return ResourceFolderType.LAYOUT; + case MENU: + return ResourceFolderType.MENU; + case MIPMAP: + return ResourceFolderType.MIPMAP; + case RAW: + return ResourceFolderType.RAW; + case XML: + return ResourceFolderType.XML; + case ATTR: + case BOOL: + case DECLARE_STYLEABLE: + case DIMEN: + case FRACTION: + case ID: + case INTEGER: + case PLURALS: + case PUBLIC: + case STRING: + case STYLE: + case STYLEABLE: + return ResourceFolderType.VALUES; + default: + assert false : type; + return ResourceFolderType.VALUES; + + } + } + + /** + * Looks up the {@link ResourceType} defined in a given {@link ResourceFolderType}. + * <p> + * Note that for {@link ResourceFolderType#VALUES} there are many, many + * different types of resources that can be defined, so this method returns + * {@code null} for that scenario. + * <p> + * Note also that {@link ResourceType#ID} is a special case: it can not just + * be defined in {@link ResourceFolderType#VALUES}, but it can also be + * defined inline via {@code @+id} in {@link ResourceFolderType#LAYOUT} and + * {@link ResourceFolderType#MENU} folders. + * + * @param folderType the resource folder type + * @return the corresponding resource type, or null if {@code folderType} is + * {@link ResourceFolderType#VALUES} + */ + @Nullable + public static ResourceType getResourceTypeFor(@NonNull ResourceFolderType folderType) { + switch (folderType) { + case ANIM: + return ResourceType.ANIM; + case ANIMATOR: + return ResourceType.ANIMATOR; + case COLOR: + return ResourceType.COLOR; + case DRAWABLE: + return ResourceType.DRAWABLE; + case INTERPOLATOR: + return ResourceType.INTERPOLATOR; + case LAYOUT: + return ResourceType.LAYOUT; + case MENU: + return ResourceType.MENU; + case MIPMAP: + return ResourceType.MIPMAP; + case RAW: + return ResourceType.RAW; + case XML: + return ResourceType.XML; + case VALUES: + return null; + default: + assert false : folderType; + return null; + } + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java index fcfdfd1..feeeef1 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java @@ -25,6 +25,7 @@ import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; import com.android.ide.eclipse.adt.internal.lint.EclipseLintRunner; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceXmlTextAction; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; @@ -41,6 +42,8 @@ import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.QualifiedName; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.ui.actions.IJavaEditorActionDefinitionIds; +import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.jface.text.BadLocationException; @@ -802,7 +805,16 @@ public abstract class AndroidXmlEditor extends FormEditor { */ private void createTextEditor() { try { - mTextEditor = new StructuredTextEditor(); + mTextEditor = new StructuredTextEditor() { + @Override + protected void createActions() { + super.createActions(); + + Action action = new RenameResourceXmlTextAction(mTextEditor); + action.setActionDefinitionId(IJavaEditorActionDefinitionIds.RENAME_ELEMENT); + setAction(IJavaEditorActionDefinitionIds.RENAME_ELEMENT, action); + } + }; int index = addPage(mTextEditor, getEditorInput()); mTextPageIndex = index; setPageText(index, mTextEditor.getTitle()); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java index aef1763..e94822b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java @@ -1116,7 +1116,7 @@ public class Hyperlinks { } /** Parse a resource reference or a theme reference and return the individual parts */ - private static Pair<ResourceType,String> parseResource(String url) { + public static Pair<ResourceType,String> parseResource(String url) { if (url.startsWith(PREFIX_THEME_REF)) { String remainder = url.substring(PREFIX_THEME_REF.length()); int colon = url.indexOf(':'); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java index 4b6b803..0d32256 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java @@ -150,7 +150,7 @@ public class LayoutCanvas extends Canvas { private DropTarget mDropTarget; /** Factory that can create {@link INode} proxies. */ - private final NodeFactory mNodeFactory = new NodeFactory(this); + private final @NonNull NodeFactory mNodeFactory = new NodeFactory(this); /** Vertical scaling & scrollbar information. */ private final CanvasTransform mVScale; @@ -599,8 +599,11 @@ public class LayoutCanvas extends Canvas { /** * Returns the factory to use to convert from {@link CanvasViewInfo} or from * {@link UiViewElementNode} to {@link INode} proxies. + * + * @return the node factory */ - NodeFactory getNodeFactory() { + @NonNull + public NodeFactory getNodeFactory() { return mNodeFactory; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java index 7c5cd4b..eb3d6f2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java @@ -15,6 +15,7 @@ */ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.FQCN_SPACE; import static com.android.SdkConstants.FQCN_SPACE_V7; @@ -22,9 +23,7 @@ import static com.android.SdkConstants.NEW_ID_PREFIX; import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.PIXEL_MARGIN; import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.PIXEL_RADIUS; - import com.android.SdkConstants; -import static com.android.SdkConstants.ANDROID_URI; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.api.INode; @@ -39,6 +38,8 @@ import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceWizard; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResult; import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; import com.android.resources.ResourceType; import com.android.utils.Pair; @@ -1184,40 +1185,78 @@ public class SelectionManager implements ISelectionProvider { if (selections.size() > 0) { NodeProxy primary = selections.get(0).getNode(); if (primary != null) { - String currentId = primary.getStringAttr(ANDROID_URI, ATTR_ID); - currentId = BaseViewRule.stripIdPrefix(currentId); - InputDialog d = new InputDialog( - AdtPlugin.getDisplay().getActiveShell(), - "Set ID", - "New ID:", - currentId, - ResourceNameValidator.create(false, (IProject) null, ResourceType.ID)); - if (d.open() == Window.OK) { - final String s = d.getValue(); - mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel("Set ID", - new Runnable() { - @Override - public void run() { - String newId = s; - newId = NEW_ID_PREFIX + BaseViewRule.stripIdPrefix(s); - for (SelectionItem item : selections) { - item.getNode().setAttribute(ANDROID_URI, ATTR_ID, newId); - } + performRename(primary, selections); + } + } + } - LayoutCanvas canvas = mCanvas; - CanvasViewInfo root = canvas.getViewHierarchy().getRoot(); - if (root != null) { - UiViewElementNode uiViewNode = root.getUiViewNode(); - NodeFactory nodeFactory = canvas.getNodeFactory(); - NodeProxy rootNode = nodeFactory.create(uiViewNode); - if (rootNode != null) { - rootNode.applyPendingChanges(); - } + /** + * Performs renaming the given node. + * + * @param primary the node to be renamed, or the primary node (to get the + * current value from if more than one node should be renamed) + * @param selections if not null, a list of nodes to apply the setting to + * (which should include the primary) + * @return the result of the renaming operation + */ + @NonNull + public RenameResult performRename( + final @NonNull INode primary, + final @Nullable List<SelectionItem> selections) { + String id = primary.getStringAttr(ANDROID_URI, ATTR_ID); + if (id != null && !id.isEmpty()) { + RenameResult result = RenameResourceWizard.renameResource( + mCanvas.getShell(), + mCanvas.getEditorDelegate().getGraphicalEditor().getProject(), + ResourceType.ID, BaseViewRule.stripIdPrefix(id), null, true /*canClear*/); + if (result.isCanceled()) { + return result; + } else if (!result.isUnavailable()) { + return result; + } + } + String currentId = primary.getStringAttr(ANDROID_URI, ATTR_ID); + currentId = BaseViewRule.stripIdPrefix(currentId); + InputDialog d = new InputDialog( + AdtPlugin.getDisplay().getActiveShell(), + "Set ID", + "New ID:", + currentId, + ResourceNameValidator.create(false, (IProject) null, ResourceType.ID)); + if (d.open() == Window.OK) { + final String s = d.getValue(); + mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel("Set ID", + new Runnable() { + @Override + public void run() { + String newId = s; + newId = NEW_ID_PREFIX + BaseViewRule.stripIdPrefix(s); + if (selections != null) { + for (SelectionItem item : selections) { + NodeProxy node = item.getNode(); + if (node != null) { + node.setAttribute(ANDROID_URI, ATTR_ID, newId); } } - }); + } else { + primary.setAttribute(ANDROID_URI, ATTR_ID, newId); + } + + LayoutCanvas canvas = mCanvas; + CanvasViewInfo root = canvas.getViewHierarchy().getRoot(); + if (root != null) { + UiViewElementNode uiViewNode = root.getUiViewNode(); + NodeFactory nodeFactory = canvas.getNodeFactory(); + NodeProxy rootNode = nodeFactory.create(uiViewNode); + if (rootNode != null) { + rootNode.applyPendingChanges(); + } + } } - } + }); + return RenameResult.name(BaseViewRule.stripIdPrefix(s)); + } else { + return RenameResult.canceled(); } } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java index 460ef21..472b158 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java @@ -50,6 +50,7 @@ import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy; import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResult; import com.android.ide.eclipse.adt.internal.resources.CyclicDependencyValidator; import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; @@ -156,6 +157,15 @@ class ClientRulesEngine implements IClientRulesEngine { } @Override + public boolean rename(INode node) { + GraphicalEditorPart editor = mRulesEngine.getEditor(); + SelectionManager manager = editor.getCanvasControl().getSelectionManager(); + RenameResult result = manager.performRename(node, null); + + return !result.isCanceled() && !result.isUnavailable(); + } + + @Override public String displayInput(@NonNull String message, @Nullable String value, final @Nullable IValidator filter) { IInputValidator validator = null; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyEditor.java index 41d4cd1..87fb0e6 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyEditor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyEditor.java @@ -21,21 +21,31 @@ import static com.android.SdkConstants.ANDROID_THEME_PREFIX; import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.DOT_PNG; import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.NEW_ID_PREFIX; import static com.android.SdkConstants.PREFIX_RESOURCE_REF; import static com.android.SdkConstants.PREFIX_THEME_REF; +import static com.android.ide.common.layout.BaseViewRule.stripIdPrefix; import com.android.annotations.NonNull; import com.android.ide.common.api.IAttributeInfo; import com.android.ide.common.api.IAttributeInfo.Format; +import com.android.ide.common.layout.BaseViewRule; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.resources.ResourceRepository; import com.android.ide.common.resources.ResourceResolver; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderService; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceWizard; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResult; import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; import com.android.ide.eclipse.adt.internal.ui.ReferenceChooserDialog; @@ -47,6 +57,9 @@ import com.google.common.collect.Maps; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.MessageDialogWithToggle; +import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.window.Window; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; @@ -301,16 +314,101 @@ class XmlPropertyEditor extends AbstractTextPropertyEditor { @Override protected boolean setEditorText(Property property, String text) throws Exception { + Object oldValue = property.getValue(); + String old = oldValue != null ? oldValue.toString() : null; + + // If users enters a new id without specifying the @id/@+id prefix, insert it + boolean isId = isIdProperty(property); + if (isId && !text.startsWith(PREFIX_RESOURCE_REF)) { + text = NEW_ID_PREFIX + text; + } + + // Handle id refactoring: if you change an id, may want to update references too. + // Ask user. + if (isId && property instanceof XmlProperty + && old != null && !old.isEmpty() + && text != null && !text.isEmpty() + && !text.equals(old)) { + XmlProperty xmlProperty = (XmlProperty) property; + IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); + String refactorPref = store.getString(AdtPrefs.PREFS_REFACTOR_IDS); + boolean performRefactor = false; + Shell shell = AdtPlugin.getShell(); + if (refactorPref == null + || refactorPref.isEmpty() + || refactorPref.equals(MessageDialogWithToggle.PROMPT)) { + MessageDialogWithToggle dialog = + MessageDialogWithToggle.openYesNoCancelQuestion( + shell, + "Update References?", + "Update all references as well? " + + "This will update all XML references and Java R field references.", + "Do not show again", + false, + store, + AdtPrefs.PREFS_REFACTOR_IDS); + switch (dialog.getReturnCode()) { + case IDialogConstants.CANCEL_ID: + return false; + case IDialogConstants.YES_ID: + performRefactor = true; + break; + case IDialogConstants.NO_ID: + performRefactor = false; + break; + } + } else { + performRefactor = refactorPref.equals(MessageDialogWithToggle.ALWAYS); + } + if (performRefactor) { + CommonXmlEditor xmlEditor = xmlProperty.getXmlEditor(); + if (xmlEditor != null) { + IProject project = xmlEditor.getProject(); + if (project != null && shell != null) { + RenameResourceWizard.renameResource(shell, project, + ResourceType.ID, stripIdPrefix(old), stripIdPrefix(text), false); + } + } + } + } + property.setValue(text); + return true; } + private static boolean isIdProperty(Property property) { + XmlProperty xmlProperty = (XmlProperty) property; + return xmlProperty.getDescriptor().getXmlLocalName().equals(ATTR_ID); + } + private void openDialog(PropertyTable propertyTable, Property property) throws Exception { XmlProperty xmlProperty = (XmlProperty) property; IAttributeInfo attributeInfo = xmlProperty.getDescriptor().getAttributeInfo(); - boolean isId = xmlProperty.getDescriptor().getXmlLocalName().equals(ATTR_ID); - if (isId) { + if (isIdProperty(property)) { + Object value = xmlProperty.getValue(); + if (value != null && !value.toString().isEmpty()) { + GraphicalEditorPart editor = xmlProperty.getGraphicalEditor(); + if (editor != null) { + LayoutCanvas canvas = editor.getCanvasControl(); + SelectionManager manager = canvas.getSelectionManager(); + + NodeProxy primary = canvas.getNodeFactory().create(xmlProperty.getNode()); + if (primary != null) { + RenameResult result = manager.performRename(primary, null); + if (result.isCanceled()) { + return; + } else if (!result.isUnavailable()) { + String name = result.getName(); + String id = NEW_ID_PREFIX + BaseViewRule.stripIdPrefix(name); + xmlProperty.setValue(id); + return; + } + } + } + } + // When editing the id attribute, don't offer a resource chooser: usually // you want to enter a *new* id here attributeInfo = null; @@ -370,7 +468,7 @@ class XmlPropertyEditor extends AbstractTextPropertyEditor { // get the resource repository for this project and the system resources. ResourceRepository projectRepository = ResourceManager.getInstance().getProjectResources(project); - Shell shell = AdtPlugin.getDisplay().getActiveShell(); + Shell shell = AdtPlugin.getShell(); ReferenceChooserDialog dlg = new ReferenceChooserDialog( project, projectRepository, diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringAssistant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringAssistant.java index 51360e8..88423e4 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringAssistant.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringAssistant.java @@ -20,8 +20,13 @@ import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceProcessor; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceWizard; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceXmlTextAction; import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringRefactoring; import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringWizard; +import com.android.resources.ResourceType; +import com.android.utils.Pair; import org.eclipse.core.resources.IFile; import org.eclipse.jface.text.IDocument; @@ -36,6 +41,7 @@ import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.ltk.core.refactoring.Refactoring; +import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring; import org.eclipse.ltk.ui.refactoring.RefactoringWizard; import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation; import org.eclipse.swt.graphics.Image; @@ -100,6 +106,7 @@ public class RefactoringAssistant implements IQuickAssistProcessor { boolean isTagName = false; boolean isAttributeName = false; boolean isStylableAttribute = false; + Pair<ResourceType, String> resource = null; IStructuredModel model = null; try { model = xmlEditor.getModelForRead(); @@ -115,6 +122,7 @@ public class RefactoringAssistant implements IQuickAssistProcessor { isValue = true; if (value.startsWith("'@") || value.startsWith("\"@")) { //$NON-NLS-1$ //$NON-NLS-2$ isReferenceValue = true; + resource = RenameResourceXmlTextAction.findResource(doc, offset); } } else if (type.equals(DOMRegionContext.XML_TAG_NAME) || type.equals(DOMRegionContext.XML_TAG_OPEN) @@ -132,6 +140,8 @@ public class RefactoringAssistant implements IQuickAssistProcessor { // On the edge of an attribute name and an attribute value isAttributeName = true; isStylableAttribute = true; + } else if (type.equals(DOMRegionContext.XML_CONTENT)) { + resource = RenameResourceXmlTextAction.findResource(doc, offset); } } } finally { @@ -141,7 +151,7 @@ public class RefactoringAssistant implements IQuickAssistProcessor { } List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); - if (isTagName || isAttributeName || isValue) { + if (isTagName || isAttributeName || isValue || resource != null) { StructuredTextEditor structuredEditor = xmlEditor.getStructuredTextEditor(); ISelectionProvider provider = structuredEditor.getSelectionProvider(); ISelection selection = provider.getSelection(); @@ -173,6 +183,12 @@ public class RefactoringAssistant implements IQuickAssistProcessor { if (isValue && !isReferenceValue) { proposals.add(new RefactoringProposal(xmlEditor, new ExtractStringRefactoring(file, xmlEditor, textSelection))); + } else if (resource != null) { + RenameResourceProcessor processor = new RenameResourceProcessor( + file.getProject(), resource.getFirst(), + resource.getSecond(), null); + RenameRefactoring refactoring = new RenameRefactoring(processor); + proposals.add(new RefactoringProposal(xmlEditor, refactoring)); } LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(xmlEditor); @@ -275,6 +291,12 @@ public class RefactoringAssistant implements IQuickAssistProcessor { } else if (mRefactoring instanceof ExtractStringRefactoring) { wizard = new ExtractStringWizard((ExtractStringRefactoring) mRefactoring, mEditor.getProject()); + } else if (mRefactoring instanceof RenameRefactoring) { + RenameRefactoring refactoring = (RenameRefactoring) mRefactoring; + RenameResourceProcessor processor = + (RenameResourceProcessor) refactoring.getProcessor(); + ResourceType type = processor.getType(); + wizard = new RenameResourceWizard((RenameRefactoring) mRefactoring, type, false); } else { throw new IllegalArgumentException(); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AdtPrefs.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AdtPrefs.java index 33fb638..800828c 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AdtPrefs.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AdtPrefs.java @@ -75,6 +75,7 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { public final static String PREFS_PREVIEWS = AdtPlugin.PLUGIN_ID + ".previews"; //$NON-NLS-1$ public final static String PREFS_SKIP_LINT_LIBS = AdtPlugin.PLUGIN_ID + ".skipLintLibs"; //$NON-NLS-1$ public final static String PREFS_AUTO_PICK_TARGET = AdtPlugin.PLUGIN_ID + ".autoPickTarget"; //$NON-NLS-1$ + public final static String PREFS_REFACTOR_IDS = AdtPlugin.PLUGIN_ID + ".refactorIds"; //$NON-NLS-1$ /** singleton instance */ private final static AdtPrefs sThis = new AdtPrefs(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeMoveParticipant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeMoveParticipant.java index 56aca98..2186486 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeMoveParticipant.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeMoveParticipant.java @@ -24,6 +24,7 @@ import com.android.ide.eclipse.adt.internal.refactorings.changes.AndroidLayoutCh import com.android.ide.eclipse.adt.internal.refactorings.changes.AndroidLayoutChangeDescription; import com.android.ide.eclipse.adt.internal.refactorings.changes.AndroidLayoutFileChanges; import com.android.ide.eclipse.adt.internal.refactorings.changes.AndroidTypeMoveChange; +import com.android.resources.ResourceFolderType; import com.android.xml.AndroidManifest; import org.eclipse.core.filebuffers.FileBuffers; @@ -231,17 +232,26 @@ public class AndroidTypeMoveParticipant extends MoveParticipant { private void addLayoutChanges(IProject project, String className) { try { IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES); - IFolder layoutFolder = resFolder.getFolder(SdkConstants.FD_RES_LAYOUT); - IResource[] members = layoutFolder.members(); - for (int i = 0; i < members.length; i++) { - IResource member = members[i]; - if ((member instanceof IFile) && member.exists()) { - IFile file = (IFile) member; - Set<AndroidLayoutChangeDescription> changes = parse(file, className); - if (changes.size() > 0) { - AndroidLayoutFileChanges fileChange = new AndroidLayoutFileChanges(file); - fileChange.getChanges().addAll(changes); - mFileChanges.add(fileChange); + for (IResource folder : resFolder.members()) { + if (!(folder instanceof IFolder)) { + continue; + } + ResourceFolderType type = ResourceFolderType.getFolderType(folder.getName()); + if (type != ResourceFolderType.LAYOUT) { + continue; + } + IFolder layoutFolder = (IFolder) folder; + IResource[] members = layoutFolder.members(); + for (int i = 0; i < members.length; i++) { + IResource member = members[i]; + if ((member instanceof IFile) && member.exists()) { + IFile file = (IFile) member; + Set<AndroidLayoutChangeDescription> changes = parse(file, className); + if (changes.size() > 0) { + AndroidLayoutFileChanges fileChange = new AndroidLayoutFileChanges(file); + fileChange.getChanges().addAll(changes); + mFileChanges.add(fileChange); + } } } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeRenameParticipant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeRenameParticipant.java index 3a7f6db..227266d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeRenameParticipant.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeRenameParticipant.java @@ -24,6 +24,7 @@ import com.android.ide.eclipse.adt.internal.refactorings.changes.AndroidLayoutCh import com.android.ide.eclipse.adt.internal.refactorings.changes.AndroidLayoutChangeDescription; import com.android.ide.eclipse.adt.internal.refactorings.changes.AndroidLayoutFileChanges; import com.android.ide.eclipse.adt.internal.refactorings.changes.AndroidTypeRenameChange; +import com.android.resources.ResourceFolderType; import com.android.xml.AndroidManifest; import org.eclipse.core.filebuffers.FileBuffers; @@ -214,17 +215,26 @@ public class AndroidTypeRenameParticipant extends AndroidRenameParticipant { private void addLayoutChanges(IProject project, String className) { try { IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES); - IFolder layoutFolder = resFolder.getFolder(SdkConstants.FD_RES_LAYOUT); - IResource[] members = layoutFolder.members(); - for (int i = 0; i < members.length; i++) { - IResource member = members[i]; - if ((member instanceof IFile) && member.exists()) { - IFile file = (IFile) member; - Set<AndroidLayoutChangeDescription> changes = parse(file, className); - if (changes.size() > 0) { - AndroidLayoutFileChanges fileChange = new AndroidLayoutFileChanges(file); - fileChange.getChanges().addAll(changes); - mFileChanges.add(fileChange); + for (IResource folder : resFolder.members()) { + if (!(folder instanceof IFolder)) { + continue; + } + ResourceFolderType type = ResourceFolderType.getFolderType(folder.getName()); + if (type != ResourceFolderType.LAYOUT) { + continue; + } + IFolder layoutFolder = (IFolder) folder; + IResource[] members = layoutFolder.members(); + for (int i = 0; i < members.length; i++) { + IResource member = members[i]; + if ((member instanceof IFile) && member.exists()) { + IFile file = (IFile) member; + Set<AndroidLayoutChangeDescription> changes = parse(file, className); + if (changes.size() > 0) { + AndroidLayoutFileChanges fileChange = new AndroidLayoutFileChanges(file); + fileChange.getChanges().addAll(changes); + mFileChanges.add(fileChange); + } } } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourcePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourcePage.java new file mode 100644 index 0000000..6779fd3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourcePage.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.android.ide.eclipse.adt.internal.refactorings.core; + +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; +import static com.android.SdkConstants.R_CLASS; + +import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; +import com.android.resources.ResourceType; + +import org.eclipse.jdt.internal.ui.refactoring.TextInputWizardPage; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +import java.util.Set; + +@SuppressWarnings("restriction") // JDT refactoring UI +class RenameResourcePage extends TextInputWizardPage implements SelectionListener { + private Label mXmlLabel; + private Label mJavaLabel; + private Button mUpdateReferences; + private boolean mCanClear; + private ResourceType mType; + private ResourceNameValidator mValidator; + + /** + * Create the wizard. + * @param type the type of the resource to be renamed + * @param initial initial renamed value + * @param canClear whether the dialog should allow clearing the field + */ + public RenameResourcePage(ResourceType type, String initial, boolean canClear) { + super(type.getName(), true, initial); + mType = type; + mCanClear = canClear; + + mValidator = ResourceNameValidator.create(false /*allowXmlExtension*/, + (Set<String>) null, mType); + } + + @SuppressWarnings("unused") // SWT constructors aren't really unused, they have side effects + @Override + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NULL); + setControl(container); + initializeDialogUnits(container); + container.setLayout(new GridLayout(2, false)); + Label nameLabel = new Label(container, SWT.NONE); + nameLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + nameLabel.setText("New Name:"); + Text text = super.createTextInputField(container); + text.selectAll(); + text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + Label xmlLabel = new Label(container, SWT.NONE); + xmlLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + xmlLabel.setText("XML:"); + mXmlLabel = new Label(container, SWT.NONE); + mXmlLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + Label javaLabel = new Label(container, SWT.NONE); + javaLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + javaLabel.setText("Java:"); + mJavaLabel = new Label(container, SWT.NONE); + mJavaLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + new Label(container, SWT.NONE); + new Label(container, SWT.NONE); + mUpdateReferences = new Button(container, SWT.CHECK); + mUpdateReferences.setSelection(true); + mUpdateReferences.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1)); + mUpdateReferences.setText("Update References"); + mUpdateReferences.addSelectionListener(this); + + Dialog.applyDialogFont(container); + } + + @Override + public void setVisible(boolean visible) { + if (visible) { + RenameResourceProcessor processor = getProcessor(); + String newName = processor.getNewName(); + if (newName != null && newName.length() > 0 + && !newName.equals(getInitialValue())) { + Text textField = getTextField(); + textField.setText(newName); + textField.setSelection(0, newName.length()); + } + } + + super.setVisible(visible); + } + + @Override + protected RefactoringStatus validateTextField(String newName) { + if (newName.isEmpty() && isEmptyInputValid()) { + getProcessor().setNewName(""); + return RefactoringStatus.createWarningStatus( + "The resource definition will be deleted"); + } + + String error = mValidator.isValid(newName); + if (error != null) { + return RefactoringStatus.createErrorStatus(error); + } + + RenameResourceProcessor processor = getProcessor(); + processor.setNewName(newName); + return processor.checkNewName(newName); + } + + private RenameResourceProcessor getProcessor() { + RenameRefactoring refactoring = (RenameRefactoring) getRefactoring(); + return (RenameResourceProcessor) refactoring.getProcessor(); + } + + @Override + protected boolean isEmptyInputValid() { + return mCanClear; + } + + @Override + protected boolean isInitialInputValid() { + RenameResourceProcessor processor = getProcessor(); + return processor.getNewName() != null + && !processor.getNewName().equals(processor.getCurrentName()); + } + + @Override + protected void textModified(String text) { + super.textModified(text); + if (mXmlLabel != null && mJavaLabel != null) { + String xml = PREFIX_RESOURCE_REF + mType.getName() + '/' + text; + String java = R_CLASS + '.' + mType.getName() + '.' + text; + if (text.isEmpty()) { + xml = java = ""; + } + mXmlLabel.setText(xml); + mJavaLabel.setText(java); + } + } + + // ---- Implements SelectionListener ---- + + @Override + public void widgetSelected(SelectionEvent e) { + if (e.getSource() == mUpdateReferences) { + RenameResourceProcessor processor = getProcessor(); + boolean update = mUpdateReferences.getSelection(); + processor.setUpdateReferences(update); + } + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceParticipant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceParticipant.java new file mode 100644 index 0000000..2b0d5d7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceParticipant.java @@ -0,0 +1,746 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.android.ide.eclipse.adt.internal.refactorings.core; + +import static com.android.SdkConstants.ANDROID_PREFIX; +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_ID; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.ATTR_TYPE; +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.EXT_XML; +import static com.android.SdkConstants.FD_RES; +import static com.android.SdkConstants.FN_RESOURCE_CLASS; +import static com.android.SdkConstants.NEW_ID_PREFIX; +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; +import static com.android.SdkConstants.PREFIX_THEME_REF; +import static com.android.SdkConstants.R_CLASS; +import static com.android.SdkConstants.TAG_ITEM; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; +import com.android.utils.SdkUtils; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.jdt.core.IField; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.internal.corext.refactoring.rename.RenameFieldProcessor; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.ltk.core.refactoring.Change; +import org.eclipse.ltk.core.refactoring.CompositeChange; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.TextChange; +import org.eclipse.ltk.core.refactoring.TextFileChange; +import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext; +import org.eclipse.ltk.core.refactoring.participants.RenameParticipant; +import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring; +import org.eclipse.ltk.core.refactoring.resource.RenameResourceChange; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.ReplaceEdit; +import org.eclipse.text.edits.TextEdit; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; +import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * A rename participant handling renames of resources (such as R.id.foo and R.layout.bar). + * This reacts to refactorings of fields in the R inner classes (such as R.id), and updates + * the XML files as appropriate; renaming .xml files, updating XML attributes, resource + * references in style declarations, and so on. + */ +@SuppressWarnings("restriction") // WTP API +public class RenameResourceParticipant extends RenameParticipant { + /** The project we're refactoring in */ + private @NonNull IProject mProject; + + /** The type of the resource we're refactoring, such as {@link ResourceType#ID} */ + private @NonNull ResourceType mType; + /** + * The type of the resource folder we're refactoring in, such as + * {@link ResourceFolderType#VALUES}. When refactoring non value files, we need to + * rename the files as well. + */ + private @NonNull ResourceFolderType mFolderType; + + /** The previous name of the resource */ + private @NonNull String mOldName; + + /** The new name of the resource */ + private @NonNull String mNewName; + + /** Whether references to the resource should be updated */ + private boolean mUpdateReferences; + + /** A match pattern to look for in XML, such as {@code @attr/foo} */ + private @NonNull String mXmlMatch1; + + /** A match pattern to look for in XML, such as {@code ?attr/foo} */ + private @Nullable String mXmlMatch2; + + /** A match pattern to look for in XML, such as {@code ?foo} */ + private @Nullable String mXmlMatch3; + + /** The value to replace a reference to {@link #mXmlMatch1} with, such as {@code @attr/bar} */ + private @NonNull String mXmlNewValue1; + + /** The value to replace a reference to {@link #mXmlMatch2} with, such as {@code ?attr/bar} */ + private @Nullable String mXmlNewValue2; + + /** The value to replace a reference to {@link #mXmlMatch3} with, such as {@code ?bar} */ + private @Nullable String mXmlNewValue3; + + /** + * If non null, this refactoring was initiated as a file rename of an XML file (and if + * null, we are just reacting to a Java field rename) + */ + private IFile mRenamedFile; + + /** + * If renaming a field, we need to create an embedded field refactoring to update the + * Java sources referring to the corresponding R class field. This is stored as an + * instance such that we can have it participate in both the condition check methods + * as well as the {@link #createChange(IProgressMonitor)} refactoring operation. + */ + private RenameRefactoring mFieldRefactoring; + + /** + * Set while we are creating an embedded Java refactoring. This could cause a recursive + * invocation of the XML renaming refactoring to react to the field, so this is flag + * during the call to the Java processor, and is used to ignore requests for adding in + * field reactions during that time. + */ + private static boolean sIgnore; + + /** + * Creates a new {@linkplain RenameResourceParticipant} + */ + public RenameResourceParticipant() { + } + + @Override + public String getName() { + return "Android Rename Field Participant"; + } + + @Override + protected boolean initialize(Object element) { + if (sIgnore) { + return false; + } + + if (element instanceof IField) { + IField field = (IField) element; + IType declaringType = field.getDeclaringType(); + if (declaringType != null) { + if (R_CLASS.equals(declaringType.getParent().getElementName())) { + String typeName = declaringType.getElementName(); + mType = ResourceType.getEnum(typeName); + if (mType != null) { + mUpdateReferences = getArguments().getUpdateReferences(); + mFolderType = AdtUtils.getFolderTypeFor(mType); + IJavaProject javaProject = (IJavaProject) field.getAncestor( + IJavaElement.JAVA_PROJECT); + mProject = javaProject.getProject(); + mOldName = field.getElementName(); + mNewName = getArguments().getNewName(); + mFieldRefactoring = null; + mRenamedFile = null; + createXmlSearchPatterns(); + return true; + } + } + } + + return false; + } else if (element instanceof IFile) { + IFile file = (IFile) element; + mProject = file.getProject(); + if (BaseProjectHelper.isAndroidProject(mProject)) { + IPath path = file.getFullPath(); + int segments = path.segmentCount(); + if (segments == 4 && path.segment(1).equals(FD_RES)) { + String parentName = file.getParent().getName(); + mFolderType = ResourceFolderType.getFolderType(parentName); + if (mFolderType != null && mFolderType != ResourceFolderType.VALUES) { + mType = AdtUtils.getResourceTypeFor(mFolderType); + if (mType != null) { + mUpdateReferences = getArguments().getUpdateReferences(); + mProject = file.getProject(); + mOldName = AdtUtils.stripAllExtensions(file.getName()); + mNewName = AdtUtils.stripAllExtensions(getArguments().getNewName()); + mRenamedFile = file; + createXmlSearchPatterns(); + + mFieldRefactoring = null; + IField field = getResourceField(mProject, mType, mOldName); + if (field != null) { + mFieldRefactoring = createFieldRefactoring(field); + } else { + // no corresponding field; aapt has not run yet. Perhaps user has + // turned off auto build. + mFieldRefactoring = null; + } + + return true; + } + } + } + } + } else if (element instanceof String) { + String uri = (String) element; + if (uri.startsWith(PREFIX_RESOURCE_REF) && !uri.startsWith(ANDROID_PREFIX)) { + RenameResourceProcessor processor = (RenameResourceProcessor) getProcessor(); + mProject = processor.getProject(); + mType = processor.getType(); + mFolderType = AdtUtils.getFolderTypeFor(mType); + mOldName = processor.getCurrentName(); + mNewName = processor.getNewName(); + assert uri.endsWith(mOldName) && uri.contains(mType.getName()) : uri; + mUpdateReferences = getArguments().getUpdateReferences(); + if (mNewName.isEmpty()) { + mUpdateReferences = false; + } + mRenamedFile = null; + createXmlSearchPatterns(); + mFieldRefactoring = null; + if (!mNewName.isEmpty()) { + IField field = getResourceField(mProject, mType, mOldName); + if (field != null) { + mFieldRefactoring = createFieldRefactoring(field); + } + } + + return true; + } + } + + return false; + } + + /** Create nested Java refactoring which updates the R field references, if applicable */ + private RenameRefactoring createFieldRefactoring(IField field) { + RenameFieldProcessor processor = new RenameFieldProcessor(field); + processor.setRenameGetter(false); + processor.setRenameSetter(false); + RenameRefactoring refactoring = new RenameRefactoring(processor); + processor.setUpdateReferences(mUpdateReferences); + processor.setUpdateTextualMatches(false); + processor.setNewElementName(mNewName); + try { + if (refactoring.isApplicable()) { + return refactoring; + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return null; + } + + private void createXmlSearchPatterns() { + // Set up search strings for the attribute iterator. This will + // identify string matches for mXmlMatch1, 2 and 3, and when matched, + // will add a replacement edit for mXmlNewValue1, 2, or 3. + mXmlMatch2 = null; + mXmlNewValue2 = null; + mXmlMatch3 = null; + mXmlNewValue3 = null; + + String typeName = mType.getName(); + if (mUpdateReferences) { + mXmlMatch1 = PREFIX_RESOURCE_REF + typeName + '/' + mOldName; + mXmlNewValue1 = PREFIX_RESOURCE_REF + typeName + '/' + mNewName; + if (mType == ResourceType.ID) { + mXmlMatch2 = NEW_ID_PREFIX + mOldName; + mXmlNewValue2 = NEW_ID_PREFIX + mNewName; + } else if (mType == ResourceType.ATTR) { + // When renaming @attr/foo, also edit ?attr/foo + mXmlMatch2 = PREFIX_THEME_REF + typeName + '/' + mOldName; + mXmlNewValue2 = PREFIX_THEME_REF + typeName + '/' + mNewName; + // as well as ?foo + mXmlMatch3 = PREFIX_THEME_REF + mOldName; + mXmlNewValue3 = PREFIX_THEME_REF + mNewName; + } + } else if (mType == ResourceType.ID) { + mXmlMatch1 = NEW_ID_PREFIX + mOldName; + mXmlNewValue1 = NEW_ID_PREFIX + mNewName; + } + } + + @Override + public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context) + throws OperationCanceledException { + if (mRenamedFile != null && getArguments().getNewName().indexOf('.') == -1 + && mRenamedFile.getName().indexOf('.') != -1) { + return RefactoringStatus.createErrorStatus( + String.format("You must include the file extension (%1$s?)", + mRenamedFile.getName().substring(mRenamedFile.getName().indexOf('.')))); + } + + // Ensure that the new name is valid + if (mNewName != null && !mNewName.isEmpty()) { + ResourceNameValidator validator = ResourceNameValidator.create(false, mProject, mType); + String error = validator.isValid(mNewName); + if (error != null) { + return RefactoringStatus.createErrorStatus(error); + } + } + + if (mFieldRefactoring != null) { + try { + sIgnore = true; + return mFieldRefactoring.checkAllConditions(pm); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } finally { + sIgnore = false; + } + } + + return new RefactoringStatus(); + } + + @Override + public Change createChange(IProgressMonitor monitor) throws CoreException, + OperationCanceledException { + if (monitor.isCanceled()) { + return null; + } + + CompositeChange result = new CompositeChange("Update resource references"); + + // Only show the children in the refactoring preview dialog + result.markAsSynthetic(); + + addResourceFileChanges(result, mProject, monitor); + + // If renaming resources in a library project, also offer to rename references + // in including projects + if (mUpdateReferences) { + ProjectState projectState = Sdk.getProjectState(mProject); + if (projectState != null && projectState.isLibrary()) { + List<ProjectState> parentProjects = projectState.getParentProjects(); + for (ProjectState state : parentProjects) { + IProject project = state.getProject(); + CompositeChange nested = new CompositeChange( + String.format("Update references in %1$s", project.getName())); + addResourceFileChanges(nested, project, monitor); + if (nested.getChildren().length > 0) { + result.add(nested); + } + } + } + } + + if (mFieldRefactoring != null) { + // We have to add in Java field refactoring + try { + sIgnore = true; + addJavaChanges(result, monitor); + } finally { + sIgnore = false; + } + } else { + // Disable field refactoring added by the default Java field rename handler + disableExistingResourceFileChange(); + } + + return (result.getChildren().length == 0) ? null : result; + } + + /** + * Adds all changes to resource files (typically XML but also renaming drawable files + * + * @param project the Android project + * @param className the layout classes + */ + private void addResourceFileChanges( + CompositeChange change, + IProject project, + IProgressMonitor monitor) + throws OperationCanceledException { + if (monitor.isCanceled()) { + return; + } + + try { + // Update resource references in the manifest + IFile manifest = project.getFile(SdkConstants.ANDROID_MANIFEST_XML); + if (manifest != null) { + addResourceXmlChanges(manifest, change, null); + } + + // Update references in XML resource files + IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES); + + IResource[] folders = resFolder.members(); + for (IResource folder : folders) { + if (!(folder instanceof IFolder)) { + continue; + } + String folderName = folder.getName(); + ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName); + IResource[] files = ((IFolder) folder).members(); + for (int i = 0; i < files.length; i++) { + IResource member = files[i]; + if ((member instanceof IFile) && member.exists()) { + IFile file = (IFile) member; + String fileName = member.getName(); + + if (SdkUtils.endsWith(fileName, DOT_XML)) { + addResourceXmlChanges(file, change, folderType); + } + + if ((mRenamedFile == null || !mRenamedFile.equals(file)) + && fileName.startsWith(mOldName) + && fileName.length() > mOldName.length() + && fileName.charAt(mOldName.length()) == '.' + && mFolderType != ResourceFolderType.VALUES + && mFolderType == folderType) { + // Rename this file + String newFile = mNewName + fileName.substring(mOldName.length()); + IPath path = file.getFullPath(); + change.add(new RenameResourceChange(path, newFile)); + } + } + } + } + } catch (CoreException e) { + RefactoringUtil.log(e); + } + } + + private void addJavaChanges(CompositeChange result, IProgressMonitor monitor) + throws CoreException, OperationCanceledException { + if (monitor.isCanceled()) { + return; + } + + RefactoringStatus status = mFieldRefactoring.checkAllConditions(monitor); + if (status != null && !status.hasError()) { + Change fieldChanges = mFieldRefactoring.createChange(monitor); + if (fieldChanges != null) { + result.add(fieldChanges); + + // Look for the field change on the R.java class; it's a derived file + // and will generate file modified manually warnings. Disable it. + disableResourceFileChange(fieldChanges); + } + } + } + + private boolean addResourceXmlChanges( + IFile file, + CompositeChange changes, + ResourceFolderType folderType) { + IModelManager modelManager = StructuredModelManager.getModelManager(); + IStructuredModel model = null; + try { + model = modelManager.getExistingModelForRead(file); + if (model == null) { + model = modelManager.getModelForRead(file); + } + if (model != null) { + IStructuredDocument document = model.getStructuredDocument(); + if (model instanceof IDOMModel) { + IDOMModel domModel = (IDOMModel) model; + Element root = domModel.getDocument().getDocumentElement(); + if (root != null) { + List<TextEdit> edits = new ArrayList<TextEdit>(); + addReplacements(edits, root, document, folderType); + if (!edits.isEmpty()) { + MultiTextEdit rootEdit = new MultiTextEdit(); + rootEdit.addChildren(edits.toArray(new TextEdit[edits.size()])); + TextFileChange change = new TextFileChange(file.getName(), file); + change.setTextType(EXT_XML); + change.setEdit(rootEdit); + changes.add(change); + } + } + } else { + return false; + } + } + + return true; + } catch (IOException e) { + AdtPlugin.log(e, null); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } finally { + if (model != null) { + model.releaseFromRead(); + } + } + + return false; + } + + private int getAttributeValueRangeStart(Attr attr, IDocument document) { + IndexedRegion region = (IndexedRegion) attr; + int potentialStart = attr.getName().length() + 2; // + 2: add =" + String text; + try { + text = document.get(region.getStartOffset(), region.getLength()); + } catch (BadLocationException e) { + return -1; + } + String value = attr.getValue(); + int index = text.indexOf(value, potentialStart); + if (index != -1) { + return region.getStartOffset() + index; + } else { + return -1; + } + } + + private void addReplacements( + @NonNull List<TextEdit> edits, + @NonNull Element element, + @NonNull IStructuredDocument document, + @Nullable ResourceFolderType folderType) { + String tag = element.getTagName(); + if (folderType == ResourceFolderType.VALUES) { + // Look for + // <item name="main_layout" type="layout">...</item> + // <item name="myid" type="id"/> + // <string name="mystring">...</string> + // etc + if (tag.equals(mType.getName()) + || (tag.equals(TAG_ITEM) + && (mType == ResourceType.ID + || mType.getName().equals(element.getAttribute(ATTR_TYPE))))) { + Attr nameNode = element.getAttributeNode(ATTR_NAME); + if (nameNode != null && nameNode.getValue().equals(mOldName)) { + int start = getAttributeValueRangeStart(nameNode, document); + if (start != -1) { + int end = start + mOldName.length(); + edits.add(new ReplaceEdit(start, end - start, mNewName)); + } + } + } + } + + NamedNodeMap attributes = element.getAttributes(); + for (int i = 0, n = attributes.getLength(); i < n; i++) { + Attr attr = (Attr) attributes.item(i); + String value = attr.getValue(); + + // If not updating references, only update XML matches that define the id + if (!mUpdateReferences && (!ATTR_ID.equals(attr.getLocalName()) || + !ANDROID_URI.equals(attr.getNamespaceURI()))) { + continue; + } + + // Replace XML attribute reference, such as + // android:id="@+id/oldName" => android:id="+id/newName" + + String match = null; + String matchedValue = null; + + if (value.equals(mXmlMatch1)) { + match = mXmlMatch1; + matchedValue = mXmlNewValue1; + } else if (value.equals(mXmlMatch2)) { + match = mXmlMatch2; + matchedValue = mXmlNewValue2; + } else if (value.equals(mXmlMatch3)) { + match = mXmlMatch3; + matchedValue = mXmlNewValue3; + } else { + continue; + } + + if (match != null) { + if (mNewName.isEmpty() && ATTR_ID.equals(attr.getLocalName()) && + ANDROID_URI.equals(attr.getNamespaceURI())) { + // Delete attribute + IndexedRegion region = (IndexedRegion) attr; + int start = region.getStartOffset(); + int end = region.getEndOffset(); + edits.add(new ReplaceEdit(start, end - start, "")); + } else { + int start = getAttributeValueRangeStart(attr, document); + if (start != -1) { + int end = start + match.length(); + edits.add(new ReplaceEdit(start, end - start, matchedValue)); + } + } + } + } + + NodeList children = element.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + addReplacements(edits, (Element) child, document, folderType); + } else if (child.getNodeType() == Node.TEXT_NODE && mUpdateReferences) { + // Replace XML text, such as @color/custom_theme_color in + // <item name="android:windowBackground">@color/custom_theme_color</item> + // + String text = child.getNodeValue(); + int index = getFirstNonBlankIndex(text); + if (index != -1) { + String match = null; + String matchedValue = null; + if (mXmlMatch1 != null + && text.startsWith(mXmlMatch1) && text.trim().equals(mXmlMatch1)) { + match = mXmlMatch1; + matchedValue = mXmlNewValue1; + } else if (mXmlMatch2 != null + && text.startsWith(mXmlMatch2) && text.trim().equals(mXmlMatch2)) { + match = mXmlMatch2; + matchedValue = mXmlNewValue2; + } else if (mXmlMatch3 != null + && text.startsWith(mXmlMatch3) && text.trim().equals(mXmlMatch3)) { + match = mXmlMatch3; + matchedValue = mXmlNewValue3; + } + if (match != null) { + IndexedRegion region = (IndexedRegion) child; + int start = region.getStartOffset() + index; + int end = start + match.length(); + edits.add(new ReplaceEdit(start, end - start, matchedValue)); + } + } + } + } + } + + /** + * Returns the index of the first non-space character in the string, or -1 + * if the string is empty or has only whitespace + * + * @param s the string to check + * @return the index of the first non whitespace character + */ + private int getFirstNonBlankIndex(String s) { + for (int i = 0, n = s.length(); i < n; i++) { + if (!Character.isWhitespace(s.charAt(i))) { + return i; + } + } + + return -1; + } + + /** + * Initiates a renaming of a resource item + * + * @param project the project containing the resource references + * @param type the type of resource + * @param name the name of the resource + * @return false if initiating the rename failed + */ + @Nullable + private static IField getResourceField( + @NonNull IProject project, + @NonNull ResourceType type, + @NonNull String name) { + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + if (javaProject == null) { + return null; + } + + String pkg = ManifestInfo.get(project).getPackage(); + IType t = javaProject.findType(pkg + '.' + R_CLASS + '.' + type.getName()); + if (t == null) { + return null; + } + + return t.getField(name); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return null; + } + + /** + * Searches for existing changes in the refactoring which modifies the R + * field to rename it. it's derived so performing this change will generate + * a "generated code was modified manually" warning + */ + private void disableExistingResourceFileChange() { + IFolder genFolder = mProject.getFolder(SdkConstants.FD_GEN_SOURCES); + if (genFolder != null && genFolder.exists()) { + ManifestInfo manifestInfo = ManifestInfo.get(mProject); + String pkg = manifestInfo.getPackage(); + if (pkg != null) { + IFile rFile = genFolder.getFile(pkg.replace('.', '/') + '/' + FN_RESOURCE_CLASS); + TextChange change = getTextChange(rFile); + if (change != null) { + change.setEnabled(false); + } + } + } + } + + /** + * Searches for existing changes in the refactoring which modifies the R + * field to rename it. it's derived so performing this change will generate + * a "generated code was modified manually" warning + */ + private void disableResourceFileChange(Change change) { + // Look for the field change on the R.java class; it's a derived file + // and will generate file modified manually warnings. Disable it. + if (change instanceof CompositeChange) { + for (Change outer : ((CompositeChange) change).getChildren()) { + if (outer instanceof CompositeChange) { + for (Change inner : ((CompositeChange) outer).getChildren()) { + if (FN_RESOURCE_CLASS.equals(inner.getName())) { + inner.setEnabled(false); + } + } + } + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceProcessor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceProcessor.java new file mode 100644 index 0000000..5ea9941 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceProcessor.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.android.ide.eclipse.adt.internal.refactorings.core; + +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; +import com.android.resources.ResourceType; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.ltk.core.refactoring.Change; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext; +import org.eclipse.ltk.core.refactoring.participants.ParticipantManager; +import org.eclipse.ltk.core.refactoring.participants.RefactoringParticipant; +import org.eclipse.ltk.core.refactoring.participants.RenameArguments; +import org.eclipse.ltk.core.refactoring.participants.RenameProcessor; +import org.eclipse.ltk.core.refactoring.participants.SharableParticipants; + +/** + * A rename processor for Android resources. + */ +public class RenameResourceProcessor extends RenameProcessor { + private IProject mProject; + private ResourceType mType; + private String mCurrentName; + private String mNewName; + private boolean mUpdateReferences = true; + private ResourceNameValidator mValidator; + private RenameArguments mRenameArguments; + + /** + * Creates a new rename resource processor. + * + * @param project the project containing the renamed resource + * @param type the type of the resource + * @param currentName the current name of the resource + * @param newName the new name of the resource, or null if not known + */ + public RenameResourceProcessor( + @NonNull IProject project, + @NonNull ResourceType type, + @NonNull String currentName, + @Nullable String newName) { + mProject = project; + mType = type; + mCurrentName = currentName; + mNewName = newName != null ? newName : currentName; + mUpdateReferences= true; + mValidator = ResourceNameValidator.create(false, mProject, mType); + } + + /** + * Returns the project containing the renamed resource + * + * @return the project containing the renamed resource + */ + @NonNull + public IProject getProject() { + return mProject; + } + + /** + * Returns the new resource name + * + * @return the new resource name + */ + @NonNull + public String getNewName() { + return mNewName; + } + + /** + * Returns the current name of the resource + * + * @return the current name of the resource + */ + public String getCurrentName() { + return mCurrentName; + } + + /** + * Returns the type of the resource + * + * @return the type of the resource + */ + @NonNull + public ResourceType getType() { + return mType; + } + + /** + * Sets the new name + * + * @param newName the new name + */ + public void setNewName(@NonNull String newName) { + mNewName = newName; + } + + /** + * Returns {@code true} if the refactoring processor also updates references + * + * @return {@code true} if the refactoring processor also updates references + */ + public boolean isUpdateReferences() { + return mUpdateReferences; + } + + /** + * Specifies if the refactoring processor also updates references. The + * default behavior is to update references. + * + * @param updateReferences {@code true} if the refactoring processor should + * also updates references + */ + public void setUpdateReferences(boolean updateReferences) { + mUpdateReferences = updateReferences; + } + + /** + * Checks the given new potential name and returns a {@link RefactoringStatus} indicating + * whether the potential new name is valid + * + * @param name the name to check + * @return a {@link RefactoringStatus} with the validation result + */ + public RefactoringStatus checkNewName(String name) { + String error = mValidator.isValid(name); + if (error != null) { + return RefactoringStatus.createFatalErrorStatus(error); + } + + return new RefactoringStatus(); + } + + @Override + public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException { + return new RefactoringStatus(); + } + + @Override + public RefactoringStatus checkFinalConditions(IProgressMonitor pm, + CheckConditionsContext context) throws CoreException { + pm.beginTask("", 1); + try { + mRenameArguments = new RenameArguments(getNewName(), isUpdateReferences()); + return new RefactoringStatus(); + } finally { + pm.done(); + } + } + + @Override + public Change createChange(IProgressMonitor pm) throws CoreException { + pm.beginTask("", 1); + try { + // Added by {@link RenameResourceParticipant} + return null; + } finally { + pm.done(); + } + } + + @Override + public Object[] getElements() { + return new Object[0]; + } + + @Override + public String getIdentifier() { + return "com.android.ide.renameResourceProcessor"; //$NON-NLS-1$ + } + + @Override + public String getProcessorName() { + return "Rename Android Resource"; + } + + @Override + public boolean isApplicable() { + return true; + } + + @Override + public RefactoringParticipant[] loadParticipants(RefactoringStatus status, + SharableParticipants shared) throws CoreException { + String[] affectedNatures = new String[] { AdtConstants.NATURE_DEFAULT }; + String url = PREFIX_RESOURCE_REF + mType.getName() + '/' + mCurrentName; + return ParticipantManager.loadRenameParticipants(status, this, url, mRenameArguments, + null, affectedNatures, shared); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceWizard.java new file mode 100644 index 0000000..6ffe25d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceWizard.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.android.ide.eclipse.adt.internal.refactorings.core; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.resources.ResourceType; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.internal.ui.IJavaHelpContextIds; +import org.eclipse.jdt.internal.ui.JavaPluginImages; +import org.eclipse.jdt.internal.ui.refactoring.reorg.RenameRefactoringWizard; +import org.eclipse.jdt.ui.refactoring.RefactoringSaveHelper; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring; +import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation; +import org.eclipse.swt.widgets.Shell; + +/** + * Rename refactoring wizard for Android resources such as {@code @id/foo} + */ +@SuppressWarnings("restriction") // JDT refactoring UI +public class RenameResourceWizard extends RenameRefactoringWizard { + private ResourceType mType; + private boolean mCanClear; + + /** + * Constructs a new {@linkplain RenameResourceWizard} + * + * @param refactoring the refactoring + * @param type the type of resource being renamed + * @param canClear whether the user can clear the value + */ + public RenameResourceWizard( + @NonNull RenameRefactoring refactoring, + @NonNull ResourceType type, + boolean canClear) { + super(refactoring, + "Rename Resource", + "Enter the new name for this resource", + JavaPluginImages.DESC_WIZBAN_REFACTOR_FIELD, + IJavaHelpContextIds.RENAME_FIELD_WIZARD_PAGE); + mType = type; + mCanClear = canClear; + } + + @Override + protected void addUserInputPages() { + RenameRefactoring refactoring = (RenameRefactoring) getRefactoring(); + RenameResourceProcessor processor = (RenameResourceProcessor) refactoring.getProcessor(); + String name = processor.getNewName(); + addPage(new RenameResourcePage(mType, name, mCanClear)); + } + + /** + * Initiates a renaming of a resource item + * + * @param shell the shell to parent the dialog to + * @param project the project containing the resource references + * @param type the type of resource + * @param currentName the name of the resource + * @param newName the new name, or null if not known + * @param canClear whether the name is allowed to be cleared + * @return false if initiating the rename failed + */ + public static RenameResult renameResource( + @NonNull Shell shell, + @NonNull IProject project, + @NonNull ResourceType type, + @NonNull String currentName, + @Nullable String newName, + boolean canClear) { + try { + RenameResourceProcessor processor = new RenameResourceProcessor(project, type, + currentName, newName); + RenameRefactoring refactoring = new RenameRefactoring(processor); + if (!refactoring.isApplicable()) { + return RenameResult.unavailable(); + } + + if (!show(refactoring, processor, shell, type, canClear)) { + return RenameResult.canceled(); + } + return RenameResult.name(processor.getNewName()); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return RenameResult.unavailable(); + } + + /** + * Show a refactoring dialog for the given resource refactoring operation + * + * @param refactoring the rename refactoring + * @param processor the field processor + * @param parent the parent shell + * @param type the resource type + * @param canClear whether the user is allowed to clear/reset the name to + * nothing + * @return true if the refactoring was performed, and false if it was + * canceled + * @throws CoreException if an unexpected error occurs + */ + private static boolean show( + @NonNull RenameRefactoring refactoring, + @NonNull RenameResourceProcessor processor, + @NonNull Shell parent, + @NonNull ResourceType type, + boolean canClear) throws CoreException { + RefactoringSaveHelper saveHelper = new RefactoringSaveHelper( + RefactoringSaveHelper.SAVE_REFACTORING); + if (!saveHelper.saveEditors(parent)) { + return false; + } + + try { + RenameResourceWizard wizard = new RenameResourceWizard(refactoring, type, canClear); + RefactoringWizardOpenOperation operation = new RefactoringWizardOpenOperation(wizard); + String dialogTitle = wizard.getDefaultPageTitle(); + int result = operation.run(parent, dialogTitle == null ? "" : dialogTitle); + RefactoringStatus status = operation.getInitialConditionCheckingStatus(); + if (status.hasFatalError()) { + return false; + } + if (result == RefactoringWizardOpenOperation.INITIAL_CONDITION_CHECKING_FAILED + || result == IDialogConstants.CANCEL_ID) { + saveHelper.triggerIncrementalBuild(); + return false; + } + + // Save modified resources; need to trigger R file regeneration + saveHelper.saveEditors(parent); + + return true; + } catch (InterruptedException e) { + return false; // Canceled + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceXmlTextAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceXmlTextAction.java new file mode 100644 index 0000000..8d52114 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceXmlTextAction.java @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.android.ide.eclipse.adt.internal.refactorings.core; + +import static com.android.SdkConstants.ANDROID_PREFIX; +import static com.android.SdkConstants.ANDROID_THEME_PREFIX; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.ATTR_TYPE; +import static com.android.SdkConstants.TAG_ITEM; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.Hyperlinks; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.resources.ResourceType; +import com.android.utils.Pair; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextSelection; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionProvider; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.texteditor.IDocumentProvider; +import org.eclipse.ui.texteditor.ITextEditor; +import org.eclipse.ui.texteditor.ITextEditorExtension; +import org.eclipse.ui.texteditor.ITextEditorExtension2; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * Text action for XML files to invoke resource renaming + * <p> + * TODO: Handle other types of renaming: invoking class renaming when editing + * class names in layout files and manifest files, renaming attribute names when + * editing a styleable attribute, etc. + */ +public final class RenameResourceXmlTextAction extends Action { + private final ITextEditor mEditor; + + /** + * Creates a new {@linkplain RenameResourceXmlTextAction} + * + * @param editor the associated editor + */ + public RenameResourceXmlTextAction(@NonNull ITextEditor editor) { + super("Rename"); + mEditor = editor; + } + + @Override + public void run() { + if (!validateEditorInputState()) { + return; + } + IDocument document = getDocument(); + if (document == null) { + return; + } + ITextSelection selection = getSelection(); + if (selection == null) { + return; + } + + Pair<ResourceType, String> resource = findResource(document, selection.getOffset()); + + if (resource == null) { + resource = findItemDefinition(document, selection.getOffset()); + } + + if (resource != null) { + ResourceType type = resource.getFirst(); + String name = resource.getSecond(); + Shell shell = mEditor.getSite().getShell(); + boolean canClear = false; + + IEditorInput input = mEditor.getEditorInput(); + if (input instanceof IFileEditorInput) { + IFileEditorInput fileInput = (IFileEditorInput) input; + IProject project = fileInput.getFile().getProject(); + RenameResourceWizard.renameResource(shell, project, type, name, null, canClear); + return; + } + } + + // Fallback: tell user the cursor isn't in the right place + MessageDialog.openInformation(mEditor.getSite().getShell(), + "Rename", + "Operation unavailable on the current selection.\n" + + "Select an Android resource name."); + } + + private boolean validateEditorInputState() { + if (mEditor instanceof ITextEditorExtension2) + return ((ITextEditorExtension2) mEditor).validateEditorInputState(); + else if (mEditor instanceof ITextEditorExtension) + return !((ITextEditorExtension) mEditor).isEditorInputReadOnly(); + else if (mEditor != null) + return mEditor.isEditable(); + else + return false; + } + + /** + * Searches for a resource URL around the caret, such as {@code @string/foo} + * + * @param document the document to search in + * @param offset the offset to search at + * @return a resource pair, or null if not found + */ + @Nullable + public static Pair<ResourceType,String> findResource(@NonNull IDocument document, int offset) { + try { + int max = document.getLength(); + if (offset >= max) { + offset = max - 1; + } else if (offset < 0) { + offset = 0; + } else if (offset > 0) { + // If the caret is right after a resource name (meaning getChar(offset) points + // to the following character), back up + char c = document.getChar(offset); + if (!isValidResourceNameChar(c)) { + offset--; + } + } + + int start = offset; + boolean valid = true; + for (; start >= 0; start--) { + char c = document.getChar(start); + if (c == '@' || c == '?') { + break; + } else if (!isValidResourceNameChar(c)) { + valid = false; + break; + } + } + if (valid) { + // Search forwards for the end + int end = start + 1; + for (; end < max; end++) { + char c = document.getChar(end); + if (!isValidResourceNameChar(c)) { + break; + } + } + if (end > start + 1) { + String url = document.get(start, end - start); + + // Don't allow renaming framework resources -- @android:string/ok etc + if (url.startsWith(ANDROID_PREFIX) || url.startsWith(ANDROID_THEME_PREFIX)) { + return null; + } + + return Hyperlinks.parseResource(url); + } + } + } catch (BadLocationException e) { + AdtPlugin.log(e, null); + } + + return null; + } + + private static boolean isValidResourceNameChar(char c) { + return c == '@' || c == '?' || c == '/' || c == '+' || Character.isJavaIdentifierPart(c); + } + + /** + * Searches for an item definition around the caret, such as + * {@code <string name="foo">My String</string>} + */ + private Pair<ResourceType, String> findItemDefinition(IDocument document, int offset) { + Node node = DomUtilities.getNode(document, offset); + if (node == null) { + return null; + } + if (node.getNodeType() == Node.TEXT_NODE) { + node = node.getParentNode(); + } + if (node == null || node.getNodeType() != Node.ELEMENT_NODE) { + return null; + } + + Element element = (Element) node; + String name = element.getAttribute(ATTR_NAME); + if (name == null || name.isEmpty()) { + return null; + } + String typeString = element.getTagName(); + if (TAG_ITEM.equals(typeString)) { + typeString = element.getAttribute(ATTR_TYPE); + if (typeString == null || typeString.isEmpty()) { + return null; + } + } + ResourceType type = ResourceType.getEnum(typeString); + if (type != null) { + return Pair.of(type, name); + } + + return null; + } + + private ITextSelection getSelection() { + ISelectionProvider selectionProvider = mEditor.getSelectionProvider(); + if (selectionProvider == null) { + return null; + } + ISelection selection = selectionProvider.getSelection(); + if (!(selection instanceof ITextSelection)) { + return null; + } + return (ITextSelection) selection; + } + + private IDocument getDocument() { + IDocumentProvider documentProvider = mEditor.getDocumentProvider(); + if (documentProvider == null) { + return null; + } + IDocument document = documentProvider.getDocument(mEditor.getEditorInput()); + if (document == null) { + return null; + } + return document; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResult.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResult.java new file mode 100644 index 0000000..ade346f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResult.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.android.ide.eclipse.adt.internal.refactorings.core; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; + +/** + * A result from a renaming operation + */ +public class RenameResult { + private boolean mCanceled; + private boolean mUnavailable; + private @Nullable String mName; + private boolean mClear; + + /** + * Constructs a new rename result + */ + private RenameResult() { + } + + /** + * Creates a new blank {@linkplain RenameResult} + * @return a new result + */ + @NonNull + public static RenameResult create() { + return new RenameResult(); + } + + /** + * Creates a new {@linkplain RenameResult} for a user canceled renaming operation + * @return a canceled operation + */ + @NonNull + public static RenameResult canceled() { + return new RenameResult().setCanceled(true); + } + + /** + * Creates a {@linkplain RenameResult} for a renaming operation that was + * not available (for example because the field attempted to be renamed + * does not yet exist (or does not exist any more) + * + * @return a new result + */ + @NonNull + public static RenameResult unavailable() { + return new RenameResult().setUnavailable(true); + } + + /** + * Creates a new {@linkplain RenameResult} for a successful renaming + * operation to the given name + * + * @param name the new name + * @return a new result + */ + @NonNull + public static RenameResult name(@Nullable String name) { + return new RenameResult().setName(name); + } + + /** + * Marks this result as canceled + * + * @param canceled whether the result was canceled + * @return this, for constructor chaining + */ + @NonNull + public RenameResult setCanceled(boolean canceled) { + mCanceled = canceled; + return this; + } + + /** + * Marks this result as unavailable + * + * @param unavailable whether this result was unavailable + * @return this, for constructor chaining + */ + @NonNull + public RenameResult setUnavailable(boolean unavailable) { + mUnavailable = unavailable; + return this; + } + + /** + * Sets the new name of the renaming operation + * + * @param name the new name + * @return this, for constructor chaining + */ + @NonNull + public RenameResult setName(@Nullable String name) { + mName = name; + return this; + } + + /** + * Marks this result as clearing the name (reverting it back to the default) + * + * @param clear whether the name was cleared + * @return this, for constructor chaining + */ + @NonNull + public RenameResult setCleared(boolean clear) { + mClear = clear; + return this; + } + + /** + * Returns whether this result represents a canceled renaming operation + * + * @return true if the operation was canceled + */ + public boolean isCanceled() { + return mCanceled; + } + + /** + * Returns whether this result represents an unavailable renaming operation + * + * @return true if the operation was not available + */ + public boolean isUnavailable() { + return mUnavailable; + } + + /** + * Returns whether this result represents a renaming back to the default (possibly + * clear) name. In this case, {@link #getName()} will return {@code null}. + * + * @return true if the name should be reset + */ + public boolean isCleared() { + return mClear; + } + + /** + * Returns the new name. + * + * @return the new name + */ + @Nullable + public String getName() { + return mName; + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringAssistantTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringAssistantTest.java index 48dc6ae..b227179 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringAssistantTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringAssistantTest.java @@ -48,8 +48,7 @@ public class RefactoringAssistantTest extends AdtProjectTest { } public void testAssistant4() throws Exception { - // Negative test: ensure that we don't offer extract string on a value that is - // already a resource (should list all but extract string) + // Check for resource rename refactoring (and don't offer extract string) checkFixes("sample1a.xml", "android:id=\"@+id/Linea^rLayout2\""); } diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a-expected-assistant4.txt b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a-expected-assistant4.txt index be08a13..30bb00b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a-expected-assistant4.txt +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/sample1a-expected-assistant4.txt @@ -1,2 +1,3 @@ Quick assistant in sample1a.xml for android:id="@+id/Linea^rLayout2": +Rename Android Resource : Initiates the "Rename Android Resource" refactoring Extract Style : Initiates the "Extract Style" refactoring diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceParticipantTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceParticipantTest.java new file mode 100644 index 0000000..a5d2a9a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceParticipantTest.java @@ -0,0 +1,789 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.android.ide.eclipse.adt.internal.refactorings.core; + +import com.android.annotations.NonNull; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.Hyperlinks; +import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.AdtProjectTest; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.resources.ResourceType; +import com.android.utils.Pair; +import com.google.common.base.Charsets; +import com.google.common.base.Splitter; +import com.google.common.io.ByteStreams; +import com.google.common.io.Files; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.IField; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.internal.corext.refactoring.rename.RenameFieldProcessor; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; +import org.eclipse.ltk.core.refactoring.Change; +import org.eclipse.ltk.core.refactoring.CompositeChange; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.TextFileChange; +import org.eclipse.ltk.core.refactoring.participants.RenameProcessor; +import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring; +import org.eclipse.ltk.core.refactoring.resource.RenameResourceChange; +import org.eclipse.text.edits.TextEdit; + +import java.io.File; +import java.io.IOException; + +@SuppressWarnings({"javadoc", "restriction"}) +public class RenameResourceParticipantTest extends AdtProjectTest { + public void testRefactor1() throws Exception { + checkRefactoring( + TEST_PROJECT, + "@string/app_name", + true /*updateReferences*/, + "myname", + + "CHANGES:\n" + + "-------\n" + + "* AndroidManifest.xml - /testRefactor1/AndroidManifest.xml\n" + + " < android:label=\"@string/app_name\"\n" + + " < android:theme=\"@style/AppTheme\" >\n" + + " < <activity\n" + + " < android:name=\"com.example.refactoringtest.MainActivity\"\n" + + " < android:label=\"@string/app_name\" >\n" + + " ---\n" + + " > android:label=\"@string/myname\"\n" + + " > android:theme=\"@style/AppTheme\" >\n" + + " > <activity\n" + + " > android:name=\"com.example.refactoringtest.MainActivity\"\n" + + " > android:label=\"@string/myname\" >\n" + + "\n" + + "\n" + + "* strings.xml - /testRefactor1/res/values/strings.xml\n" + + " < <string name=\"app_name\">RefactoringTest</string>\n" + + " ---\n" + + " > <string name=\"myname\">RefactoringTest</string>\n" + + "\n" + + "\n" + + "* R.java - /testRefactor1/gen/com/example/refactoringtest/R.java\n" + + " < public static final int app_name=0x7f040000;\n" + + " ---\n" + + " > public static final int myname=0x7f040000;"); + } + + public void testRefactor2() throws Exception { + checkRefactoring( + TEST_PROJECT, + "@+id/menu_settings", + true /*updateReferences*/, + "new_id_for_the_action_bar", + + "CHANGES:\n" + + "-------\n" + + "* activity_main.xml - /testRefactor2/res/menu/activity_main.xml\n" + + " < android:id=\"@+id/menu_settings\"\n" + + " ---\n" + + " > android:id=\"@+id/new_id_for_the_action_bar\"\n" + + "\n" + + "\n" + + "* R.java - /testRefactor2/gen/com/example/refactoringtest/R.java\n" + + " < public static final int menu_settings=0x7f070003;\n" + + " ---\n" + + " > public static final int new_id_for_the_action_bar=0x7f070003;"); + } + + public void testRefactor3() throws Exception { + checkRefactoring( + TEST_PROJECT, + "@+id/textView1", + true /*updateReferences*/, + "output", + + "CHANGES:\n" + + "-------\n" + + "* activity_main.xml - /testRefactor3/res/layout/activity_main.xml\n" + + " < android:id=\"@+id/textView1\"\n" + + " < android:layout_width=\"wrap_content\"\n" + + " < android:layout_height=\"wrap_content\"\n" + + " < android:layout_centerVertical=\"true\"\n" + + " < android:layout_toRightOf=\"@+id/button2\"\n" + + " < android:text=\"@string/hello_world\" />\n" + + " <\n" + + " < <Button\n" + + " < android:id=\"@+id/button1\"\n" + + " < android:layout_width=\"wrap_content\"\n" + + " < android:layout_height=\"wrap_content\"\n" + + " < android:layout_alignLeft=\"@+id/textView1\"\n" + + " < android:layout_below=\"@+id/textView1\"\n" + + " ---\n" + + " > android:id=\"@+id/output\"\n" + + " > android:layout_width=\"wrap_content\"\n" + + " > android:layout_height=\"wrap_content\"\n" + + " > android:layout_centerVertical=\"true\"\n" + + " > android:layout_toRightOf=\"@+id/button2\"\n" + + " > android:text=\"@string/hello_world\" />\n" + + " >\n" + + " > <Button\n" + + " > android:id=\"@+id/button1\"\n" + + " > android:layout_width=\"wrap_content\"\n" + + " > android:layout_height=\"wrap_content\"\n" + + " > android:layout_alignLeft=\"@+id/output\"\n" + + " > android:layout_below=\"@+id/output\"\n" + + "\n" + + "\n" + + "* MainActivity.java - /testRefactor3/src/com/example/refactoringtest/MainActivity.java\n" + + " < View view1 = findViewById(R.id.textView1);\n" + + " ---\n" + + " > View view1 = findViewById(R.id.output);\n" + + "\n" + + "\n" + + "* R.java - /testRefactor3/gen/com/example/refactoringtest/R.java\n" + + " < public static final int textView1=0x7f070000;\n" + + " ---\n" + + " > public static final int output=0x7f070000;"); + } + + public void testRefactor4() throws Exception { + checkRefactoring( + TEST_PROJECT, + // same as testRefactor3, but use @id rather than @+id even though @+id is in file + "@id/textView1", + true /*updateReferences*/, + "output", + + "CHANGES:\n" + + "-------\n" + + "* activity_main.xml - /testRefactor4/res/layout/activity_main.xml\n" + + " < android:id=\"@+id/textView1\"\n" + + " < android:layout_width=\"wrap_content\"\n" + + " < android:layout_height=\"wrap_content\"\n" + + " < android:layout_centerVertical=\"true\"\n" + + " < android:layout_toRightOf=\"@+id/button2\"\n" + + " < android:text=\"@string/hello_world\" />\n" + + " <\n" + + " < <Button\n" + + " < android:id=\"@+id/button1\"\n" + + " < android:layout_width=\"wrap_content\"\n" + + " < android:layout_height=\"wrap_content\"\n" + + " < android:layout_alignLeft=\"@+id/textView1\"\n" + + " < android:layout_below=\"@+id/textView1\"\n" + + " ---\n" + + " > android:id=\"@+id/output\"\n" + + " > android:layout_width=\"wrap_content\"\n" + + " > android:layout_height=\"wrap_content\"\n" + + " > android:layout_centerVertical=\"true\"\n" + + " > android:layout_toRightOf=\"@+id/button2\"\n" + + " > android:text=\"@string/hello_world\" />\n" + + " >\n" + + " > <Button\n" + + " > android:id=\"@+id/button1\"\n" + + " > android:layout_width=\"wrap_content\"\n" + + " > android:layout_height=\"wrap_content\"\n" + + " > android:layout_alignLeft=\"@+id/output\"\n" + + " > android:layout_below=\"@+id/output\"\n" + + "\n" + + "\n" + + "* MainActivity.java - /testRefactor4/src/com/example/refactoringtest/MainActivity.java\n" + + " < View view1 = findViewById(R.id.textView1);\n" + + " ---\n" + + " > View view1 = findViewById(R.id.output);\n" + + "\n" + + "\n" + + "* R.java - /testRefactor4/gen/com/example/refactoringtest/R.java\n" + + " < public static final int textView1=0x7f070000;\n" + + " ---\n" + + " > public static final int output=0x7f070000;"); + } + + public void testRefactor5() throws Exception { + checkRefactoring( + TEST_PROJECT, + "@layout/activity_main", + true /*updateReferences*/, + "newlayout", + + "CHANGES:\n" + + "-------\n" + + "* Rename 'testRefactor5/res/layout/activity_main.xml' to 'newlayout.xml'\n" + + "* Rename 'testRefactor5/res/layout-land/activity_main.xml' to 'newlayout.xml'\n" + + "* MainActivity.java - /testRefactor5/src/com/example/refactoringtest/MainActivity.java\n" + + " < setContentView(R.layout.activity_main);\n" + + " ---\n" + + " > setContentView(R.layout.newlayout);\n" + + "\n" + + "\n" + + "* R.java - /testRefactor5/gen/com/example/refactoringtest/R.java\n" + + " < public static final int activity_main=0x7f030000;\n" + + " ---\n" + + " > public static final int newlayout=0x7f030000;"); + } + + public void testRefactor6() throws Exception { + checkRefactoring( + TEST_PROJECT, + "@drawable/ic_launcher", + true /*updateReferences*/, + "newlauncher", + + "CHANGES:\n" + + "-------\n" + + "* AndroidManifest.xml - /testRefactor6/AndroidManifest.xml\n" + + " < android:icon=\"@drawable/ic_launcher\"\n" + + " ---\n" + + " > android:icon=\"@drawable/newlauncher\"\n" + + "\n" + + "\n" + + "* Rename 'testRefactor6/res/drawable-hdpi/ic_launcher.png' to 'newlauncher.png'\n" + + "* Rename 'testRefactor6/res/drawable-ldpi/ic_launcher.png' to 'newlauncher.png'\n" + + "* Rename 'testRefactor6/res/drawable-mdpi/ic_launcher.png' to 'newlauncher.png'\n" + + "* Rename 'testRefactor6/res/drawable-xhdpi/ic_launcher.png' to 'newlauncher.png'\n" + + "* R.java - /testRefactor6/gen/com/example/refactoringtest/R.java\n" + + " < public static final int ic_launcher=0x7f020000;\n" + + " ---\n" + + " > public static final int newlauncher=0x7f020000;"); + } + + public void testRefactor7() throws Exception { + // Test refactoring initiated on a file rename + IProject project = createProject(TEST_PROJECT); + IFile file = project.getFile("res/layout/activity_main.xml"); + checkRefactoring( + project, + file, + true /*updateReferences*/, + "newlayout", + + "CHANGES:\n" + + "-------\n" + + "* Rename 'testRefactor7/res/layout/activity_main.xml' to 'newlayout.xml'\n" + + "* Rename 'testRefactor7/res/layout-land/activity_main.xml' to 'newlayout.xml'\n" + + "* MainActivity.java - /testRefactor7/src/com/example/refactoringtest/MainActivity.java\n" + + " < setContentView(R.layout.activity_main);\n" + + " ---\n" + + " > setContentView(R.layout.newlayout);\n" + + "\n" + + "\n" + + "* R.java - /testRefactor7/gen/com/example/refactoringtest/R.java\n" + + " < public static final int activity_main=0x7f030000;\n" + + " ---\n" + + " > public static final int newlayout=0x7f030000;"); + } + + public void testRefactor8() throws Exception { + // Test refactoring initiated on a Java field rename + IProject project = createProject(TEST_PROJECT); + IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + assertNotNull(javaProject); + IType type = javaProject.findType("com.example.refactoringtest.R.layout"); + if (type == null || !type.exists()) { + type = javaProject.findType("com.example.refactoringtest.R$layout"); + System.out.println("Had to switch to $ notation"); + } + assertNotNull(type); + assertTrue(type.exists()); + IField field = type.getField("activity_main"); + assertNotNull(field); + assertTrue(field.exists()); + + checkRefactoring( + project, + field, + true /*updateReferences*/, + "newlauncher", + + "CHANGES:\n" + + "-------\n" + + "* MainActivity.java - /testRefactor8/src/com/example/refactoringtest/MainActivity.java\n" + + " < setContentView(R.layout.activity_main);\n" + + " ---\n" + + " > setContentView(R.layout.newlauncher);\n" + + "\n" + + "\n" + + "* R.java - /testRefactor8/gen/com/example/refactoringtest/R.java\n" + + " < public static final int activity_main=0x7f030000;\n" + + " ---\n" + + " > public static final int newlauncher=0x7f030000;\n" + + "\n" + + "\n" + + "* Rename 'testRefactor8/res/layout/activity_main.xml' to 'newlauncher.xml'\n" + + "* Rename 'testRefactor8/res/layout-land/activity_main.xml' to 'newlauncher.xml'"); + } + + public void testInvalidName() throws Exception { + checkRefactoring( + TEST_PROJECT, + "@drawable/ic_launcher", + true /*updateReferences*/, + "Newlauncher", + + "<ERROR\n" + + "\t\n" + + "ERROR: File-based resource names must start with a lowercase letter.\n" + + "Context: <Unspecified context>\n" + + "code: none\n" + + "Data: null\n" + + ">"); + } + + public void testRefactor9() throws Exception { + // same as testRefactor4, but not updating references + checkRefactoring( + TEST_PROJECT, + "@id/textView1", + false /*updateReferences*/, + "output", + + "CHANGES:\n" + + "-------\n" + + "* activity_main.xml - /testRefactor9/res/layout/activity_main.xml\n" + + " < android:id=\"@+id/textView1\"\n" + + " ---\n" + + " > android:id=\"@+id/output\"\n" + + "\n" + + "\n" + + "* R.java - /testRefactor9/gen/com/example/refactoringtest/R.java\n" + + " < public static final int textView1=0x7f070000;\n" + + " ---\n" + + " > public static final int output=0x7f070000;"); + } + + // ---- Only test infrastructure below ---- + + private void checkRefactoring( + @NonNull Object[] testData, + @NonNull Object resource, + boolean updateReferences, + @NonNull String newName, + @NonNull String expected) throws Exception { + IProject project = createProject(testData); + checkRefactoring(project, resource, updateReferences, newName, expected); + } + + private void checkRefactoring( + @NonNull IProject project, + @NonNull Object resource, + boolean updateReferences, + @NonNull String newName, + @NonNull String expected) throws Exception { + RenameProcessor processor = null; + if (resource instanceof String) { + String url = (String) resource; + assert url.startsWith("@") : resource; + Pair<ResourceType, String> pair = Hyperlinks.parseResource(url); + assertNotNull(url, pair); + ResourceType type = pair.getFirst(); + String currentName = pair.getSecond(); + RenameResourceProcessor p; + p = new RenameResourceProcessor(project, type, currentName, newName); + p.setUpdateReferences(updateReferences); + processor = p; + } else if (resource instanceof IResource) { + IResource r = (IResource) resource; + org.eclipse.ltk.internal.core.refactoring.resource.RenameResourceProcessor p; + p = new org.eclipse.ltk.internal.core.refactoring.resource.RenameResourceProcessor(r); + String fileName = r.getName(); + int dot = fileName.indexOf('.'); + String extension = (dot != -1) ? fileName.substring(dot) : ""; + p.setNewResourceName(newName + extension); + p.setUpdateReferences(updateReferences); + processor = p; + } else if (resource instanceof IField) { + RenameFieldProcessor p = new RenameFieldProcessor((IField) resource); + p.setNewElementName(newName); + p.setUpdateReferences(updateReferences); + processor = p; + } else { + fail("Unsupported resource element in tests: " + resource); + } + + assertNotNull(processor); + + RenameRefactoring refactoring = new RenameRefactoring(processor); + RefactoringStatus status = refactoring.checkAllConditions(new NullProgressMonitor()); + assertNotNull(status); + if (!status.isOK()) { + assertEquals(status.toString(), expected); + return; + } + assertTrue(status.toString(), status.isOK()); + Change change = refactoring.createChange(new NullProgressMonitor()); + assertNotNull(change); + String explanation = "CHANGES:\n-------\n" + describe(change); + if (!expected.trim().equals(explanation.trim())) { // allow trimming endlines in expected + assertEquals(expected, explanation); + } + } + + private IProject createProject(Object[] testData) throws Exception { + String name = getName(); + IProject project = createProject(name); + File projectDir = AdtUtils.getAbsolutePath(project).toFile(); + assertNotNull(projectDir); + assertTrue(projectDir.getPath(), projectDir.exists()); + createTestDataDir(projectDir, testData); + project.refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor()); + + for (int i = 0; i < testData.length; i+= 2) { + assertTrue(testData[i].toString(), testData[i] instanceof String); + String relative = (String) testData[i]; + IResource member = project.findMember(relative); + assertNotNull(relative, member); + assertTrue(member.getClass().getSimpleName(), member instanceof IFile); + } + + return project; + } + + private String describe(Change change) throws Exception { + StringBuilder sb = new StringBuilder(1000); + describe(sb, change, 0); + + // Trim trailing space + for (int i = sb.length() - 1; i >= 0; i--) { + if (!Character.isWhitespace(sb.charAt(i))) { + sb.setLength(i + 1); + break; + } + } + + return sb.toString(); + } + + private void describe(StringBuilder sb, Change change, int indent) throws Exception { + if (change instanceof CompositeChange + && ((CompositeChange) change).isSynthetic()) { + // Don't display information about synthetic changes + } else { + // Describe this change + indent(sb, indent); + sb.append("* "); + sb.append(change.getName()); + if (change instanceof TextFileChange) { + TextFileChange tfc = (TextFileChange) change; + sb.append(" - "); + sb.append(tfc.getFile().getFullPath()); + sb.append('\n'); + } + if (change instanceof TextFileChange) { + TextFileChange tfc = (TextFileChange) change; + TextEdit edit = tfc.getEdit(); + IFile file = tfc.getFile(); + byte[] bytes = ByteStreams.toByteArray(file.getContents()); + String before = new String(bytes, Charsets.UTF_8); + IDocument document = new Document(); + document.replace(0, 0, before); + edit.apply(document); + String after = document.get(); + String diff = getDiff(before, after); + for (String line : Splitter.on('\n').split(diff)) { + if (!line.trim().isEmpty()) { + indent(sb, indent + 1); + sb.append(line); + } + sb.append('\n'); + } + } else if (change instanceof RenameResourceChange) { + // Change name, appended above, is adequate + } else { + indent(sb, indent); + sb.append("<unknown change type " + change.getClass().getName() + ">"); + } + sb.append('\n'); + } + + if (change instanceof CompositeChange) { + CompositeChange composite = (CompositeChange) change; + Change[] children = composite.getChildren(); + for (Change child : children) { + describe(sb, child, indent + (composite.isSynthetic() ? 0 : 1)); + } + } + } + + private static void indent(StringBuilder sb, int indent) { + for (int i = 0; i < indent; i++) { + sb.append(" "); + } + } + + private void createTestDataDir(File dir, Object[] data) throws IOException { + for (int i = 0, n = data.length; i < n; i += 2) { + assertTrue("Must be a path: " + data[i], data[i] instanceof String); + String relativePath = ((String) data[i]).replace('/', File.separatorChar); + File to = new File(dir, relativePath); + File parent = to.getParentFile(); + if (!parent.exists()) { + boolean mkdirs = parent.mkdirs(); + assertTrue(to.getPath(), mkdirs); + } + + Object o = data[i + 1]; + if (o instanceof String) { + String contents = (String) o; + Files.write(contents, to, Charsets.UTF_8); + } else if (o instanceof byte[]) { + Files.write((byte[]) o, to); + } else { + fail("Data must be a String or a byte[] for " + to); + } + } + } + + // Test sources + + private static final String SAMPLE_MANIFEST = + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " package=\"com.example.refactoringtest\"\n" + + " android:versionCode=\"1\"\n" + + " android:versionName=\"1.0\" >\n" + + "\n" + + " <uses-sdk\n" + + " android:minSdkVersion=\"8\"\n" + + " android:targetSdkVersion=\"17\" />\n" + + "\n" + + " <application\n" + + " android:icon=\"@drawable/ic_launcher\"\n" + + " android:label=\"@string/app_name\"\n" + + " android:theme=\"@style/AppTheme\" >\n" + + " <activity\n" + + " android:name=\"com.example.refactoringtest.MainActivity\"\n" + + " android:label=\"@string/app_name\" >\n" + + " <intent-filter>\n" + + " <action android:name=\"android.intent.action.MAIN\" />\n" + + "\n" + + " <category android:name=\"android.intent.category.LAUNCHER\" />\n" + + " </intent-filter>\n" + + " </activity>\n" + + " </application>\n" + + "\n" + + "</manifest>"; + + private static final String SAMPLE_MAIN_ACTIVITY = + "package com.example.refactoringtest;\n" + + "\n" + + "import android.os.Bundle;\n" + + "import android.app.Activity;\n" + + "import android.view.Menu;\n" + + "import android.view.View;\n" + + "\n" + + "public class MainActivity extends Activity {\n" + + "\n" + + " @Override\n" + + " protected void onCreate(Bundle savedInstanceState) {\n" + + " super.onCreate(savedInstanceState);\n" + + " setContentView(R.layout.activity_main);\n" + + " View view1 = findViewById(R.id.textView1);\n" + + " }\n" + + "\n" + + " @Override\n" + + " public boolean onCreateOptionsMenu(Menu menu) {\n" + + " // Inflate the menu; this adds items to the action bar if it is present.\n" + + " getMenuInflater().inflate(R.menu.activity_main, menu);\n" + + " return true;\n" + + " }\n" + + "\n" + + "}\n"; + + private static final String SAMPLE_LAYOUT = + "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " xmlns:tools=\"http://schemas.android.com/tools\"\n" + + " android:layout_width=\"match_parent\"\n" + + " android:layout_height=\"match_parent\"\n" + + " tools:context=\".MainActivity\" >\n" + + "\n" + + " <TextView\n" + + " android:id=\"@+id/textView1\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_centerVertical=\"true\"\n" + + " android:layout_toRightOf=\"@+id/button2\"\n" + + " android:text=\"@string/hello_world\" />\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button1\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignLeft=\"@+id/textView1\"\n" + + " android:layout_below=\"@+id/textView1\"\n" + + " android:layout_marginLeft=\"22dp\"\n" + + " android:layout_marginTop=\"24dp\"\n" + + " android:text=\"Button\" />\n" + + "\n" + + " <Button\n" + + " android:id=\"@+id/button2\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_alignParentLeft=\"true\"\n" + + " android:layout_alignParentTop=\"true\"\n" + + " android:text=\"Button\" />\n" + + "\n" + + "</RelativeLayout>"; + + private static final String SAMPLE_LAYOUT_2 = + "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " xmlns:tools=\"http://schemas.android.com/tools\"\n" + + " android:layout_width=\"match_parent\"\n" + + " android:layout_height=\"match_parent\"\n" + + " tools:context=\".MainActivity\" >\n" + + "\n" + + "\n" + + "</RelativeLayout>"; + + + private static final String SAMPLE_MENU = + "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\" >\n" + + "\n" + + " <item\n" + + " android:id=\"@+id/menu_settings\"\n" + + " android:orderInCategory=\"100\"\n" + + " android:showAsAction=\"never\"\n" + + " android:title=\"@string/menu_settings\"/>\n" + + "\n" + + "</menu>"; + + private static final String SAMPLE_STRINGS = + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + + "<resources>\n" + + "\n" + + " <string name=\"app_name\">RefactoringTest</string>\n" + + " <string name=\"hello_world\">Hello world!</string>\n" + + " <string name=\"menu_settings\">Settings</string>\n" + + "\n" + + "</resources>"; + + private static final String SAMPLE_STYLES = + "<resources>\n" + + "\n" + + " <!--\n" + + " Base application theme, dependent on API level. This theme is replaced\n" + + " by AppBaseTheme from res/values-vXX/styles.xml on newer devices.\n" + + " -->\n" + + " <style name=\"AppBaseTheme\" parent=\"android:Theme.Light\">\n" + + " <!--\n" + + " Theme customizations available in newer API levels can go in\n" + + " res/values-vXX/styles.xml, while customizations related to\n" + + " backward-compatibility can go here.\n" + + " -->\n" + + " </style>\n" + + "\n" + + " <!-- Application theme. -->\n" + + " <style name=\"AppTheme\" parent=\"AppBaseTheme\">\n" + + " <!-- All customizations that are NOT specific to a particular API-level can go here. -->\n" + + " </style>\n" + + "\n" + + "</resources>"; + + private static final String SAMPLE_R = + "/* AUTO-GENERATED FILE. DO NOT MODIFY.\n" + + " *\n" + + " * This class was automatically generated by the\n" + + " * aapt tool from the resource data it found. It\n" + + " * should not be modified by hand.\n" + + " */\n" + + "\n" + + "package com.example.refactoringtest;\n" + + "\n" + + "public final class R {\n" + + " public static final class attr {\n" + + " }\n" + + " public static final class drawable {\n" + + " public static final int ic_launcher=0x7f020000;\n" + + " }\n" + + " public static final class id {\n" + + " public static final int button1=0x7f070002;\n" + + " public static final int button2=0x7f070001;\n" + + " public static final int menu_settings=0x7f070003;\n" + + " public static final int textView1=0x7f070000;\n" + + " }\n" + + " public static final class layout {\n" + + " public static final int activity_main=0x7f030000;\n" + + " }\n" + + " public static final class menu {\n" + + " public static final int activity_main=0x7f060000;\n" + + " }\n" + + " public static final class string {\n" + + " public static final int app_name=0x7f040000;\n" + + " public static final int hello_world=0x7f040001;\n" + + " public static final int menu_settings=0x7f040002;\n" + + " }\n" + + " public static final class style {\n" + + " /** \n" + + " Base application theme, dependent on API level. This theme is replaced\n" + + " by AppBaseTheme from res/values-vXX/styles.xml on newer devices.\n" + + " \n" + + "\n" + + " Theme customizations available in newer API levels can go in\n" + + " res/values-vXX/styles.xml, while customizations related to\n" + + " backward-compatibility can go here.\n" + + " \n" + + "\n" + + " Base application theme for API 11+. This theme completely replaces\n" + + " AppBaseTheme from res/values/styles.xml on API 11+ devices.\n" + + " \n" + + " API 11 theme customizations can go here. \n" + + "\n" + + " Base application theme for API 14+. This theme completely replaces\n" + + " AppBaseTheme from BOTH res/values/styles.xml and\n" + + " res/values-v11/styles.xml on API 14+ devices.\n" + + " \n" + + " API 14 theme customizations can go here. \n" + + " */\n" + + " public static final int AppBaseTheme=0x7f050000;\n" + + " /** Application theme. \n" + + " All customizations that are NOT specific to a particular API-level can go here. \n" + + " */\n" + + " public static final int AppTheme=0x7f050001;\n" + + " }\n" + + "}\n"; + + private static final Object[] TEST_PROJECT = new Object[] { + "AndroidManifest.xml", + SAMPLE_MANIFEST, + + "src/com/example/refactoringtest/MainActivity.java", + SAMPLE_MAIN_ACTIVITY, + + "gen/com/example/refactoringtest/R.java", + SAMPLE_R, + + "res/drawable-xhdpi/ic_launcher.png", + new byte[] { 0 }, + "res/drawable-hdpi/ic_launcher.png", + new byte[] { 0 }, + "res/drawable-ldpi/ic_launcher.png", + new byte[] { 0 }, + "res/drawable-mdpi/ic_launcher.png", + new byte[] { 0 }, + + "res/layout/activity_main.xml", + SAMPLE_LAYOUT, + + "res/layout-land/activity_main.xml", + SAMPLE_LAYOUT_2, + + "res/menu/activity_main.xml", + SAMPLE_MENU, + + "res/values/strings.xml", // file 3 + SAMPLE_STRINGS, + + "res/values/styles.xml", // file 3 + SAMPLE_STYLES, + }; +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java index f006ba9..d0d2cd2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/common/layout/LayoutTestBase.java @@ -353,6 +353,12 @@ public class LayoutTestBase extends TestCase { fail("Not supported in tests yet"); return null; } + + @Override + public boolean rename(INode node) { + fail("Not supported in tests yet"); + return false; + } } public void testDummy() { diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceXmlTextActionTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceXmlTextActionTest.java new file mode 100644 index 0000000..047168d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceXmlTextActionTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.android.ide.eclipse.adt.internal.refactorings.core; + +import com.android.resources.ResourceType; +import com.android.utils.Pair; + +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; + +import junit.framework.TestCase; + +@SuppressWarnings("javadoc") +public class RenameResourceXmlTextActionTest extends TestCase { + public void test_Simple() throws Exception { + checkWord("^foo", null); + checkWord("'foo'^", null); + checkWord("^@bogus", null); + checkWord("@bo^gus", null); + checkWord("bogus@^", null); + checkWord(" @string/nam^e ", Pair.of(ResourceType.STRING, "name")); + checkWord("@string/nam^e ", Pair.of(ResourceType.STRING, "name")); + checkWord("\"^@string/name ", Pair.of(ResourceType.STRING, "name")); + checkWord("^@string/name ", Pair.of(ResourceType.STRING, "name")); + checkWord("\n^@string/name ", Pair.of(ResourceType.STRING, "name")); + checkWord("\n^@string/name(", Pair.of(ResourceType.STRING, "name")); + checkWord("\n^@string/name;", Pair.of(ResourceType.STRING, "name")); + checkWord("\n^@string/name5", Pair.of(ResourceType.STRING, "name5")); + checkWord("\n@string/name5^", Pair.of(ResourceType.STRING, "name5")); + checkWord("\n@string/name5^(", Pair.of(ResourceType.STRING, "name5")); + checkWord("\n@stri^ng/name5(", Pair.of(ResourceType.STRING, "name5")); + checkWord("\n@string^/name5(", Pair.of(ResourceType.STRING, "name5")); + checkWord("\n@string/^name5(", Pair.of(ResourceType.STRING, "name5")); + checkWord("\n@string^name5(", null); + checkWord("\n@strings^/name5(", null); + checkWord("\n@+id/^myid(", Pair.of(ResourceType.ID, "myid")); + checkWord("\n?a^ttr/foo\"", Pair.of(ResourceType.ATTR, "foo")); + checkWord("\n?f^oo\"", Pair.of(ResourceType.ATTR, "foo")); + checkWord("\n^?foo\"", Pair.of(ResourceType.ATTR, "foo")); + } + + private void checkWord(String contents, Pair<ResourceType, String> expectedResource) + throws Exception { + int cursor = contents.indexOf('^'); + assertTrue("Must set cursor position with ^ in " + contents, cursor != -1); + contents = contents.substring(0, cursor) + contents.substring(cursor + 1); + assertEquals(-1, contents.indexOf('^')); + assertEquals(-1, contents.indexOf('[')); + assertEquals(-1, contents.indexOf(']')); + + IDocument document = new Document(); + document.replace(0, 0, contents); + Pair<ResourceType, String> resource = + RenameResourceXmlTextAction.findResource(document, cursor); + assertEquals(expectedResource, resource); + } +} |