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