ADT: Extract String IDs from Layout XML strings.
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,6 +565,11 @@ public abstract class AndroidEditor extends FormEditor implements IResourceChang
|
||||
* 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() {
|
||||
|
||||
@@ -32,6 +32,8 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -66,6 +68,12 @@ public final class ReferenceAttributeDescriptor extends TextAttributeDescriptor
|
||||
}
|
||||
|
||||
|
||||
/** 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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
@@ -107,7 +108,8 @@ public class ExtractStringAction implements IWorkbenchWindowActionDelegate {
|
||||
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 {
|
||||
@@ -130,6 +132,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.
|
||||
@@ -138,12 +155,7 @@ 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();
|
||||
private IFile getSelectedFile(IEditorPart editor) {
|
||||
if (editor != null) {
|
||||
IEditorInput input = editor.getEditorInput();
|
||||
|
||||
@@ -162,8 +174,6 @@ public class ExtractStringAction implements IWorkbenchWindowActionDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
@@ -142,9 +155,14 @@ public class ExtractStringRefactoring extends Refactoring {
|
||||
|
||||
/** 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,9 +198,9 @@ 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));
|
||||
@@ -195,11 +213,15 @@ 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() {
|
||||
@@ -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;
|
||||
}
|
||||
@@ -222,11 +245,13 @@ public class ExtractStringRefactoring extends Refactoring {
|
||||
* 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);
|
||||
@@ -243,6 +268,7 @@ 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;
|
||||
}
|
||||
@@ -304,10 +330,10 @@ public class ExtractStringRefactoring extends Refactoring {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -334,11 +360,36 @@ public class ExtractStringRefactoring extends Refactoring {
|
||||
// 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();
|
||||
}
|
||||
@@ -353,6 +404,7 @@ public class ExtractStringRefactoring extends Refactoring {
|
||||
* 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());
|
||||
}
|
||||
@@ -412,6 +464,205 @@ public class ExtractStringRefactoring extends Refactoring {
|
||||
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()
|
||||
@@ -514,9 +765,18 @@ public class ExtractStringRefactoring extends Refactoring {
|
||||
}
|
||||
|
||||
if (mMode == Mode.EDIT_SOURCE) {
|
||||
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
|
||||
List<Change> changes = computeJavaChanges(mUnit, mXmlStringId, mTokenString,
|
||||
changes = computeJavaChanges(mUnit, mXmlStringId, mTokenString,
|
||||
status, SubMonitor.convert(monitor, 1));
|
||||
}
|
||||
if (changes != null) {
|
||||
mChanges.addAll(changes);
|
||||
}
|
||||
@@ -634,7 +894,7 @@ 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.
|
||||
@@ -730,6 +990,205 @@ public class ExtractStringRefactoring extends Refactoring {
|
||||
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}.
|
||||
*/
|
||||
@@ -766,7 +1225,8 @@ public class ExtractStringRefactoring extends Refactoring {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -856,12 +1316,12 @@ public class ExtractStringRefactoring extends Refactoring {
|
||||
// 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());
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user