blob: 8ecb088364f22590fc90cf046c3cc0c244ecea33 [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.eclipse.org/org/documents/epl-v10.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ide.eclipse.adt.internal.refactorings.core;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.ANDROID_PREFIX;
import static com.android.SdkConstants.ANDROID_THEME_PREFIX;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_TYPE;
import static com.android.SdkConstants.TAG_ITEM;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.resources.ResourceUrl;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.resources.ResourceType;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.internal.corext.refactoring.rename.RenameTypeProcessor;
import org.eclipse.jdt.internal.ui.refactoring.reorg.RenameTypeWizard;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring;
import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.ui.texteditor.ITextEditorExtension;
import org.eclipse.ui.texteditor.ITextEditorExtension2;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.util.List;
/**
* Text action for XML files to invoke renaming
* <p>
* TODO: Handle other types of renaming: invoking class renaming when editing
* class names in layout files and manifest files, renaming attribute names when
* editing a styleable attribute, etc.
*/
@SuppressWarnings("restriction") // Java rename refactoring
public final class RenameResourceXmlTextAction extends Action {
private final ITextEditor mEditor;
/**
* Creates a new {@linkplain RenameResourceXmlTextAction}
*
* @param editor the associated editor
*/
public RenameResourceXmlTextAction(@NonNull ITextEditor editor) {
super("Rename");
mEditor = editor;
}
@Override
public void run() {
if (!validateEditorInputState()) {
return;
}
IFile file = getFile();
if (file == null) {
return;
}
IProject project = file.getProject();
if (project == null) {
return;
}
IDocument document = getDocument();
if (document == null) {
return;
}
ITextSelection selection = getSelection();
if (selection == null) {
return;
}
ResourceUrl resource = findResource(document, selection.getOffset());
if (resource == null) {
resource = findItemDefinition(document, selection.getOffset());
}
if (resource != null) {
ResourceType type = resource.type;
String name = resource.name;
Shell shell = mEditor.getSite().getShell();
boolean canClear = false;
RenameResourceWizard.renameResource(shell, project, type, name, null, canClear);
return;
}
String className = findClassName(document, file, selection.getOffset());
if (className != null) {
assert className.equals(className.trim());
IType type = findType(className, project);
if (type != null) {
RenameTypeProcessor processor = new RenameTypeProcessor(type);
//processor.setNewElementName(className);
processor.setUpdateQualifiedNames(true);
processor.setUpdateSimilarDeclarations(false);
//processor.setMatchStrategy(?);
//processor.setFilePatterns(patterns);
processor.setUpdateReferences(true);
RenameRefactoring refactoring = new RenameRefactoring(processor);
RenameTypeWizard wizard = new RenameTypeWizard(refactoring);
RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
try {
IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
op.run(window.getShell(), wizard.getDefaultPageTitle());
} catch (InterruptedException e) {
}
}
return;
}
// Fallback: tell user the cursor isn't in the right place
MessageDialog.openInformation(mEditor.getSite().getShell(),
"Rename",
"Operation unavailable on the current selection.\n"
+ "Select an Android resource name or class.");
}
private boolean validateEditorInputState() {
if (mEditor instanceof ITextEditorExtension2)
return ((ITextEditorExtension2) mEditor).validateEditorInputState();
else if (mEditor instanceof ITextEditorExtension)
return !((ITextEditorExtension) mEditor).isEditorInputReadOnly();
else if (mEditor != null)
return mEditor.isEditable();
else
return false;
}
/**
* Searches for a resource URL around the caret, such as {@code @string/foo}
*
* @param document the document to search in
* @param offset the offset to search at
* @return a resource pair, or null if not found
*/
@Nullable
public static ResourceUrl findResource(@NonNull IDocument document, int offset) {
try {
int max = document.getLength();
if (offset >= max) {
offset = max - 1;
} else if (offset < 0) {
offset = 0;
} else if (offset > 0) {
// If the caret is right after a resource name (meaning getChar(offset) points
// to the following character), back up
char c = document.getChar(offset);
if (!isValidResourceNameChar(c)) {
offset--;
}
}
int start = offset;
boolean valid = true;
for (; start >= 0; start--) {
char c = document.getChar(start);
if (c == '@' || c == '?') {
break;
} else if (!isValidResourceNameChar(c)) {
valid = false;
break;
}
}
if (valid) {
// Search forwards for the end
int end = start + 1;
for (; end < max; end++) {
char c = document.getChar(end);
if (!isValidResourceNameChar(c)) {
break;
}
}
if (end > start + 1) {
String url = document.get(start, end - start);
// Don't allow renaming framework resources -- @android:string/ok etc
if (url.startsWith(ANDROID_PREFIX) || url.startsWith(ANDROID_THEME_PREFIX)) {
return null;
}
return ResourceUrl.parse(url);
}
}
} catch (BadLocationException e) {
AdtPlugin.log(e, null);
}
return null;
}
private static boolean isValidResourceNameChar(char c) {
return c == '@' || c == '?' || c == '/' || c == '+' || Character.isJavaIdentifierPart(c);
}
/**
* Searches for an item definition around the caret, such as
* {@code <string name="foo">My String</string>}
*/
private ResourceUrl findItemDefinition(IDocument document, int offset) {
Node node = DomUtilities.getNode(document, offset);
if (node == null) {
return null;
}
if (node.getNodeType() == Node.TEXT_NODE) {
node = node.getParentNode();
}
if (node == null || node.getNodeType() != Node.ELEMENT_NODE) {
return null;
}
Element element = (Element) node;
String name = element.getAttribute(ATTR_NAME);
if (name == null || name.isEmpty()) {
return null;
}
String typeString = element.getTagName();
if (TAG_ITEM.equals(typeString)) {
typeString = element.getAttribute(ATTR_TYPE);
if (typeString == null || typeString.isEmpty()) {
return null;
}
}
ResourceType type = ResourceType.getEnum(typeString);
if (type != null) {
return ResourceUrl.create(type, name, false, false);
}
return null;
}
/**
* Searches for a fully qualified class name around the caret, such as {@code foo.bar.MyClass}
*
* @param document the document to search in
* @param file the file, if known
* @param offset the offset to search at
* @return a resource pair, or null if not found
*/
@Nullable
public static String findClassName(
@NonNull IDocument document,
@Nullable IFile file,
int offset) {
try {
int max = document.getLength();
if (offset >= max) {
offset = max - 1;
} else if (offset < 0) {
offset = 0;
} else if (offset > 0) {
// If the caret is right after a resource name (meaning getChar(offset) points
// to the following character), back up
char c = document.getChar(offset);
if (Character.isJavaIdentifierPart(c)) {
offset--;
}
}
int start = offset;
for (; start >= 0; start--) {
char c = document.getChar(start);
if (c == '"' || c == '<' || c == '/') {
start++;
break;
} else if (c != '.' && !Character.isJavaIdentifierPart(c)) {
return null;
}
}
// Search forwards for the end
int end = start + 1;
for (; end < max; end++) {
char c = document.getChar(end);
if (c != '.' && !Character.isJavaIdentifierPart(c)) {
if (c != '"' && c != '>' && !Character.isWhitespace(c)) {
return null;
}
break;
}
}
if (end > start + 1) {
String fqcn = document.get(start, end - start);
int dot = fqcn.indexOf('.');
if (dot == -1) { // Only support fully qualified names
return null;
}
if (dot == 0) { // Special case for manifests: prepend package
if (file != null && file.getName().equals(ANDROID_MANIFEST_XML)) {
ManifestInfo info = ManifestInfo.get(file.getProject());
return info.getPackage() + fqcn;
}
return null;
}
return fqcn;
}
} catch (BadLocationException e) {
AdtPlugin.log(e, null);
}
return null;
}
@Nullable
private IType findType(@NonNull String className, @NonNull IProject project) {
IType type = null;
try {
IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
type = javaProject.findType(className);
if (type == null || !type.exists()) {
return null;
}
if (!type.isBinary()) {
return type;
}
// See if this class is coming through a library project jar file and
// if so locate the real class
ProjectState projectState = Sdk.getProjectState(project);
if (projectState != null) {
List<IProject> libraries = projectState.getFullLibraryProjects();
for (IProject library : libraries) {
javaProject = BaseProjectHelper.getJavaProject(library);
type = javaProject.findType(className);
if (type != null && type.exists() && !type.isBinary()) {
return type;
}
}
}
} catch (CoreException e) {
AdtPlugin.log(e, null);
}
return null;
}
private ITextSelection getSelection() {
ISelectionProvider selectionProvider = mEditor.getSelectionProvider();
if (selectionProvider == null) {
return null;
}
ISelection selection = selectionProvider.getSelection();
if (!(selection instanceof ITextSelection)) {
return null;
}
return (ITextSelection) selection;
}
private IDocument getDocument() {
IDocumentProvider documentProvider = mEditor.getDocumentProvider();
if (documentProvider == null) {
return null;
}
IDocument document = documentProvider.getDocument(mEditor.getEditorInput());
if (document == null) {
return null;
}
return document;
}
@Nullable
private IFile getFile() {
IEditorInput input = mEditor.getEditorInput();
if (input instanceof IFileEditorInput) {
IFileEditorInput fileInput = (IFileEditorInput) input;
return fileInput.getFile();
}
return null;
}
}