Home | History | Annotate | Download | only in core
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.refactorings.core;
     18 
     19 import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
     20 import static com.android.SdkConstants.ANDROID_PREFIX;
     21 import static com.android.SdkConstants.ANDROID_THEME_PREFIX;
     22 import static com.android.SdkConstants.ATTR_NAME;
     23 import static com.android.SdkConstants.ATTR_TYPE;
     24 import static com.android.SdkConstants.TAG_ITEM;
     25 
     26 import com.android.annotations.NonNull;
     27 import com.android.annotations.Nullable;
     28 import com.android.ide.common.resources.ResourceUrl;
     29 import com.android.ide.eclipse.adt.AdtPlugin;
     30 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
     31 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
     32 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
     33 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
     34 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     35 import com.android.resources.ResourceType;
     36 
     37 import org.eclipse.core.resources.IFile;
     38 import org.eclipse.core.resources.IProject;
     39 import org.eclipse.core.runtime.CoreException;
     40 import org.eclipse.jdt.core.IJavaProject;
     41 import org.eclipse.jdt.core.IType;
     42 import org.eclipse.jdt.internal.corext.refactoring.rename.RenameTypeProcessor;
     43 import org.eclipse.jdt.internal.ui.refactoring.reorg.RenameTypeWizard;
     44 import org.eclipse.jface.action.Action;
     45 import org.eclipse.jface.dialogs.MessageDialog;
     46 import org.eclipse.jface.text.BadLocationException;
     47 import org.eclipse.jface.text.IDocument;
     48 import org.eclipse.jface.text.ITextSelection;
     49 import org.eclipse.jface.viewers.ISelection;
     50 import org.eclipse.jface.viewers.ISelectionProvider;
     51 import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring;
     52 import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
     53 import org.eclipse.swt.widgets.Shell;
     54 import org.eclipse.ui.IEditorInput;
     55 import org.eclipse.ui.IFileEditorInput;
     56 import org.eclipse.ui.IWorkbenchWindow;
     57 import org.eclipse.ui.PlatformUI;
     58 import org.eclipse.ui.texteditor.IDocumentProvider;
     59 import org.eclipse.ui.texteditor.ITextEditor;
     60 import org.eclipse.ui.texteditor.ITextEditorExtension;
     61 import org.eclipse.ui.texteditor.ITextEditorExtension2;
     62 import org.w3c.dom.Element;
     63 import org.w3c.dom.Node;
     64 
     65 import java.util.List;
     66 
     67 /**
     68  * Text action for XML files to invoke renaming
     69  * <p>
     70  * TODO: Handle other types of renaming: invoking class renaming when editing
     71  * class names in layout files and manifest files, renaming attribute names when
     72  * editing a styleable attribute, etc.
     73  */
     74 @SuppressWarnings("restriction") // Java rename refactoring
     75 public final class RenameResourceXmlTextAction extends Action {
     76     private final ITextEditor mEditor;
     77 
     78     /**
     79      * Creates a new {@linkplain RenameResourceXmlTextAction}
     80      *
     81      * @param editor the associated editor
     82      */
     83     public RenameResourceXmlTextAction(@NonNull ITextEditor editor) {
     84         super("Rename");
     85         mEditor = editor;
     86     }
     87 
     88     @Override
     89     public void run() {
     90         if (!validateEditorInputState()) {
     91             return;
     92         }
     93         IFile file = getFile();
     94         if (file == null) {
     95             return;
     96         }
     97         IProject project = file.getProject();
     98         if (project == null) {
     99             return;
    100         }
    101         IDocument document = getDocument();
    102         if (document == null) {
    103             return;
    104         }
    105         ITextSelection selection = getSelection();
    106         if (selection == null) {
    107             return;
    108         }
    109 
    110         ResourceUrl resource = findResource(document, selection.getOffset());
    111 
    112         if (resource == null) {
    113             resource = findItemDefinition(document, selection.getOffset());
    114         }
    115 
    116         if (resource != null) {
    117             ResourceType type = resource.type;
    118             String name = resource.name;
    119             Shell shell = mEditor.getSite().getShell();
    120             boolean canClear = false;
    121 
    122             RenameResourceWizard.renameResource(shell, project, type, name, null, canClear);
    123             return;
    124         }
    125 
    126         String className = findClassName(document, file, selection.getOffset());
    127         if (className != null) {
    128             assert className.equals(className.trim());
    129             IType type = findType(className, project);
    130             if (type != null) {
    131                 RenameTypeProcessor processor = new RenameTypeProcessor(type);
    132                 //processor.setNewElementName(className);
    133                 processor.setUpdateQualifiedNames(true);
    134                 processor.setUpdateSimilarDeclarations(false);
    135                 //processor.setMatchStrategy(?);
    136                 //processor.setFilePatterns(patterns);
    137                 processor.setUpdateReferences(true);
    138 
    139                 RenameRefactoring refactoring = new RenameRefactoring(processor);
    140                 RenameTypeWizard wizard = new RenameTypeWizard(refactoring);
    141                 RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
    142                 try {
    143                     IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
    144                     op.run(window.getShell(), wizard.getDefaultPageTitle());
    145                 } catch (InterruptedException e) {
    146                 }
    147             }
    148 
    149             return;
    150         }
    151 
    152         // Fallback: tell user the cursor isn't in the right place
    153         MessageDialog.openInformation(mEditor.getSite().getShell(),
    154                 "Rename",
    155                 "Operation unavailable on the current selection.\n"
    156                         + "Select an Android resource name or class.");
    157     }
    158 
    159     private boolean validateEditorInputState() {
    160         if (mEditor instanceof ITextEditorExtension2)
    161             return ((ITextEditorExtension2) mEditor).validateEditorInputState();
    162         else if (mEditor instanceof ITextEditorExtension)
    163             return !((ITextEditorExtension) mEditor).isEditorInputReadOnly();
    164         else if (mEditor != null)
    165             return mEditor.isEditable();
    166         else
    167             return false;
    168     }
    169 
    170     /**
    171      * Searches for a resource URL around the caret, such as {@code @string/foo}
    172      *
    173      * @param document the document to search in
    174      * @param offset the offset to search at
    175      * @return a resource pair, or null if not found
    176      */
    177     @Nullable
    178     public static ResourceUrl findResource(@NonNull IDocument document, int offset) {
    179         try {
    180             int max = document.getLength();
    181             if (offset >= max) {
    182                 offset = max - 1;
    183             } else if (offset < 0) {
    184                 offset = 0;
    185             } else if (offset > 0) {
    186                 // If the caret is right after a resource name (meaning getChar(offset) points
    187                 // to the following character), back up
    188                 char c = document.getChar(offset);
    189                 if (!isValidResourceNameChar(c)) {
    190                     offset--;
    191                 }
    192             }
    193 
    194             int start = offset;
    195             boolean valid = true;
    196             for (; start >= 0; start--) {
    197                 char c = document.getChar(start);
    198                 if (c == '@' || c == '?') {
    199                     break;
    200                 } else if (!isValidResourceNameChar(c)) {
    201                     valid = false;
    202                     break;
    203                 }
    204             }
    205             if (valid) {
    206                 // Search forwards for the end
    207                 int end = start + 1;
    208                 for (; end < max; end++) {
    209                     char c = document.getChar(end);
    210                     if (!isValidResourceNameChar(c)) {
    211                         break;
    212                     }
    213                 }
    214                 if (end > start + 1) {
    215                     String url = document.get(start, end - start);
    216 
    217                     // Don't allow renaming framework resources -- @android:string/ok etc
    218                     if (url.startsWith(ANDROID_PREFIX) || url.startsWith(ANDROID_THEME_PREFIX)) {
    219                         return null;
    220                     }
    221 
    222                     return ResourceUrl.parse(url);
    223                 }
    224             }
    225         } catch (BadLocationException e) {
    226             AdtPlugin.log(e, null);
    227         }
    228 
    229         return null;
    230     }
    231 
    232     private static boolean isValidResourceNameChar(char c) {
    233         return c == '@' || c == '?' || c == '/' || c == '+' || Character.isJavaIdentifierPart(c);
    234     }
    235 
    236     /**
    237      * Searches for an item definition around the caret, such as
    238      * {@code   <string name="foo">My String</string>}
    239      */
    240     private ResourceUrl findItemDefinition(IDocument document, int offset) {
    241         Node node = DomUtilities.getNode(document, offset);
    242         if (node == null) {
    243             return null;
    244         }
    245         if (node.getNodeType() == Node.TEXT_NODE) {
    246             node = node.getParentNode();
    247         }
    248         if (node == null || node.getNodeType() != Node.ELEMENT_NODE) {
    249             return null;
    250         }
    251 
    252         Element element = (Element) node;
    253         String name = element.getAttribute(ATTR_NAME);
    254         if (name == null || name.isEmpty()) {
    255             return null;
    256         }
    257         String typeString = element.getTagName();
    258         if (TAG_ITEM.equals(typeString)) {
    259             typeString = element.getAttribute(ATTR_TYPE);
    260             if (typeString == null || typeString.isEmpty()) {
    261                 return null;
    262             }
    263         }
    264         ResourceType type = ResourceType.getEnum(typeString);
    265         if (type != null) {
    266             return ResourceUrl.create(type, name, false, false);
    267         }
    268 
    269         return null;
    270     }
    271 
    272     /**
    273      * Searches for a fully qualified class name around the caret, such as {@code foo.bar.MyClass}
    274      *
    275      * @param document the document to search in
    276      * @param file the file, if known
    277      * @param offset the offset to search at
    278      * @return a resource pair, or null if not found
    279      */
    280     @Nullable
    281     public static String findClassName(
    282             @NonNull IDocument document,
    283             @Nullable IFile file,
    284             int offset) {
    285         try {
    286             int max = document.getLength();
    287             if (offset >= max) {
    288                 offset = max - 1;
    289             } else if (offset < 0) {
    290                 offset = 0;
    291             } else if (offset > 0) {
    292                 // If the caret is right after a resource name (meaning getChar(offset) points
    293                 // to the following character), back up
    294                 char c = document.getChar(offset);
    295                 if (Character.isJavaIdentifierPart(c)) {
    296                     offset--;
    297                 }
    298             }
    299 
    300             int start = offset;
    301             for (; start >= 0; start--) {
    302                 char c = document.getChar(start);
    303                 if (c == '"' || c == '<' || c == '/') {
    304                     start++;
    305                     break;
    306                 } else if (c != '.' && !Character.isJavaIdentifierPart(c)) {
    307                     return null;
    308                 }
    309             }
    310             // Search forwards for the end
    311             int end = start + 1;
    312             for (; end < max; end++) {
    313                 char c = document.getChar(end);
    314                 if (c != '.' && !Character.isJavaIdentifierPart(c)) {
    315                     if (c != '"' && c != '>' && !Character.isWhitespace(c)) {
    316                         return null;
    317                     }
    318                     break;
    319                 }
    320             }
    321             if (end > start + 1) {
    322                 String fqcn = document.get(start, end - start);
    323                 int dot = fqcn.indexOf('.');
    324                 if (dot == -1) { // Only support fully qualified names
    325                     return null;
    326                 }
    327                 if (dot == 0) { // Special case for manifests: prepend package
    328                     if (file != null && file.getName().equals(ANDROID_MANIFEST_XML)) {
    329                         ManifestInfo info = ManifestInfo.get(file.getProject());
    330                         return info.getPackage() + fqcn;
    331                     }
    332                     return null;
    333                 }
    334 
    335                 return fqcn;
    336             }
    337         } catch (BadLocationException e) {
    338             AdtPlugin.log(e, null);
    339         }
    340 
    341         return null;
    342     }
    343 
    344     @Nullable
    345     private IType findType(@NonNull String className, @NonNull IProject project) {
    346         IType type = null;
    347         try {
    348             IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
    349             type = javaProject.findType(className);
    350             if (type == null || !type.exists()) {
    351                 return null;
    352             }
    353             if (!type.isBinary()) {
    354                 return type;
    355             }
    356             // See if this class is coming through a library project jar file and
    357             // if so locate the real class
    358             ProjectState projectState = Sdk.getProjectState(project);
    359             if (projectState != null) {
    360                 List<IProject> libraries = projectState.getFullLibraryProjects();
    361                 for (IProject library : libraries) {
    362                     javaProject = BaseProjectHelper.getJavaProject(library);
    363                     type = javaProject.findType(className);
    364                     if (type != null && type.exists() && !type.isBinary()) {
    365                         return type;
    366                     }
    367                 }
    368             }
    369         } catch (CoreException e) {
    370             AdtPlugin.log(e, null);
    371         }
    372 
    373         return null;
    374     }
    375 
    376     private ITextSelection getSelection() {
    377         ISelectionProvider selectionProvider = mEditor.getSelectionProvider();
    378         if (selectionProvider == null) {
    379             return null;
    380         }
    381         ISelection selection = selectionProvider.getSelection();
    382         if (!(selection instanceof ITextSelection)) {
    383             return null;
    384         }
    385         return (ITextSelection) selection;
    386     }
    387 
    388     private IDocument getDocument() {
    389         IDocumentProvider documentProvider = mEditor.getDocumentProvider();
    390         if (documentProvider == null) {
    391             return null;
    392         }
    393         IDocument document = documentProvider.getDocument(mEditor.getEditorInput());
    394         if (document == null) {
    395             return null;
    396         }
    397         return document;
    398     }
    399 
    400     @Nullable
    401     private IFile getFile() {
    402         IEditorInput input = mEditor.getEditorInput();
    403         if (input instanceof IFileEditorInput) {
    404             IFileEditorInput fileInput = (IFileEditorInput) input;
    405             return fileInput.getFile();
    406         }
    407 
    408         return null;
    409     }
    410 }
    411