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