/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/org/documents/epl-v10.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ide.eclipse.adt.internal.refactorings.core; import static com.android.SdkConstants.ANDROID_PREFIX; import static com.android.SdkConstants.ANDROID_THEME_PREFIX; import static com.android.SdkConstants.ATTR_NAME; import static com.android.SdkConstants.ATTR_TYPE; import static com.android.SdkConstants.TAG_ITEM; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.Hyperlinks; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; import com.android.resources.ResourceType; import com.android.utils.Pair; import org.eclipse.core.resources.IProject; import org.eclipse.jface.action.Action; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.ITextSelection; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IFileEditorInput; import org.eclipse.ui.texteditor.IDocumentProvider; import org.eclipse.ui.texteditor.ITextEditor; import org.eclipse.ui.texteditor.ITextEditorExtension; import org.eclipse.ui.texteditor.ITextEditorExtension2; import org.w3c.dom.Element; import org.w3c.dom.Node; /** * Text action for XML files to invoke resource renaming * <p> * TODO: Handle other types of renaming: invoking class renaming when editing * class names in layout files and manifest files, renaming attribute names when * editing a styleable attribute, etc. */ public final class RenameResourceXmlTextAction extends Action { private final ITextEditor mEditor; /** * Creates a new {@linkplain RenameResourceXmlTextAction} * * @param editor the associated editor */ public RenameResourceXmlTextAction(@NonNull ITextEditor editor) { super("Rename"); mEditor = editor; } @Override public void run() { if (!validateEditorInputState()) { return; } IDocument document = getDocument(); if (document == null) { return; } ITextSelection selection = getSelection(); if (selection == null) { return; } Pair<ResourceType, String> resource = findResource(document, selection.getOffset()); if (resource == null) { resource = findItemDefinition(document, selection.getOffset()); } if (resource != null) { ResourceType type = resource.getFirst(); String name = resource.getSecond(); Shell shell = mEditor.getSite().getShell(); boolean canClear = false; IEditorInput input = mEditor.getEditorInput(); if (input instanceof IFileEditorInput) { IFileEditorInput fileInput = (IFileEditorInput) input; IProject project = fileInput.getFile().getProject(); RenameResourceWizard.renameResource(shell, project, type, name, null, canClear); return; } } // Fallback: tell user the cursor isn't in the right place MessageDialog.openInformation(mEditor.getSite().getShell(), "Rename", "Operation unavailable on the current selection.\n" + "Select an Android resource name."); } private boolean validateEditorInputState() { if (mEditor instanceof ITextEditorExtension2) return ((ITextEditorExtension2) mEditor).validateEditorInputState(); else if (mEditor instanceof ITextEditorExtension) return !((ITextEditorExtension) mEditor).isEditorInputReadOnly(); else if (mEditor != null) return mEditor.isEditable(); else return false; } /** * Searches for a resource URL around the caret, such as {@code @string/foo} * * @param document the document to search in * @param offset the offset to search at * @return a resource pair, or null if not found */ @Nullable public static Pair<ResourceType,String> findResource(@NonNull IDocument document, int offset) { try { int max = document.getLength(); if (offset >= max) { offset = max - 1; } else if (offset < 0) { offset = 0; } else if (offset > 0) { // If the caret is right after a resource name (meaning getChar(offset) points // to the following character), back up char c = document.getChar(offset); if (!isValidResourceNameChar(c)) { offset--; } } int start = offset; boolean valid = true; for (; start >= 0; start--) { char c = document.getChar(start); if (c == '@' || c == '?') { break; } else if (!isValidResourceNameChar(c)) { valid = false; break; } } if (valid) { // Search forwards for the end int end = start + 1; for (; end < max; end++) { char c = document.getChar(end); if (!isValidResourceNameChar(c)) { break; } } if (end > start + 1) { String url = document.get(start, end - start); // Don't allow renaming framework resources -- @android:string/ok etc if (url.startsWith(ANDROID_PREFIX) || url.startsWith(ANDROID_THEME_PREFIX)) { return null; } return Hyperlinks.parseResource(url); } } } catch (BadLocationException e) { AdtPlugin.log(e, null); } return null; } private static boolean isValidResourceNameChar(char c) { return c == '@' || c == '?' || c == '/' || c == '+' || Character.isJavaIdentifierPart(c); } /** * Searches for an item definition around the caret, such as * {@code <string name="foo">My String</string>} */ private Pair<ResourceType, String> findItemDefinition(IDocument document, int offset) { Node node = DomUtilities.getNode(document, offset); if (node == null) { return null; } if (node.getNodeType() == Node.TEXT_NODE) { node = node.getParentNode(); } if (node == null || node.getNodeType() != Node.ELEMENT_NODE) { return null; } Element element = (Element) node; String name = element.getAttribute(ATTR_NAME); if (name == null || name.isEmpty()) { return null; } String typeString = element.getTagName(); if (TAG_ITEM.equals(typeString)) { typeString = element.getAttribute(ATTR_TYPE); if (typeString == null || typeString.isEmpty()) { return null; } } ResourceType type = ResourceType.getEnum(typeString); if (type != null) { return Pair.of(type, name); } return null; } private ITextSelection getSelection() { ISelectionProvider selectionProvider = mEditor.getSelectionProvider(); if (selectionProvider == null) { return null; } ISelection selection = selectionProvider.getSelection(); if (!(selection instanceof ITextSelection)) { return null; } return (ITextSelection) selection; } private IDocument getDocument() { IDocumentProvider documentProvider = mEditor.getDocumentProvider(); if (documentProvider == null) { return null; } IDocument document = documentProvider.getDocument(mEditor.getEditorInput()); if (document == null) { return null; } return document; } }