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