aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRaphael <raphael@google.com>2009-07-10 19:49:31 -0400
committerRaphael <raphael@google.com>2009-07-11 15:22:37 -0400
commit6331632428c680ffce4656bd245d5674ac03e0ff (patch)
tree397ee7c8b637cb0ef13f78fd37ffd5d0e425d85d
parent7ed0334797902bcac80baa7032f93b0f9e79cb14 (diff)
downloadsdk-6331632428c680ffce4656bd245d5674ac03e0ff.zip
sdk-6331632428c680ffce4656bd245d5674ac03e0ff.tar.gz
sdk-6331632428c680ffce4656bd245d5674ac03e0ff.tar.bz2
ADT: Extract String IDs from Layout XML strings.
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml6
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java6
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidEditor.java123
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ReferenceAttributeDescriptor.java20
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringAction.java64
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java650
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/FolderConfiguration.java2
-rwxr-xr-xeclipse/scripts/create_all_symlinks.sh9
8 files changed, 689 insertions, 191 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
index dc2c612..76eefe6 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
@@ -666,6 +666,12 @@
id="com.android.ide.eclipse.adt.refactoring.extract.string"
name="Extract Android String">
</command>
+ <keyBinding
+ commandId="com.android.ide.eclipse.adt.refactoring.extract.string"
+ contextId="org.eclipse.ui.globalScope"
+ keyConfigurationId="org.eclipse.ui.defaultAcceleratorConfiguration"
+ keySequence="M3+M2+A S">
+ </keyBinding>
</extension>
<extension
point="org.eclipse.ltk.core.refactoring.refactoringContributions">
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java
index 725c6a8..75d33b3 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java
@@ -310,12 +310,12 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor {
Object[] choices = null;
if (attrInfo.isInValue) {
// Editing an attribute's value... Get the attribute name and then the
- // possible choice for the tuple(parent,attribute)
+ // possible choices for the tuple(parent,attribute)
String value = attrInfo.value;
if (value.startsWith("'") || value.startsWith("\"")) { //$NON-NLS-1$ //$NON-NLS-2$
value = value.substring(1);
// The prefix that was found at the beginning only scan for characters
- // valid of tag name. We now know the real prefix for this attribute's
+ // valid for tag name. We now know the real prefix for this attribute's
// value, which is needed to generate the completion choices below.
attrInfo.correctedPrefix = value;
} else {
@@ -772,7 +772,7 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor {
IDescriptorProvider descriptorProvider = data.getDescriptorProvider(mDescriptorId);
if (descriptorProvider != null) {
- mRootDescriptor = new ElementDescriptor("",
+ mRootDescriptor = new ElementDescriptor("", //$NON-NLS-1$
descriptorProvider.getRootElementDescriptors());
}
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidEditor.java
index 94d7d48..336b46a 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidEditor.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidEditor.java
@@ -79,7 +79,7 @@ import java.net.URL;
* source editor. This can be a no-op if desired.
*/
public abstract class AndroidEditor extends FormEditor implements IResourceChangeListener {
-
+
/** Preference name for the current page of this file */
private static final String PREF_CURRENT_PAGE = "_current_page";
@@ -91,7 +91,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
/** Width hint for text fields. Helps the grid layout resize properly on smaller screens */
public static final int TEXT_WIDTH_HINT = 50;
-
+
/** Page index of the text editor (always the last page) */
private int mTextPageIndex;
/** The text editor */
@@ -108,7 +108,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
public AndroidEditor() {
super();
ResourcesPlugin.getWorkspace().addResourceChangeListener(this);
-
+
mTargetListener = new ITargetChangeListener() {
public void onProjectTargetChange(IProject changedProject) {
if (changedProject == getProject()) {
@@ -118,7 +118,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
public void onTargetsLoaded() {
commitPages(false /* onSave */);
-
+
// recreate the ui root node always
initUiRootNode(true /*force*/);
}
@@ -133,14 +133,14 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
* UI node editor.
*/
abstract public UiElementNode getUiRootNode();
-
+
/**
* Creates the various form pages.
* <p/>
* Derived classes must implement this to add their own specific tabs.
*/
abstract protected void createFormPages();
-
+
/**
* Creates the initial UI Root Node, including the known mandatory elements.
* @param force if true, a new UiManifestNode is recreated even if it already exists.
@@ -150,9 +150,9 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
/**
* Subclasses should override this method to process the new XML Model, which XML
* root node is given.
- *
+ *
* The base implementation is empty.
- *
+ *
* @param xml_doc The XML document, if available, or null if none exists.
*/
protected void xmlModelChanged(Document xml_doc) {
@@ -169,7 +169,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
createAndroidPages();
selectDefaultPage(null /* defaultPageId */);
}
-
+
/**
* Creates the page for the Android Editors
*/
@@ -193,7 +193,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
action = mTextEditor.getAction(ActionFactory.REDO.getId());
bars.setGlobalActionHandler(ActionFactory.REDO.getId(), action);
-
+
bars.updateActionBars();
}
}
@@ -207,7 +207,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
if (defaultPageId == null) {
if (getEditorInput() instanceof IFileEditorInput) {
IFile file = ((IFileEditorInput) getEditorInput()).getFile();
-
+
QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
getClass().getSimpleName() + PREF_CURRENT_PAGE);
String pageId;
@@ -234,7 +234,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
}
}
}
-
+
/**
* Removes all the pages from the editor.
*/
@@ -247,10 +247,10 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
/**
* Overrides the parent's setActivePage to be able to switch to the xml editor.
- *
+ *
* If the special pageId TEXT_EDITOR_ID is given, switches to the mTextPageIndex page.
* This is needed because the editor doesn't actually derive from IFormPage and thus
- * doesn't have the get-by-page-id method. In this case, the method returns null since
+ * doesn't have the get-by-page-id method. In this case, the method returns null since
* IEditorPart does not implement IFormPage.
*/
@Override
@@ -262,18 +262,18 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
return super.setActivePage(pageId);
}
}
-
-
+
+
/**
* Notifies this multi-page editor that the page with the given id has been
* activated. This method is called when the user selects a different tab.
- *
+ *
* @see MultiPageEditorPart#pageChange(int)
*/
@Override
protected void pageChange(int newPageIndex) {
super.pageChange(newPageIndex);
-
+
if (getEditorInput() instanceof IFileEditorInput) {
IFile file = ((IFileEditorInput) getEditorInput()).getFile();
@@ -288,9 +288,9 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
}
/**
- * Notifies this listener that some resource changes
+ * Notifies this listener that some resource changes
* are happening, or have already happened.
- *
+ *
* Closes all project files on project close.
* @see IResourceChangeListener
*/
@@ -318,7 +318,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
* Initializes the editor part with a site and input.
* <p/>
* Checks that the input is an instance of {@link IFileEditorInput}.
- *
+ *
* @see FormEditor
*/
@Override
@@ -330,7 +330,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
/**
* Removes attached listeners.
- *
+ *
* @see WorkbenchPart
*/
@Override
@@ -341,7 +341,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
if (mXmlModelStateListener != null) {
xml_model.removeModelStateListener(mXmlModelStateListener);
}
-
+
} finally {
xml_model.releaseFromRead();
}
@@ -355,7 +355,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
super.dispose();
}
-
+
/**
* Commit all dirty pages then saves the contents of the text editor.
* <p/>
@@ -403,7 +403,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
* The incorrect casting makes the original implementation crash due
* to our {@link StructuredTextEditor} not being an {@link IFormPage}
* so we have to override and duplicate to fix it.
- *
+ *
* @param onSave <code>true</code> if commit is performed as part
* of the 'save' operation, <code>false</code> otherwise.
* @since 3.3
@@ -421,7 +421,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
}
}
}
- }
+ }
}
/* (non-Javadoc)
@@ -451,8 +451,8 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
* <li> Links starting with "file:/" are simply sent to a local browser.
* <li> Links starting with "page:" are expected to be an editor page id to switch to.
* <li> Other links are ignored.
- * </ul>
- *
+ * </ul>
+ *
* @return A new hyper-link listener for FormText to use.
*/
public final IHyperlinkListener createHyperlinkListener() {
@@ -478,7 +478,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
/**
* Open the http link into a browser
- *
+ *
* @param link The URL to open in a browser
*/
private void openLinkInBrowser(String link) {
@@ -516,7 +516,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
"Error opening the Android XML editor. Is the document an XML file?");
throw new RuntimeException("Android XML Editor Error", new CoreException(status));
}
-
+
IStructuredModel xml_model = getModelForRead();
if (xml_model != null) {
try {
@@ -534,9 +534,9 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
"Android XML Editor Error", null, e.getStatus());
}
}
-
+
/**
- * Returns the ISourceViewer associated with the Structured Text editor.
+ * Returns the ISourceViewer associated with the Structured Text editor.
*/
public final ISourceViewer getStructuredSourceViewer() {
if (mTextEditor != null) {
@@ -558,13 +558,18 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
}
return null;
}
-
+
/**
* Returns a version of the model that has been shared for read.
* <p/>
* Callers <em>must</em> call model.releaseFromRead() when done, typically
* in a try..finally clause.
- *
+ *
+ * Portability note: this uses getModelManager which is part of wst.sse.core; however
+ * the interface returned is part of wst.sse.core.internal.provisional so we can
+ * expect it to change in a distant future if they start cleaning their codebase,
+ * however unlikely that is.
+ *
* @return The model for the XML document or null if cannot be obtained from the editor
*/
public final IStructuredModel getModelForRead() {
@@ -576,14 +581,14 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
}
}
return null;
- }
-
+ }
+
/**
* Returns a version of the model that has been shared for edit.
* <p/>
* Callers <em>must</em> call model.releaseFromEdit() when done, typically
* in a try..finally clause.
- *
+ *
* @return The model for the XML document or null if cannot be obtained from the editor
*/
public final IStructuredModel getModelForEdit() {
@@ -595,7 +600,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
}
}
return null;
- }
+ }
/**
* Helper class to perform edits on the XML model whilst making sure the
@@ -609,7 +614,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
* <p/>
* The method is synchronous. As soon as the {@link IStructuredModel#changedModel()} method
* is called, XML model listeners will be triggered.
- *
+ *
* @param edit_action Something that will change the XML.
*/
public final void editXmlModel(Runnable edit_action) {
@@ -623,7 +628,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
model.releaseFromEdit();
}
}
-
+
/**
* Starts an "undo recording" session. This is managed by the underlying undo manager
* associated to the structured XML model.
@@ -632,7 +637,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
* <p/>
* beginUndoRecording/endUndoRecording calls can be nested (inner calls are ignored, only one
* undo operation is recorded.)
- *
+ *
* @param label The label for the undo operation. Can be null but we should really try to put
* something meaningful if possible.
* @return True if the undo recording actually started, false if any kind of error occured.
@@ -652,7 +657,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
}
return false;
}
-
+
/**
* Ends an "undo recording" session.
* <p/>
@@ -671,14 +676,14 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
}
}
}
-
+
/**
* Creates an "undo recording" session by calling the undoableAction runnable
* using {@link #beginUndoRecording(String)} and {@link #endUndoRecording()}.
* <p>
* You can nest several calls to {@link #wrapUndoRecording(String, Runnable)}, only one
* recording session will be created.
- *
+ *
* @param label The label for the undo operation. Can be null. Ideally we should really try
* to put something meaningful if possible.
*/
@@ -693,7 +698,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
}
}
}
-
+
/**
* Returns the XML {@link Document} or null if we can't get it
*/
@@ -709,7 +714,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
}
return null;
}
-
+
/**
* Returns the {@link IProject} for the edited file.
*/
@@ -719,16 +724,16 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
if (input instanceof FileEditorInput) {
FileEditorInput fileInput = (FileEditorInput)input;
IFile inputFile = fileInput.getFile();
-
+
if (inputFile != null) {
return inputFile.getProject();
}
}
}
-
+
return null;
}
-
+
/**
* Returns the {@link AndroidTargetData} for the edited file.
*/
@@ -738,22 +743,22 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
Sdk currentSdk = Sdk.getCurrent();
if (currentSdk != null) {
IAndroidTarget target = currentSdk.getTarget(project);
-
+
if (target != null) {
return currentSdk.getTargetData(target);
}
}
}
-
+
return null;
}
-
+
/**
* Listen to changes in the underlying XML model in the structured editor.
*/
private class XmlModelStateListener implements IModelStateListener {
-
+
/**
* A model is about to be changed. This typically is initiated by one
* client of the model, to signal a large change and/or a change to the
@@ -765,7 +770,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
public void modelAboutToBeChanged(IStructuredModel model) {
// pass
}
-
+
/**
* Signals that the changes foretold by modelAboutToBeChanged have been
* made. A typical use might be to refresh, or to resume processing that
@@ -776,7 +781,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
public void modelChanged(IStructuredModel model) {
xmlModelChanged(getXmlDocument(model));
}
-
+
/**
* Notifies that a model's dirty state has changed, and passes that state
* in isDirty. A model becomes dirty when any change is made, and becomes
@@ -787,7 +792,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
public void modelDirtyStateChanged(IStructuredModel model, boolean isDirty) {
// pass
}
-
+
/**
* A modelDeleted means the underlying resource has been deleted. The
* model itself is not removed from model management until all have
@@ -799,7 +804,7 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
public void modelResourceDeleted(IStructuredModel model) {
// pass
}
-
+
/**
* A model has been renamed or copied (as in saveAs..). In the renamed
* case, the two paramenters are the same instance, and only contain the
@@ -810,14 +815,14 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
public void modelResourceMoved(IStructuredModel oldModel, IStructuredModel newModel) {
// pass
}
-
+
/**
* This AndroidEditor implementation of IModelChangedListener is empty.
*/
public void modelAboutToBeReinitialized(IStructuredModel structuredModel) {
// pass
}
-
+
/**
* This AndroidEditor implementation of IModelChangedListener is empty.
*/
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ReferenceAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ReferenceAttributeDescriptor.java
index c6aecd2..fc0ed83 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ReferenceAttributeDescriptor.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/ReferenceAttributeDescriptor.java
@@ -32,8 +32,10 @@ import org.eclipse.swt.widgets.Composite;
*/
public final class ReferenceAttributeDescriptor extends TextAttributeDescriptor {
+ /** The {@link ResourceType} that this reference attribute can accept. It can be null,
+ * in which case any reference type can be used. */
private ResourceType mResourceType;
-
+
/**
* Creates a reference attributes that can contain any type of resources.
* @param xmlLocalName The XML name of the attribute (case sensitive)
@@ -46,7 +48,7 @@ public final class ReferenceAttributeDescriptor extends TextAttributeDescriptor
String tooltip) {
super(xmlLocalName, uiName, nsUri, tooltip);
}
-
+
/**
* Creates a reference attributes that can contain a reference to a specific
* {@link ResourceType}.
@@ -58,14 +60,20 @@ public final class ReferenceAttributeDescriptor extends TextAttributeDescriptor
* See {@link SdkConstants#NS_RESOURCES} for a common value.
* @param tooltip A non-empty tooltip string or null
*/
- public ReferenceAttributeDescriptor(ResourceType resourceType,
+ public ReferenceAttributeDescriptor(ResourceType resourceType,
String xmlLocalName, String uiName, String nsUri,
String tooltip) {
super(xmlLocalName, uiName, nsUri, tooltip);
mResourceType = resourceType;
}
-
-
+
+
+ /** Returns the {@link ResourceType} that this reference attribute can accept.
+ * It can be null, in which case any reference type can be used. */
+ public ResourceType getResourceType() {
+ return mResourceType;
+ }
+
/**
* @return A new {@link UiResourceAttributeNode} linked to this reference descriptor.
*/
@@ -73,7 +81,7 @@ public final class ReferenceAttributeDescriptor extends TextAttributeDescriptor
public UiAttributeNode createUiNode(UiElementNode uiParent) {
return new UiResourceAttributeNode(mResourceType, this, uiParent);
}
-
+
// ------- IPropertyDescriptor Methods
@Override
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringAction.java
index 668cff3..6caf966 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringAction.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringAction.java
@@ -4,7 +4,7 @@
* 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
@@ -47,7 +47,7 @@ import org.eclipse.ui.part.FileEditorInput;
* Action executed when the "Extract String" menu item is invoked.
* <p/>
* The intent of the action is to start a refactoring that extracts a source string and
- * replaces it by an Android string resource ID.
+ * replaces it by an Android string resource ID.
* <p/>
* Workflow:
* <ul>
@@ -74,6 +74,7 @@ public class ExtractStringAction implements IWorkbenchWindowActionDelegate {
/** Keep track of the current workbench window. */
private IWorkbenchWindow mWindow;
private ITextSelection mSelection;
+ private IEditorPart mEditor;
private IFile mFile;
/**
@@ -103,11 +104,12 @@ public class ExtractStringAction implements IWorkbenchWindowActionDelegate {
mSelection = null;
mFile = null;
-
+
if (selection instanceof ITextSelection) {
mSelection = (ITextSelection) selection;
if (mSelection.getLength() > 0) {
- mFile = getSelectedFile();
+ mEditor = getActiveEditor();
+ mFile = getSelectedFile(mEditor);
}
}
@@ -119,7 +121,7 @@ public class ExtractStringAction implements IWorkbenchWindowActionDelegate {
*/
public void run(IAction action) {
if (mSelection != null && mFile != null) {
- ExtractStringRefactoring ref = new ExtractStringRefactoring(mFile, mSelection);
+ ExtractStringRefactoring ref = new ExtractStringRefactoring(mFile, mEditor, mSelection);
RefactoringWizard wizard = new ExtractStringWizard(ref, mFile.getProject());
RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
try {
@@ -131,6 +133,21 @@ public class ExtractStringAction implements IWorkbenchWindowActionDelegate {
}
/**
+ * Returns the active editor (hopefully matching our selection) or null.
+ */
+ private IEditorPart getActiveEditor() {
+ IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ if (wwin != null) {
+ IWorkbenchPage page = wwin.getActivePage();
+ if (page != null) {
+ return page.getActiveEditor();
+ }
+ }
+
+ return null;
+ }
+
+ /**
* Returns the active {@link IFile} (hopefully matching our selection) or null.
* The file is only returned if it's a file from a project with an Android nature.
* <p/>
@@ -138,33 +155,26 @@ public class ExtractStringAction implements IWorkbenchWindowActionDelegate {
* for the refactoring. This check is performed when the refactoring is invoked since
* it can then produce meaningful error messages as needed.
*/
- private IFile getSelectedFile() {
- IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
- if (wwin != null) {
- IWorkbenchPage page = wwin.getActivePage();
- if (page != null) {
- IEditorPart editor = page.getActiveEditor();
- if (editor != null) {
- IEditorInput input = editor.getEditorInput();
-
- if (input instanceof FileEditorInput) {
- FileEditorInput fi = (FileEditorInput) input;
- IFile file = fi.getFile();
- if (file.exists()) {
- IProject proj = file.getProject();
- try {
- if (proj != null && proj.hasNature(AndroidConstants.NATURE)) {
- return file;
- }
- } catch (CoreException e) {
- // ignore
- }
+ private IFile getSelectedFile(IEditorPart editor) {
+ if (editor != null) {
+ IEditorInput input = editor.getEditorInput();
+
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fi = (FileEditorInput) input;
+ IFile file = fi.getFile();
+ if (file.exists()) {
+ IProject proj = file.getProject();
+ try {
+ if (proj != null && proj.hasNature(AndroidConstants.NATURE)) {
+ return file;
}
+ } catch (CoreException e) {
+ // ignore
}
}
}
}
-
+
return null;
}
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java
index 0cccbd2..155cee0 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java
@@ -4,7 +4,7 @@
* 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
@@ -17,8 +17,17 @@
package com.android.ide.eclipse.adt.internal.refactorings.extractstring;
import com.android.ide.eclipse.adt.AndroidConstants;
+import com.android.ide.eclipse.adt.internal.editors.AndroidEditor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ReferenceAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.ide.eclipse.adt.internal.project.AndroidManifestParser;
+import com.android.ide.eclipse.adt.internal.resources.ResourceType;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceFolderType;
+import com.android.sdklib.SdkConstants;
+import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
@@ -63,6 +72,16 @@ import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.edits.TextEditGroup;
+import org.eclipse.ui.IEditorPart;
+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.text.IStructuredDocument;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
+import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
+import org.w3c.dom.Node;
import java.io.BufferedReader;
import java.io.IOException;
@@ -70,6 +89,7 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -85,11 +105,6 @@ import java.util.Map;
* <li> The action finds the {@link ICompilationUnit} being edited as well as the current
* {@link ITextSelection}. The action creates a new instance of this refactoring as
* well as an {@link ExtractStringWizard} and runs the operation.
- * <li> TODO: to support refactoring from an XML file, the action should give the {@link IFile}
- * and then here we would have to determine whether it's a suitable Android XML file or a
- * suitable Java file.
- * TODO: enumerate the exact valid contexts in Android XML files, e.g. attributes in layout
- * files or text elements (e.g. <string>foo</string>) for values, etc.
* <li> Step 1 of the refactoring is to check the preliminary conditions. Right now we check
* that the java source is not read-only and is in sync. We also try to find a string under
* the selection. If this fails, the refactoring is aborted.
@@ -104,7 +119,7 @@ import java.util.Map;
* and compute the actual changes.
* <li> When all changes are computed, {@link #createChange(IProgressMonitor)} is invoked.
* </ul>
- *
+ *
* The list of changes are:
* <ul>
* <li> If the target XML does not exist, create it with the new string ID.
@@ -113,8 +128,6 @@ import java.util.Map;
* <li> Create an AST rewriter to edit the source Java file and replace all occurences by the
* new computed R.string.foo. Also need to rewrite imports to import R as needed.
* If there's already a conflicting R included, we need to insert the FQCN instead.
- * <li> TODO: If the source is an XML file, determine if we need to change an attribute or a
- * a text element.
* <li> TODO: Have a pref in the wizard: [x] Change other XML Files
* <li> TODO: Have a pref in the wizard: [x] Change other Java Files
* </ul>
@@ -139,12 +152,17 @@ public class ExtractStringRefactoring extends Refactoring {
*/
SELECT_NEW_ID
}
-
+
/** The {@link Mode} of operation of the refactoring. */
private final Mode mMode;
+ /** Non-null when editing an Android Resource XML file: identifies the attribute name
+ * of the value being edited. When null, the source is an Android Java file. */
+ private String mXmlAttributeName;
/** The file model being manipulated.
* Value is null when not on {@link Mode#EDIT_SOURCE} mode. */
private final IFile mFile;
+ /** The editor. Non-null when invoked from {@link ExtractStringAction}. Null otherwise. */
+ private final IEditorPart mEditor;
/** The project that contains {@link #mFile} and that contains the target XML file to modify. */
private final IProject mProject;
/** The start of the selection in {@link #mFile}.
@@ -180,14 +198,14 @@ public class ExtractStringRefactoring extends Refactoring {
private static final String KEY_SEL_START = "sel-start"; //$NON-NLS-1$
private static final String KEY_SEL_END = "sel-end"; //$NON-NLS-1$
private static final String KEY_TOK_ESC = "tok-esc"; //$NON-NLS-1$
+ private static final String KEY_XML_ATTR_NAME = "xml-attr-name"; //$NON-NLS-1$
- public ExtractStringRefactoring(Map<String, String> arguments)
- throws NullPointerException {
+ public ExtractStringRefactoring(Map<String, String> arguments) throws NullPointerException {
mMode = Mode.valueOf(arguments.get(KEY_MODE));
IPath path = Path.fromPortableString(arguments.get(KEY_PROJECT));
mProject = (IProject) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
-
+
if (mMode == Mode.EDIT_SOURCE) {
path = Path.fromPortableString(arguments.get(KEY_FILE));
mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
@@ -195,13 +213,17 @@ public class ExtractStringRefactoring extends Refactoring {
mSelectionStart = Integer.parseInt(arguments.get(KEY_SEL_START));
mSelectionEnd = Integer.parseInt(arguments.get(KEY_SEL_END));
mTokenString = arguments.get(KEY_TOK_ESC);
+ mXmlAttributeName = arguments.get(KEY_XML_ATTR_NAME);
} else {
mFile = null;
mSelectionStart = mSelectionEnd = -1;
mTokenString = null;
+ mXmlAttributeName = null;
}
+
+ mEditor = null;
}
-
+
private Map<String, String> createArgumentMap() {
HashMap<String, String> args = new HashMap<String, String>();
args.put(KEY_MODE, mMode.name());
@@ -211,6 +233,7 @@ public class ExtractStringRefactoring extends Refactoring {
args.put(KEY_SEL_START, Integer.toString(mSelectionStart));
args.put(KEY_SEL_END, Integer.toString(mSelectionEnd));
args.put(KEY_TOK_ESC, mTokenString);
+ args.put(KEY_XML_ATTR_NAME, mXmlAttributeName);
}
return args;
}
@@ -220,13 +243,15 @@ public class ExtractStringRefactoring extends Refactoring {
* *existing* source file. Its purpose is then to get the selected string of
* the source and propose to change it by an XML id. The XML id may be a new one
* or an existing one.
- *
+ *
* @param file The source file to process. Cannot be null. File must exist in workspace.
+ * @param editor
* @param selection The selection in the source file. Cannot be null or empty.
*/
- public ExtractStringRefactoring(IFile file, ITextSelection selection) {
+ public ExtractStringRefactoring(IFile file, IEditorPart editor, ITextSelection selection) {
mMode = Mode.EDIT_SOURCE;
mFile = file;
+ mEditor = editor;
mProject = file.getProject();
mSelectionStart = selection.getOffset();
mSelectionEnd = mSelectionStart + Math.max(0, selection.getLength() - 1);
@@ -235,7 +260,7 @@ public class ExtractStringRefactoring extends Refactoring {
/**
* Constructor to use when the Extract String refactoring is called without
* any source file. Its purpose is then to create a new XML string ID.
- *
+ *
* @param project The project where the target XML file to modify is located. Cannot be null.
* @param enforceNew If true the XML ID must be a new one. If false, an existing ID can be
* used.
@@ -243,10 +268,11 @@ public class ExtractStringRefactoring extends Refactoring {
public ExtractStringRefactoring(IProject project, boolean enforceNew) {
mMode = enforceNew ? Mode.SELECT_NEW_ID : Mode.SELECT_ID;
mFile = null;
+ mEditor = null;
mProject = project;
mSelectionStart = mSelectionEnd = -1;
}
-
+
/**
* @see org.eclipse.ltk.core.refactoring.Refactoring#getName()
*/
@@ -260,11 +286,11 @@ public class ExtractStringRefactoring extends Refactoring {
return "Extract Android String";
}
-
+
public Mode getMode() {
return mMode;
}
-
+
/**
* Gets the actual string selected, after UTF characters have been escaped,
* good for display.
@@ -272,11 +298,11 @@ public class ExtractStringRefactoring extends Refactoring {
public String getTokenString() {
return mTokenString;
}
-
+
public String getXmlStringId() {
return mXmlStringId;
}
-
+
/**
* Step 1 of 3 of the refactoring:
* Checks that the current selection meets the initial condition before the ExtractString
@@ -289,10 +315,10 @@ public class ExtractStringRefactoring extends Refactoring {
* <p/>
* This is also used to extract the string to be modified, so that we can display it in
* the refactoring wizard.
- *
+ *
* @see org.eclipse.ltk.core.refactoring.Refactoring#checkInitialConditions(org.eclipse.core.runtime.IProgressMonitor)
- *
- * @throws CoreException
+ *
+ * @throws CoreException
*/
@Override
public RefactoringStatus checkInitialConditions(IProgressMonitor monitor)
@@ -302,15 +328,15 @@ public class ExtractStringRefactoring extends Refactoring {
mTokenString = null;
RefactoringStatus status = new RefactoringStatus();
-
+
try {
- monitor.beginTask("Checking preconditions...", 5);
+ monitor.beginTask("Checking preconditions...", 6);
if (mMode != Mode.EDIT_SOURCE) {
- monitor.worked(5);
+ monitor.worked(6);
return status;
}
-
+
if (!checkSourceFile(mFile, status, monitor)) {
return status;
}
@@ -324,35 +350,61 @@ public class ExtractStringRefactoring extends Refactoring {
status.addFatalError("The file is read-only, please make it writeable first.");
return status;
}
-
+
// This is a Java file. Check if it contains the selection we want.
if (!findSelectionInJavaUnit(mUnit, status, monitor)) {
return status;
}
-
+
} catch (Exception e) {
// That was not a Java file. Ignore.
}
-
- if (mUnit == null) {
- // Check this an XML file and get the selection and its context.
- // TODO
- status.addFatalError("Selection must be inside a Java source file.");
+
+ if (mUnit != null) {
+ monitor.worked(1);
+ return status;
+ }
+
+ // Check this a Layout XML file and get the selection and its context.
+ if (mFile != null && AndroidConstants.EXT_XML.equals(mFile.getFileExtension())) {
+
+ // Currently we only support Android resource XML files, so they must have a path
+ // similar to
+ // project/res/<type>[-<configuration>]/*.xml
+ // There is no support for sub folders, so the segment count must be 4.
+ // We don't need to check the type folder name because a/ we only accept
+ // an AndroidEditor source and b/ aapt generates a compilation error for
+ // unknown folders.
+ IPath path = mFile.getFullPath();
+ // check if we are inside the project/res/* folder.
+ if (path.segmentCount() == 4) {
+ if (path.segment(1).equalsIgnoreCase(SdkConstants.FD_RESOURCES)) {
+ if (!findSelectionInXmlFile(mFile, status, monitor)) {
+ return status;
+ }
+ }
+ }
+ }
+
+ if (!status.isOK()) {
+ status.addFatalError("Selection must be inside a Java source or an Android Layout XML file.");
}
+
} finally {
monitor.done();
}
-
+
return status;
}
/**
* Try to find the selected Java element in the compilation unit.
- *
+ *
* If selection matches a string literal, capture it, otherwise add a fatal error
* to the status.
- *
+ *
* On success, advance the monitor by 3.
+ * Returns status.isOK().
*/
private boolean findSelectionInJavaUnit(ICompilationUnit unit,
RefactoringStatus status, IProgressMonitor monitor) {
@@ -373,7 +425,7 @@ public class ExtractStringRefactoring extends Refactoring {
token = scanner.getNextToken()) {
if (scanner.getCurrentTokenStartPosition() <= mSelectionStart &&
scanner.getCurrentTokenEndPosition() >= mSelectionEnd) {
- // found the token, but only keep of the right type
+ // found the token, but only keep if the right type
if (token == ITerminalSymbols.TokenNameStringLiteral) {
mTokenString = new String(scanner.getCurrentTokenSource());
}
@@ -404,21 +456,220 @@ public class ExtractStringRefactoring extends Refactoring {
mTokenString = null;
}
}
-
+
if (mTokenString == null) {
status.addFatalError("Please select a Java string literal.");
}
-
+
monitor.worked(1);
return status.isOK();
}
+ /**
+ * Try to find the selected XML element. This implementation replies on the refactoring
+ * originating from an Android Layout Editor. We rely on some internal properties of the
+ * Structured XML editor to retrieve file content to avoid parsing it again. We also rely
+ * on our specific Android XML model to get element & attribute descriptor properties.
+ *
+ * If selection matches a string literal, capture it, otherwise add a fatal error
+ * to the status.
+ *
+ * On success, advance the monitor by 1.
+ * Returns status.isOK().
+ */
+ private boolean findSelectionInXmlFile(IFile file,
+ RefactoringStatus status,
+ IProgressMonitor monitor) {
+
+ try {
+ if (!(mEditor instanceof AndroidEditor)) {
+ status.addFatalError("Only the Android XML Editor is currently supported.");
+ return status.isOK();
+ }
+
+ AndroidEditor editor = (AndroidEditor) mEditor;
+ IStructuredModel smodel = null;
+ Node node = null;
+ String attrName = null;
+
+ try {
+ // See the portability note in AndroidEditor#getModelForRead() javadoc.
+ smodel = editor.getModelForRead();
+ if (smodel != null) {
+ // The structured model gives the us the actual XML Node element where the
+ // offset is. By using this Node, we can find the exact UiElementNode of our
+ // model and thus we'll be able to get the properties of the attribute -- to
+ // check if it accepts a string reference. This does not however tell us if
+ // the selection is actually in an attribute value, nor which attribute is
+ // being edited.
+ for(int offset = mSelectionStart; offset >= 0 && node == null; --offset) {
+ node = (Node) smodel.getIndexedRegion(offset);
+ }
+
+ if (node == null) {
+ status.addFatalError("The selection does not match any element in the XML document.");
+ return status.isOK();
+ }
+
+ if (node.getNodeType() != Node.ELEMENT_NODE) {
+ status.addFatalError("The selection is not inside an actual XML element.");
+ return status.isOK();
+ }
+
+ IStructuredDocument sdoc = smodel.getStructuredDocument();
+ if (sdoc != null) {
+ // Portability note: all the structured document implementation is
+ // under wst.sse.core.internal.provisional so we can expect it to change in
+ // a distant future if they start cleaning their codebase, however unlikely
+ // that is.
+
+ int selStart = mSelectionStart;
+ IStructuredDocumentRegion region =
+ sdoc.getRegionAtCharacterOffset(selStart);
+ if (region != null &&
+ DOMRegionContext.XML_TAG_NAME.equals(region.getType())) {
+ // The region gives us the textual representation of the XML element
+ // where the selection starts, split using sub-regions. We now just
+ // need to iterate through the sub-regions to find which one
+ // contains the actual selection. We're interested in an attribute
+ // value however when we find one we want to memorize the attribute
+ // name that was defined just before.
+
+ int startInRegion = selStart - region.getStartOffset();
+
+ int nb = region.getNumberOfRegions();
+ ITextRegionList list = region.getRegions();
+
+ for (int i = 0; i < nb; i++) {
+ ITextRegion subRegion = list.get(i);
+ String type = subRegion.getType();
+
+ if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
+ attrName = region.getText(subRegion);
+ }
+
+ if (subRegion.getStart() <= startInRegion &&
+ startInRegion <= subRegion.getEnd() &&
+ DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
+ // We found the value. Only accept it if not empty
+ // and if we found an attribute name before.
+ String text = region.getText(subRegion);
+
+ // The attribute value will contain the XML quotes. Remove them.
+ int len = text.length();
+ if (len >= 2 &&
+ text.charAt(0) == '"' &&
+ text.charAt(len - 1) == '"') {
+ text = text.substring(1, len - 1);
+ } else if (len >= 2 &&
+ text.charAt(0) == '\'' &&
+ text.charAt(len - 1) == '\'') {
+ text = text.substring(1, len - 1);
+ }
+ if (text.length() > 0 && attrName != null) {
+ mTokenString = text;
+ }
+
+ break;
+ }
+ }
+
+ if (mTokenString == null) {
+ status.addFatalError(
+ "The selection is not inside an actual XML attribute value.");
+ }
+ }
+ }
+
+ if (mTokenString != null && node != null && attrName != null) {
+
+ UiElementNode rootUiNode = editor.getUiRootNode();
+ UiElementNode currentUiNode =
+ rootUiNode == null ? null : rootUiNode.findXmlNode(node);
+ ReferenceAttributeDescriptor attrDesc = null;
+
+ if (currentUiNode != null) {
+ // remove any namespace prefix from the attribute name
+ String name = attrName;
+ int pos = name.indexOf(':');
+ if (pos > 0 && pos < name.length() - 1) {
+ name = name.substring(pos + 1);
+ }
+
+ for (UiAttributeNode attrNode : currentUiNode.getUiAttributes()) {
+ if (attrNode.getDescriptor().getXmlLocalName().equals(name)) {
+ AttributeDescriptor desc = attrNode.getDescriptor();
+ if (desc instanceof ReferenceAttributeDescriptor) {
+ attrDesc = (ReferenceAttributeDescriptor) desc;
+ }
+ break;
+ }
+ }
+ }
+
+ // The attribute descriptor is a resource reference. It must either accept
+ // of any resource type or specifically accept string types.
+ if (attrDesc != null &&
+ (attrDesc.getResourceType() == null ||
+ attrDesc.getResourceType() == ResourceType.STRING)) {
+ // We have one more check to do: is the current string value already
+ // an Android XML string reference? If so, we can't edit it.
+ if (mTokenString.startsWith("@")) { //$NON-NLS-1$
+ int pos1 = 0;
+ if (mTokenString.length() > 1 && mTokenString.charAt(1) == '+') {
+ pos1++;
+ }
+ int pos2 = mTokenString.indexOf('/');
+ if (pos2 > pos1) {
+ String kind = mTokenString.substring(pos1 + 1, pos2);
+ mTokenString = null;
+ status.addFatalError(String.format(
+ "The attribute %1$s already contains a %2$s reference.",
+ attrName,
+ kind));
+ }
+ }
+
+ if (mTokenString != null) {
+ // We're done with all our checks. mTokenString contains the
+ // current attribute value. We don't memorize the region nor the
+ // attribute, however we memorize the textual attribute name so
+ // that we can offer replacement for all its occurrences.
+ mXmlAttributeName = attrName;
+ }
+
+ } else {
+ mTokenString = null;
+ status.addFatalError(String.format(
+ "The attribute %1$s does not accept a string reference.",
+ attrName));
+ }
+
+ } else {
+ // We shouldn't get here: we're missing one of the token string, the node
+ // or the attribute name. All of them have been checked earlier so don't
+ // set any specific error.
+ mTokenString = null;
+ }
+ }
+ } finally {
+ if (smodel != null) {
+ smodel.releaseFromRead();
+ }
+ }
+
+ } finally {
+ monitor.worked(1);
+ }
+
+ return status.isOK();
+ }
/**
* Tests from org.eclipse.jdt.internal.corext.refactoringChecks#validateEdit()
* Might not be useful.
- *
+ *
* On success, advance the monitor by 2.
- *
+ *
* @return False if caller should abort, true if caller should continue.
*/
private boolean checkSourceFile(IFile file,
@@ -430,7 +681,7 @@ public class ExtractStringRefactoring extends Refactoring {
return false;
}
monitor.worked(1);
-
+
// make sure we can write to it.
ResourceAttributes resAttr = file.getResourceAttributes();
if (resAttr == null || resAttr.isReadOnly()) {
@@ -438,7 +689,7 @@ public class ExtractStringRefactoring extends Refactoring {
return false;
}
monitor.worked(1);
-
+
return true;
}
@@ -449,13 +700,13 @@ public class ExtractStringRefactoring extends Refactoring {
* <p/>
* In this case, most of the sanity checks are done by the wizard so essentially this
* should only be called if the wizard positively validated the user input.
- *
+ *
* Here we do check that the target resource XML file either does not exists or
* is not read-only.
- *
+ *
* @see org.eclipse.ltk.core.refactoring.Refactoring#checkFinalConditions(IProgressMonitor)
- *
- * @throws CoreException
+ *
+ * @throws CoreException
*/
@Override
public RefactoringStatus checkFinalConditions(IProgressMonitor monitor)
@@ -490,14 +741,14 @@ public class ExtractStringRefactoring extends Refactoring {
}
}
monitor.worked(1);
-
+
if (status.hasError()) {
return status;
}
-
+
mChanges = new ArrayList<Change>();
-
-
+
+
// Prepare the change for the XML file.
if (!mXmlHelper.isResIdDuplicate(mProject, mTargetXmlFileWsPath, mXmlStringId)) {
@@ -514,19 +765,28 @@ public class ExtractStringRefactoring extends Refactoring {
}
if (mMode == Mode.EDIT_SOURCE) {
- // Prepare the change to the Java compilation unit
- List<Change> changes = computeJavaChanges(mUnit, mXmlStringId, mTokenString,
- status, SubMonitor.convert(monitor, 1));
+ List<Change> changes = null;
+ if (mXmlAttributeName != null) {
+ // Prepare the change to the Android resource XML file
+ changes = computeXmlSourceChanges(mFile,
+ mXmlStringId, mTokenString, mXmlAttributeName,
+ status, monitor);
+
+ } else {
+ // Prepare the change to the Java compilation unit
+ changes = computeJavaChanges(mUnit, mXmlStringId, mTokenString,
+ status, SubMonitor.convert(monitor, 1));
+ }
if (changes != null) {
mChanges.addAll(changes);
}
}
-
+
monitor.worked(1);
} finally {
monitor.done();
}
-
+
return status;
}
@@ -535,7 +795,7 @@ public class ExtractStringRefactoring extends Refactoring {
* ID to the given XML File.
* <p/>
* This does not actually modify the file.
- *
+ *
* @param targetXml The file resource to modify.
* @param xmlStringId The new ID to insert.
* @param tokenString The old string, which will be the value in the XML string.
@@ -549,7 +809,7 @@ public class ExtractStringRefactoring extends Refactoring {
TextFileChange xmlChange = new TextFileChange(getName(), targetXml);
xmlChange.setTextType("xml"); //$NON-NLS-1$
-
+
TextEdit edit = null;
TextEditGroup editGroup = null;
@@ -571,9 +831,9 @@ public class ExtractStringRefactoring extends Refactoring {
// The file exist. Attempt to parse it as a valid XML document.
try {
int[] indices = new int[2];
-
+
// TODO case where we replace the value of an existing XML String ID
-
+
if (findXmlOpeningTagPos(targetXml.getContents(), "resources", indices)) { //$NON-NLS-1$
// Indices[1] indicates whether we found > or />. It can only be 1 or 2.
// Indices[0] is the position of the first character of either > or />.
@@ -582,7 +842,7 @@ public class ExtractStringRefactoring extends Refactoring {
// could by capturing whatever whitespace is after the closing bracket and
// applying it here before our tag, unless we were dealing with an empty
// resource tag.)
-
+
int offset = indices[0];
int len = indices[1];
StringBuilder content = new StringBuilder();
@@ -634,8 +894,8 @@ public class ExtractStringRefactoring extends Refactoring {
* We need to deal with the case where the element is written as <resources/>, in
* which case the caller will want to replace /> by ">...</...>". To do that we return
* two values: the first offset of the closing tag (e.g. / or >) and the length, which
- * can only be 1 or 2. If it's 2, the caller have to deal with /> instead of just >.
- *
+ * can only be 1 or 2. If it's 2, the caller has to deal with /> instead of just >.
+ *
* @param contents An existing buffer to parse.
* @param tag The tag to look for.
* @param indices The return values: [0] is the offset of the closing bracket and [1] is
@@ -650,7 +910,7 @@ public class ExtractStringRefactoring extends Refactoring {
tag = "<" + tag;
int tagLen = tag.length();
int maxLen = tagLen < 3 ? 3 : tagLen;
-
+
try {
int offset = 0;
int i = 0;
@@ -684,11 +944,11 @@ public class ExtractStringRefactoring extends Refactoring {
indices[1]++;
}
return true;
-
+
} else if (!inComment && !inTag) {
// not a comment and not our tag yet, so we're capturing because a
// tag is being opened but we don't know which one yet.
-
+
// look for either the opening or a comment or
// the opening of our tag.
if (len == 3 && sb.equals("<--")) { //$NON-NLS-1$
@@ -726,10 +986,209 @@ public class ExtractStringRefactoring extends Refactoring {
// oh come on...
}
}
-
+
return false;
}
+
+ /**
+ * Computes the changes to be made to the source Android XML file(s) and
+ * returns a list of {@link Change}.
+ */
+ private List<Change> computeXmlSourceChanges(IFile sourceFile,
+ String xmlStringId,
+ String tokenString,
+ String xmlAttrName,
+ RefactoringStatus status,
+ IProgressMonitor monitor) {
+
+ if (!sourceFile.exists()) {
+ status.addFatalError(String.format("XML file '%1$s' does not exist.",
+ sourceFile.getFullPath().toOSString()));
+ return null;
+ }
+
+ // In the initial condition check we validated that this file is part of
+ // an Android resource folder, with a folder path that looks like
+ // /project/res/<type>-<configuration>/<filename.xml>
+ // Here we are going to offer XML source change for the same filename accross all
+ // configurations of the same res type. E.g. if we're processing a res/layout/main.xml
+ // file then we want to offer changes for res/layout-fr/main.xml. We compute such a
+ // list here.
+ HashSet<IFile> files = new HashSet<IFile>();
+ files.add(sourceFile);
+
+ if (AndroidConstants.EXT_XML.equals(sourceFile.getFileExtension())) {
+ IPath path = sourceFile.getFullPath();
+ if (path.segmentCount() == 4 && path.segment(1).equals(SdkConstants.FD_RESOURCES)) {
+ IProject project = sourceFile.getProject();
+ String filename = path.segment(3);
+ String initialTypeName = path.segment(2);
+ ResourceFolderType type = ResourceFolderType.getFolderType(initialTypeName);
+
+ IContainer res = sourceFile.getParent().getParent();
+ if (type != null && res != null && res.getType() == IResource.FOLDER) {
+ try {
+ for (IResource r : res.members()) {
+ if (r != null && r.getType() == IResource.FOLDER) {
+ String name = r.getName();
+ // Skip the initial folder name, it's already in the list.
+ if (!name.equals(initialTypeName)) {
+ // Only accept the same folder type (e.g. layout-*)
+ ResourceFolderType t =
+ ResourceFolderType.getFolderType(initialTypeName);
+ if (t == type) {
+ // recompute the path
+ IPath p = res.getFullPath().append(name).append(filename);
+ IResource f = project.findMember(p);
+ if (f != null && f instanceof IFile) {
+ files.add((IFile) f);
+ }
+ }
+ }
+ }
+ }
+ } catch (CoreException e) {
+ // Ignore.
+ }
+ }
+ }
+ }
+
+ SubMonitor subMonitor = SubMonitor.convert(monitor, Math.min(1, files.size()));
+
+ ArrayList<Change> changes = new ArrayList<Change>();
+
+ try {
+ // Portability note: getModelManager is part of wst.sse.core however the
+ // interface returned is part of wst.sse.core.internal.provisional so we can
+ // expect it to change in a distant future if they start cleaning their codebase,
+ // however unlikely that is.
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+
+ for (IFile file : files) {
+
+ IStructuredDocument sdoc = modelManager.createStructuredDocumentFor(file);
+
+ if (sdoc == null) {
+ status.addFatalError("XML structured document not found"); //$NON-NLS-1$
+ return null;
+ }
+
+ TextFileChange xmlChange = new TextFileChange(getName(), file);
+ xmlChange.setTextType("xml"); //$NON-NLS-1$
+
+ MultiTextEdit multiEdit = new MultiTextEdit();
+ ArrayList<TextEditGroup> editGroups = new ArrayList<TextEditGroup>();
+
+ String quotedReplacement = quotedAttrValue("@string/" + xmlStringId);
+
+ // Prepare the change set
+ try {
+ for (IStructuredDocumentRegion region : sdoc.getStructuredDocumentRegions()) {
+ // Only look at XML "top regions"
+ if (!DOMRegionContext.XML_TAG_NAME.equals(region.getType())) {
+ continue;
+ }
+
+ int nb = region.getNumberOfRegions();
+ ITextRegionList list = region.getRegions();
+ String lastAttrName = null;
+
+ for (int i = 0; i < nb; i++) {
+ ITextRegion subRegion = list.get(i);
+ String type = subRegion.getType();
+
+ if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
+ // Memorize the last attribute name seen
+ lastAttrName = region.getText(subRegion);
+
+ } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
+ // Check this is the attribute and the original string
+ String text = region.getText(subRegion);
+
+ int len = text.length();
+ if (len >= 2 &&
+ text.charAt(0) == '"' &&
+ text.charAt(len - 1) == '"') {
+ text = text.substring(1, len - 1);
+ } else if (len >= 2 &&
+ text.charAt(0) == '\'' &&
+ text.charAt(len - 1) == '\'') {
+ text = text.substring(1, len - 1);
+ }
+
+ if (xmlAttrName.equals(lastAttrName) && tokenString.equals(text)) {
+
+ // Found an occurrence. Create a change for it.
+ TextEdit edit = new ReplaceEdit(
+ region.getStartOffset() + subRegion.getStart(),
+ subRegion.getTextLength(),
+ quotedReplacement);
+ TextEditGroup editGroup = new TextEditGroup(
+ "Replace attribute string by ID",
+ edit);
+
+ multiEdit.addChild(edit);
+ editGroups.add(editGroup);
+ }
+ }
+ }
+ }
+ } catch (Throwable t) {
+ // Since we use some internal APIs, use a broad catch-all to report any
+ // unexpected issue rather than crash the whole refactoring.
+ status.addFatalError(
+ String.format("XML refactoring error: %1$s", t.getMessage()));
+ } finally {
+ if (multiEdit.hasChildren()) {
+ xmlChange.setEdit(multiEdit);
+ for (TextEditGroup group : editGroups) {
+ xmlChange.addTextEditChangeGroup(
+ new TextEditChangeGroup(xmlChange, group));
+ }
+ changes.add(xmlChange);
+ }
+ subMonitor.worked(1);
+ }
+ } // for files
+
+ } catch (IOException e) {
+ status.addFatalError(String.format("XML model IO error: %1$s.", e.getMessage()));
+ } catch (CoreException e) {
+ status.addFatalError(String.format("XML model core error: %1$s.", e.getMessage()));
+ } finally {
+ if (changes.size() > 0) {
+ return changes;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a quoted attribute value suitable to be placed after an attributeName=
+ * statement in an XML stream.
+ *
+ * According to http://www.w3.org/TR/2008/REC-xml-20081126/#NT-AttValue
+ * the attribute value can be either quoted using ' or " and the corresponding
+ * entities &apos; or &quot; must be used inside.
+ */
+ private String quotedAttrValue(String attrValue) {
+ if (attrValue.indexOf('"') == -1) {
+ // no double-quotes inside, use double-quotes around.
+ return '"' + attrValue + '"';
+ }
+ if (attrValue.indexOf('\'') == -1) {
+ // no single-quotes inside, use single-quotes around.
+ return '\'' + attrValue + '\'';
+ }
+ // If we get here, there's a mix. Opt for double-quote around and replace
+ // inner double-quotes.
+ attrValue = attrValue.replace("\"", "&quot;"); //$NON-NLS-1$ //$NON-NLS-2$
+ return '"' + attrValue + '"';
+ }
+
/**
* Computes the changes to be made to Java file(s) and returns a list of {@link Change}.
*/
@@ -762,14 +1221,15 @@ public class ExtractStringRefactoring extends Refactoring {
error = e.getLocalizedMessage();
}
}
-
+
if (error != null) {
status.addFatalError(
String.format("Failed to parse file %1$s: %2$s.",
- manifestFile.getFullPath(), error));
+ manifestFile == null ? "" : manifestFile.getFullPath(), //$NON-NLS-1$
+ error));
return null;
}
-
+
// TODO in a future version we might want to collect various Java files that
// need to be updated in the same project and process them all together.
// To do that we need to use an ASTRequestor and parser.createASTs, kind of
@@ -780,11 +1240,11 @@ public class ExtractStringRefactoring extends Refactoring {
// public void acceptAST(ICompilationUnit sourceUnit, CompilationUnit astNode) {
// super.acceptAST(sourceUnit, astNode);
// // TODO process astNode
- // }
+ // }
// };
// ...
// parser.createASTs(compilationUnits, bindingKeys, requestor, monitor)
- //
+ //
// and then add multiple TextFileChange to the changes arraylist.
// Right now the changes array will contain one TextFileChange at most.
@@ -842,26 +1302,26 @@ public class ExtractStringRefactoring extends Refactoring {
// Only create a change set if any edit was collected
if (edit.hasChildren()) {
change.setEdit(edit);
-
+
// Create TextEditChangeGroups which let the user turn changes on or off
// individually. This must be done after the change.setEdit() call above.
for (TextEditGroup editGroup : astEditGroups) {
change.addTextEditChangeGroup(new TextEditChangeGroup(change, editGroup));
}
-
+
changes.add(change);
}
-
+
// TODO to modify another Java source, loop back to the creation of the
// TextFileChange and accumulate in changes. Right now only one source is
// modified.
-
+
+ subMonitor.worked(1);
+
if (changes.size() > 0) {
return changes;
}
- subMonitor.worked(1);
-
} catch (CoreException e) {
// ImportRewrite.rewriteImports failed.
status.addFatalError(e.getMessage());
@@ -895,12 +1355,12 @@ public class ExtractStringRefactoring extends Refactoring {
@Override
public boolean visit(StringLiteral node) {
if (node.getLiteralValue().equals(mOldString)) {
-
+
Name qualifierName = mAst.newName(mRQualifier + ".string"); //$NON-NLS-1$
SimpleName idName = mAst.newSimpleName(mXmlId);
QualifiedName newNode = mAst.newQualifiedName(qualifierName, idName);
-
- TextEditGroup editGroup = new TextEditGroup("Replace string by ID");
+
+ TextEditGroup editGroup = new TextEditGroup("Replace string by ID");
mEditGroups.add(editGroup);
mRewriter.replace(node, newNode, editGroup);
}
@@ -910,11 +1370,11 @@ public class ExtractStringRefactoring extends Refactoring {
/**
* Step 3 of 3 of the refactoring: returns the {@link Change} that will be able to do the
- * work and creates a descriptor that can be used to replay that refactoring later.
- *
+ * work and creates a descriptor that can be used to replay that refactoring later.
+ *
* @see org.eclipse.ltk.core.refactoring.Refactoring#createChange(org.eclipse.core.runtime.IProgressMonitor)
- *
- * @throws CoreException
+ *
+ * @throws CoreException
*/
@Override
public Change createChange(IProgressMonitor monitor)
@@ -922,7 +1382,7 @@ public class ExtractStringRefactoring extends Refactoring {
try {
monitor.beginTask("Applying changes...", 1);
-
+
CompositeChange change = new CompositeChange(
getName(),
mChanges.toArray(new Change[mChanges.size()])) {
@@ -933,25 +1393,25 @@ public class ExtractStringRefactoring extends Refactoring {
"Extracts string '%1$s' into R.string.%2$s",
mTokenString,
mXmlStringId);
-
+
ExtractStringDescriptor desc = new ExtractStringDescriptor(
mProject.getName(), //project
comment, //description
comment, //comment
createArgumentMap());
-
+
return new RefactoringChangeDescriptor(desc);
}
};
-
+
monitor.worked(1);
-
+
return change;
-
+
} finally {
monitor.done();
}
-
+
}
/**
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/FolderConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/FolderConfiguration.java
index b589d26..aea146b 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/FolderConfiguration.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/FolderConfiguration.java
@@ -366,7 +366,7 @@ public final class FolderConfiguration implements Comparable<FolderConfiguration
}
}
- return result.toString();
+ return result == null ? null : result.toString();
}
public int compareTo(FolderConfiguration folderConfig) {
diff --git a/eclipse/scripts/create_all_symlinks.sh b/eclipse/scripts/create_all_symlinks.sh
index 8508343..dcbc5b3 100755
--- a/eclipse/scripts/create_all_symlinks.sh
+++ b/eclipse/scripts/create_all_symlinks.sh
@@ -1,5 +1,14 @@
#!/bin/bash
+HOST=`uname`
+if [ "${HOST:0:6}" == "CYGWIN" ]; then
+ if [ "x$1" == "x" ] || [ `basename "$1"` != "layoutlib.jar" ]; then
+ echo "Usage: $0 sdk/platforms/xxx/data/layoutlib.jar"
+ echo "Argument 1 should be the path to the layoutlib.jar that should be updated by create_bridge_symlinks.sh."
+ exit 1
+ fi
+fi
+
echo "### $0 executing"
function die() {