blob: 3db380832db4d0ce9d9e8a5c69e7988d7d86c6f2 [file] [log] [blame]
/*
* Copyright (C) 2011 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.build;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.XMLNS_ANDROID;
import static com.android.SdkConstants.XMLNS_URI;
import com.android.ide.common.resources.ResourceUrl;
import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AdtUtils;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
import com.android.resources.ResourceType;
import com.android.utils.Pair;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext;
import org.eclipse.jface.text.quickassist.IQuickAssistProcessor;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.ui.IMarkerResolution;
import org.eclipse.ui.IMarkerResolution2;
import org.eclipse.ui.IMarkerResolutionGenerator2;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.editors.text.TextFileDocumentProvider;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.util.List;
/**
* Shared handler for both quick assist processors (Control key handler) and quick fix
* marker resolution (Problem view handling), since there is a lot of overlap between
* these two UI handlers.
*/
@SuppressWarnings("restriction") // XML model
public class AaptQuickFix implements IMarkerResolutionGenerator2, IQuickAssistProcessor {
public AaptQuickFix() {
}
/** Returns the error message from aapt that signals missing resources */
private static String getTargetMarkerErrorMessage() {
return "No resource found that matches the given name";
}
/** Returns the error message from aapt that signals a missing namespace declaration */
private static String getUnboundErrorMessage() {
return "Error parsing XML: unbound prefix";
}
// ---- Implements IMarkerResolution2 ----
@Override
public boolean hasResolutions(IMarker marker) {
String message = null;
try {
message = (String) marker.getAttribute(IMarker.MESSAGE);
} catch (CoreException e) {
AdtPlugin.log(e, null);
}
return message != null
&& (message.contains(getTargetMarkerErrorMessage())
|| message.contains(getUnboundErrorMessage()));
}
@Override
public IMarkerResolution[] getResolutions(IMarker marker) {
IResource markerResource = marker.getResource();
IProject project = markerResource.getProject();
try {
String message = (String) marker.getAttribute(IMarker.MESSAGE);
if (message.contains(getUnboundErrorMessage()) && markerResource instanceof IFile) {
return new IMarkerResolution[] {
new CreateNamespaceFix((IFile) markerResource)
};
}
} catch (CoreException e1) {
AdtPlugin.log(e1, null);
}
int start = marker.getAttribute(IMarker.CHAR_START, 0);
int end = marker.getAttribute(IMarker.CHAR_END, 0);
if (end > start) {
int length = end - start;
IDocumentProvider provider = new TextFileDocumentProvider();
try {
provider.connect(markerResource);
IDocument document = provider.getDocument(markerResource);
String resource = document.get(start, length);
if (ResourceHelper.canCreateResource(resource)) {
return new IMarkerResolution[] {
new CreateResourceProposal(project, resource)
};
}
} catch (Exception e) {
AdtPlugin.log(e, "Can't find range information for %1$s", markerResource);
} finally {
provider.disconnect(markerResource);
}
}
return null;
}
// ---- Implements IQuickAssistProcessor ----
@Override
public boolean canAssist(IQuickAssistInvocationContext invocationContext) {
return true;
}
@Override
public boolean canFix(Annotation annotation) {
return true;
}
@Override
public ICompletionProposal[] computeQuickAssistProposals(
IQuickAssistInvocationContext invocationContext) {
// We have to find the corresponding project/file (so we can look up the aapt
// error markers). Unfortunately, an IQuickAssistProcessor only gets
// access to an ISourceViewer which has no hooks back to the surrounding
// editor.
//
// However, the IQuickAssistProcessor will only be used interactively by a file
// being edited, so we can cheat like the hyperlink detector and simply
// look up the currently active file in the IDE. To be on the safe side,
// we'll make sure that that editor has the same sourceViewer such that
// we are indeed looking at the right file:
ISourceViewer sourceViewer = invocationContext.getSourceViewer();
AndroidXmlEditor editor = AndroidXmlEditor.fromTextViewer(sourceViewer);
if (editor != null) {
IFile file = editor.getInputFile();
if (file == null) {
return null;
}
IDocument document = sourceViewer.getDocument();
List<IMarker> markers = AdtUtils.findMarkersOnLine(AdtConstants.MARKER_AAPT_COMPILE,
file, document, invocationContext.getOffset());
try {
for (IMarker marker : markers) {
String message = marker.getAttribute(IMarker.MESSAGE, ""); //$NON-NLS-1$
if (message.contains(getTargetMarkerErrorMessage())) {
int start = marker.getAttribute(IMarker.CHAR_START, 0);
int end = marker.getAttribute(IMarker.CHAR_END, 0);
int length = end - start;
String resource = document.get(start, length);
// Can only offer create value for non-framework value
// resources
if (ResourceHelper.canCreateResource(resource)) {
IProject project = editor.getProject();
return new ICompletionProposal[] {
new CreateResourceProposal(project, resource)
};
}
} else if (message.contains(getUnboundErrorMessage())) {
return new ICompletionProposal[] {
new CreateNamespaceFix(null)
};
}
}
} catch (BadLocationException e) {
AdtPlugin.log(e, null);
}
}
return null;
}
@Override
public String getErrorMessage() {
return null;
}
/** Quick fix to insert namespace binding when missing */
private final static class CreateNamespaceFix
implements ICompletionProposal, IMarkerResolution2 {
private IFile mFile;
public CreateNamespaceFix(IFile file) {
mFile = file;
}
private IndexedRegion perform(IDocument doc) {
IModelManager manager = StructuredModelManager.getModelManager();
IStructuredModel model = manager.getExistingModelForEdit(doc);
if (model != null) {
try {
perform(model);
} finally {
model.releaseFromEdit();
}
}
return null;
}
private IndexedRegion perform(IFile file) {
IModelManager manager = StructuredModelManager.getModelManager();
IStructuredModel model;
try {
model = manager.getModelForEdit(file);
if (model != null) {
try {
perform(model);
} finally {
model.releaseFromEdit();
}
}
} catch (Exception e) {
AdtPlugin.log(e, "Can't look up XML model");
}
return null;
}
private IndexedRegion perform(IStructuredModel model) {
if (model instanceof IDOMModel) {
IDOMModel domModel = (IDOMModel) model;
Document document = domModel.getDocument();
Element element = document.getDocumentElement();
Attr attr = document.createAttributeNS(XMLNS_URI, XMLNS_ANDROID);
attr.setValue(ANDROID_URI);
element.getAttributes().setNamedItemNS(attr);
return (IndexedRegion) attr;
}
return null;
}
// ---- Implements ICompletionProposal ----
@Override
public void apply(IDocument document) {
perform(document);
}
@Override
public String getAdditionalProposalInfo() {
return "Adds an Android namespace declaratiopn to the root element.";
}
@Override
public IContextInformation getContextInformation() {
return null;
}
@Override
public String getDisplayString() {
return "Insert namespace binding";
}
@Override
public Image getImage() {
return AdtPlugin.getAndroidLogo();
}
@Override
public Point getSelection(IDocument doc) {
return null;
}
// ---- Implements MarkerResolution2 ----
@Override
public String getLabel() {
return getDisplayString();
}
@Override
public void run(IMarker marker) {
try {
AdtPlugin.openFile(mFile, null);
} catch (PartInitException e) {
AdtPlugin.log(e, "Can't open file %1$s", mFile.getName());
}
IndexedRegion indexedRegion = perform(mFile);
if (indexedRegion != null) {
try {
IRegion region =
new Region(indexedRegion.getStartOffset(), indexedRegion.getLength());
AdtPlugin.openFile(mFile, region);
} catch (PartInitException e) {
AdtPlugin.log(e, "Can't open file %1$s", mFile.getName());
}
}
}
@Override
public String getDescription() {
return getAdditionalProposalInfo();
}
}
private static class CreateResourceProposal
implements ICompletionProposal, IMarkerResolution2 {
private final IProject mProject;
private final String mResource;
CreateResourceProposal(IProject project, String resource) {
super();
mProject = project;
mResource = resource;
}
private void perform() {
ResourceUrl resource = ResourceUrl.parse(mResource);
if (resource == null) {
return;
}
ResourceType type = resource.type;
String name = resource.name;
assert !resource.framework;
String value = ""; //$NON-NLS-1$
// Try to pick a reasonable first guess. The new value will be highlighted and
// selected for editing, but if we have an initial value then the new file
// won't show an error.
switch (type) {
case STRING: value = "TODO"; break; //$NON-NLS-1$
case DIMEN: value = "1dp"; break; //$NON-NLS-1$
case BOOL: value = "true"; break; //$NON-NLS-1$
case COLOR: value = "#000000"; break; //$NON-NLS-1$
case INTEGER: value = "1"; break; //$NON-NLS-1$
case ARRAY: value = "<item>1</item>"; break; //$NON-NLS-1$
}
Pair<IFile, IRegion> location =
ResourceHelper.createResource(mProject, type, name, value);
if (location != null) {
IFile file = location.getFirst();
IRegion region = location.getSecond();
try {
AdtPlugin.openFile(file, region);
} catch (PartInitException e) {
AdtPlugin.log(e, "Can't open file %1$s", file.getName());
}
}
}
// ---- Implements ICompletionProposal ----
@Override
public void apply(IDocument document) {
perform();
}
@Override
public String getAdditionalProposalInfo() {
return "Creates an XML file entry for the given missing resource "
+ "and opens it in the editor.";
}
@Override
public IContextInformation getContextInformation() {
return null;
}
@Override
public String getDisplayString() {
return String.format("Create resource %1$s", mResource);
}
@Override
public Image getImage() {
return AdtPlugin.getAndroidLogo();
}
@Override
public Point getSelection(IDocument document) {
return null;
}
// ---- Implements MarkerResolution2 ----
@Override
public String getLabel() {
return getDisplayString();
}
@Override
public void run(IMarker marker) {
perform();
}
@Override
public String getDescription() {
return getAdditionalProposalInfo();
}
}
}