diff options
author | Raphael <raphael@google.com> | 2009-07-10 19:49:31 -0400 |
---|---|---|
committer | Raphael <raphael@google.com> | 2009-07-11 15:22:37 -0400 |
commit | 6331632428c680ffce4656bd245d5674ac03e0ff (patch) | |
tree | 397ee7c8b637cb0ef13f78fd37ffd5d0e425d85d /eclipse | |
parent | 7ed0334797902bcac80baa7032f93b0f9e79cb14 (diff) | |
download | sdk-6331632428c680ffce4656bd245d5674ac03e0ff.zip sdk-6331632428c680ffce4656bd245d5674ac03e0ff.tar.gz sdk-6331632428c680ffce4656bd245d5674ac03e0ff.tar.bz2 |
ADT: Extract String IDs from Layout XML strings.
Diffstat (limited to 'eclipse')
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 ' or " 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("\"", """); //$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() { |