• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
RenameResourceXmlTextAction(@onNull ITextEditor editor)83     public RenameResourceXmlTextAction(@NonNull ITextEditor editor) {
84         super("Rename");
85         mEditor = editor;
86     }
87 
88     @Override
run()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 
validateEditorInputState()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
findResource(@onNull IDocument document, int offset)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 
isValidResourceNameChar(char c)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      */
findItemDefinition(IDocument document, int offset)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
findClassName( @onNull IDocument document, @Nullable IFile file, int offset)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
findType(@onNull String className, @NonNull IProject project)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 
getSelection()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 
getDocument()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
getFile()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