groovy
guava
hardcoded
+hardcodes
hotfix
href
http
placeholder
plugin
popup
+popups
pre
precompiler
pref
<super type="org.eclipse.core.resources.textmarker"/>
<persistent value="true"/>
</extension>
+ <extension point="org.eclipse.ui.ide.markerResolution">
+ <markerResolutionGenerator
+ markerType="com.android.ide.eclipse.common.aaptProblem"
+ class="com.android.ide.eclipse.adt.internal.build.AaptQuickFix"/>
+ </extension>
<extension
id="ResourceManagerBuilder"
name="Android Resource Manager"
class="com.android.ide.eclipse.adt.internal.editors.xml.XmlSourceViewerConfig"
target="com.android.ide.eclipse.editors.xml.XmlEditor">
</sourceViewerConfiguration>
+ <provisionalConfiguration
+ type="org.eclipse.jface.text.quickassist.IQuickAssistProcessor"
+ class="com.android.ide.eclipse.adt.internal.build.AaptQuickFix"
+ target="org.eclipse.wst.xml.XML_DEFAULT" />
</extension>
<extension
point="org.eclipse.ui.propertyPages">
--- /dev/null
+/*
+ * 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 com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidContentAssist;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.xml.Hyperlinks;
+import com.android.ide.eclipse.adt.internal.ui.ResourceChooser;
+import com.android.resources.ResourceType;
+import com.android.util.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.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;
+
+/**
+ * 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.
+ */
+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";
+ }
+
+ // ---- Implements IMarkerResolution2 ----
+
+ 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());
+ }
+
+ public IMarkerResolution[] getResolutions(IMarker marker) {
+ IResource markerResource = marker.getResource();
+ IProject project = markerResource.getProject();
+
+ 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 (ResourceChooser.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 ----
+
+ public boolean canAssist(IQuickAssistInvocationContext invocationContext) {
+ return true;
+ }
+
+ public boolean canFix(Annotation annotation) {
+ return true;
+ }
+
+ 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 = AndroidContentAssist.getAndroidXmlEditor(sourceViewer);
+ if (editor != null) {
+ IFile file = editor.getInputFile();
+
+ try {
+ IMarker[] markers = file.findMarkers(AdtConstants.MARKER_AAPT_COMPILE, true,
+ IResource.DEPTH_ZERO);
+
+ // Look for a match on the same line as the caret.
+ int offset = invocationContext.getOffset();
+ IDocument document = sourceViewer.getDocument();
+ int currentLine = document.getLineOfOffset(offset) + 1;
+
+ for (IMarker marker : markers) {
+ int line = marker.getAttribute(IMarker.LINE_NUMBER, -1);
+ if (line == currentLine) {
+ 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);
+ if (end > start) {
+ int length = end - start;
+ String resource = document.get(start, length);
+ // Can only offer create value for non-framework value
+ // resources
+ if (ResourceChooser.canCreateResource(resource)) {
+ IProject project = editor.getProject();
+ return new ICompletionProposal[] {
+ new CreateResourceProposal(project, resource)
+ };
+ }
+ }
+ }
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ return null;
+ }
+
+ public String getErrorMessage() {
+ return null;
+ }
+
+ 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() {
+ Pair<ResourceType,String> resource = Hyperlinks.parseResource(mResource);
+ ResourceType type = resource.getFirst();
+ String name = resource.getSecond();
+ 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 =
+ ResourceChooser.createResource(mProject, type, name, value);
+ if (location != null) {
+ IFile file = location.getFirst();
+ IRegion region = location.getSecond();
+ try {
+ Hyperlinks.openFile(file, region);
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Can't open file %1$s", file.getName());
+ }
+ }
+ }
+
+ // ---- Implements ICompletionProposal ----
+
+ public void apply(IDocument document) {
+ perform();
+ }
+
+ public String getAdditionalProposalInfo() {
+ return "Creates an XML file entry for the given missing resource "
+ + "and opens it in the editor.";
+ }
+
+ public IContextInformation getContextInformation() {
+ return null;
+ }
+
+ public String getDisplayString() {
+ return String.format("Create resource %1$s", mResource);
+ }
+
+ public Image getImage() {
+ return AdtPlugin.getAndroidLogo();
+ }
+
+ public Point getSelection(IDocument document) {
+ return null;
+ }
+
+ // ---- Implements MarkerResolution2 ----
+
+ public String getLabel() {
+ return getDisplayString();
+ }
+
+ public void run(IMarker marker) {
+ perform();
+ }
+
+ public String getDescription() {
+ return getAdditionalProposalInfo();
+ }
+ }
+}
/**
* Returns the active {@link AndroidXmlEditor} matching this source viewer.
*/
- private AndroidXmlEditor getAndroidXmlEditor(ITextViewer viewer) {
+ public static AndroidXmlEditor getAndroidXmlEditor(ITextViewer viewer) {
IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
if (wwin != null) {
IWorkbenchPage page = wwin.getActivePage();
return null;
}
- protected static IndexedRegion getRegion(Node node) {
+ public static IndexedRegion getRegion(Node node) {
if (node instanceof IndexedRegion) {
return (IndexedRegion) node;
}
}
}
- /** Opens the given file and shows the given (optional) region */
- private static void openFile(IFile file, IRegion region) throws PartInitException {
+ /**
+ * Opens the given file and shows the given (optional) region
+ *
+ * @param file the file to be opened
+ * @param region an optional region which if set will be selected and shown to the
+ * user
+ * @throws PartInitException if something goes wrong
+ */
+ public static void openFile(IFile file, IRegion region) throws PartInitException {
IEditorPart sourceEditor = getEditor();
IWorkbenchPage page = sourceEditor.getEditorSite().getPage();
IEditorPart targetEditor = IDE.openEditor(page, file, true);
if (targetEditor instanceof AndroidXmlEditor) {
AndroidXmlEditor editor = (AndroidXmlEditor) targetEditor;
- if ((region != null)) {
+ if (region != null) {
editor.show(region.getOffset(), region.getLength());
} else {
editor.setActivePage(AndroidXmlEditor.TEXT_EDITOR_ID);
}
/** Return the resource type of the given url, and the resource name */
- private static Pair<ResourceType,String> parseResource(String url) {
+ public static Pair<ResourceType,String> parseResource(String url) {
if (!url.startsWith("@")) { //$NON-NLS-1$
return null;
}
}
/** Returns the editor applicable to this hyperlink detection */
- private static IEditorPart getEditor() {
+ public static IEditorPart getEditor() {
// I would like to be able to find this via getAdapter(TextEditor.class) but
// couldn't find a way to initialize the editor context from
// AndroidSourceViewerConfig#getHyperlinkDetectorTargets (which only has
package com.android.ide.eclipse.adt.internal.ui;
import static com.android.AndroidConstants.FD_RES_VALUES;
+import static com.android.ide.eclipse.adt.AdtConstants.ANDROID_PKG;
import static com.android.ide.eclipse.adt.AdtConstants.EXT_XML;
import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP;
import static com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors.NAME_ATTR;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.VisualRefactoring;
import com.android.ide.eclipse.adt.internal.editors.resources.descriptors.ResourcesDescriptors;
import com.android.ide.eclipse.adt.internal.editors.xml.Hyperlinks;
import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringRefactoring;
import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
import com.android.ide.eclipse.adt.internal.resources.manager.ResourceItem;
import com.android.ide.eclipse.adt.internal.resources.manager.ResourceRepository;
+import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileWizard;
+import com.android.resources.FolderTypeRelationship;
+import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
+import com.android.util.Pair;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.IInputValidator;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.Region;
import org.eclipse.jface.window.Window;
import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
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.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.w3c.dom.Document;
return null;
}
+ Pair<IFile, IRegion> resource = createResource(mProject, type, name, value);
+ if (resource != null) {
+ return name;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns true if this class can create the given resource
+ *
+ * @param resource the resource to be created
+ * @return true if the {@link #createResource} method can create this resource
+ */
+ public static boolean canCreateResource(String resource) {
+ // Cannot create framework resources
+ if (resource.startsWith('@' + ANDROID_PKG + ':')) {
+ return false;
+ }
+
+ Pair<ResourceType,String> parsed = Hyperlinks.parseResource(resource);
+ if (parsed != null) {
+ // We can create all value types
+ ResourceType type = parsed.getFirst();
+ if (ResourceNameValidator.isValueBasedResourceType(type)) {
+ return true;
+ }
+
+ // We can create -some- file-based types - those supported by the New XML wizard:
+
+ for (ResourceFolderType folderType : FolderTypeRelationship.getRelatedFolders(type)) {
+ if (NewXmlFileWizard.canCreateXmlFile(folderType)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /** Creates a file-based resource, like a layout */
+ private static Pair<IFile,IRegion> createFileResource(IProject project, ResourceType type,
+ String name) {
+
+ ResourceFolderType folderType = null;
+ for (ResourceFolderType f : FolderTypeRelationship.getRelatedFolders(type)) {
+ if (NewXmlFileWizard.canCreateXmlFile(f)) {
+ folderType = f;
+ break;
+ }
+ }
+ if (folderType == null) {
+ return null;
+ }
+
+ // Find "dimens.xml" file in res/values/ (or corresponding name for other
+ // value types)
+ IPath projectPath = new Path(FD_RESOURCES + WS_SEP + folderType.getName() + WS_SEP
+ + name + '.' + EXT_XML);
+ IFile file = project.getFile(projectPath);
+ IRegion region = new Region(0, 0);
+ return Pair.of(NewXmlFileWizard.createXmlFile(project, file, folderType), region);
+ }
+
+ /**
+ * Creates a resource of a given type, name and (if applicable) value
+ *
+ * @param project the project to contain the resource
+ * @param type the type of resource
+ * @param name the name of the resource
+ * @param value the value of the resource, if it is a value-type resource
+ * @return a pair of the file containing the resource and a region where the value
+ * appears
+ */
+ public static Pair<IFile,IRegion> createResource(IProject project, ResourceType type,
+ String name, String value) {
+ if (!ResourceNameValidator.isValueBasedResourceType(type)) {
+ return createFileResource(project, type, name);
+ }
+
// Find "dimens.xml" file in res/values/ (or corresponding name for other
// value types)
String fileName = type.getName() + 's';
- String projectPath = FD_RESOURCES + WS_SEP + FD_RES_VALUES + WS_SEP + fileName + '.' + EXT_XML;
- IResource member = mProject.findMember(projectPath);
+ String projectPath = FD_RESOURCES + WS_SEP + FD_RES_VALUES + WS_SEP
+ + fileName + '.' + EXT_XML;
+ Object editRequester = project;
+ IResource member = project.findMember(projectPath);
if (member != null) {
if (member instanceof IFile) {
IFile file = (IFile) member;
model = manager.getModelForEdit(file);
}
if (model instanceof IDOMModel) {
- model.beginRecording(this, String.format("Add %1$s",
+ model.beginRecording(editRequester, String.format("Add %1$s",
type.getDisplayName()));
IDOMModel domModel = (IDOMModel) model;
Document document = domModel.getDocument();
Text valueNode = document.createTextNode(value);
element.appendChild(valueNode);
model.save();
- return name;
+ IndexedRegion domRegion = VisualRefactoring.getRegion(valueNode);
+ int startOffset = domRegion.getStartOffset();
+ int length = domRegion.getLength();
+ IRegion region = new Region(startOffset, length);
+ return Pair.of(file, region);
}
} catch (Exception e) {
AdtPlugin.log(e, "Cannot access XML value model");
} finally {
if (model != null) {
- model.endRecording(this);
+ model.endRecording(editRequester);
model.releaseFromEdit();
}
}
sb.append(name);
sb.append('"');
sb.append('>');
+ int start = sb.length();
sb.append(value);
+ int end = sb.length();
sb.append('<').append('/');
sb.append(type.getName());
sb.append(">\n"); //$NON-NLS-1$
try {
byte[] buf = result.getBytes("UTF8"); //$NON-NLS-1$
InputStream stream = new ByteArrayInputStream(buf);
- IFile file = mProject.getFile(new Path(projectPath));
+ IFile file = project.getFile(new Path(projectPath));
file.create(stream, true /*force*/, null /*progress*/);
- return name;
+ IRegion region = new Region(start, end - start);
+ return Pair.of(file, region);
} catch (UnsupportedEncodingException e) {
error = e.getMessage();
} catch (CoreException e) {
}
}
+ /**
+ * Returns the {@link TypeInfo} for the given {@link ResourceFolderType}, or null if
+ * not found
+ *
+ * @param folderType the {@link ResourceFolderType} to look for
+ * @return the corresponding {@link TypeInfo}
+ */
+ static TypeInfo getTypeInfo(ResourceFolderType folderType) {
+ for (TypeInfo typeInfo : sTypes) {
+ if (typeInfo.getResFolderType() == folderType) {
+ return typeInfo;
+ }
+ }
+
+ return null;
+ }
}
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.IconFactory;
import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileCreationPage.TypeInfo;
+import com.android.resources.ResourceFolderType;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
private IFile createXmlFile() {
IFile file = mMainPage.getDestinationFile();
- String name = file.getFullPath().toString();
- boolean need_delete = false;
-
- if (file.exists()) {
- if (!AdtPlugin.displayPrompt("New Android XML File",
- String.format("Do you want to overwrite the file %1$s ?", name))) {
- // abort if user selects cancel.
- return null;
- }
- need_delete = true;
- } else {
- createWsParentDirectory(file.getParent());
- }
-
TypeInfo type = mMainPage.getSelectedType();
if (type == null) {
// this is not expected to happen
+ String name = file.getFullPath().toString();
AdtPlugin.log(IStatus.ERROR, "Failed to create %1$s: missing type", name); //$NON-NLS-1$
return null;
}
return null;
}
+ String attrs = type.getDefaultAttrs(mMainPage.getProject());
+
+ return createXmlFile(file, xmlns, root, attrs);
+ }
+
+ /** Creates a new file using the given root element, namespace and root attributes */
+ private static IFile createXmlFile(IFile file, String xmlns,
+ String root, String rootAttributes) {
+ String name = file.getFullPath().toString();
+ boolean need_delete = false;
+
+ if (file.exists()) {
+ if (!AdtPlugin.displayPrompt("New Android XML File",
+ String.format("Do you want to overwrite the file %1$s ?", name))) {
+ // abort if user selects cancel.
+ return null;
+ }
+ need_delete = true;
+ } else {
+ createWsParentDirectory(file.getParent());
+ }
+
StringBuilder sb = new StringBuilder("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$
sb.append('<').append(root);
sb.append('\n').append(" xmlns:android=\"").append(xmlns).append("\""); //$NON-NLS-1$ //$NON-NLS-2$
}
- String attrs = type.getDefaultAttrs(mMainPage.getProject());
- if (attrs != null) {
+ if (rootAttributes != null) {
sb.append("\n "); //$NON-NLS-1$
- sb.append(attrs.replace("\n", "\n ")); //$NON-NLS-1$ //$NON-NLS-2$
+ sb.append(rootAttributes.replace("\n", "\n ")); //$NON-NLS-1$ //$NON-NLS-2$
}
sb.append(">\n"); //$NON-NLS-1$
return null;
}
- private boolean createWsParentDirectory(IContainer wsPath) {
+ /**
+ * Returns true if the New XML Wizard can create new files of the given
+ * {@link ResourceFolderType}
+ *
+ * @param folderType the folder type to create a file for
+ * @return true if this wizard can create new files for the given folder type
+ */
+ public static boolean canCreateXmlFile(ResourceFolderType folderType) {
+ TypeInfo typeInfo = NewXmlFileCreationPage.getTypeInfo(folderType);
+ return typeInfo != null && (typeInfo.getDefaultRoot() != null ||
+ typeInfo.getRootSeed() instanceof String);
+ }
+
+ /**
+ * Creates a new XML file using the template according to the given folder type
+ *
+ * @param project the project to create the file in
+ * @param file the file to be created
+ * @param folderType the type of folder to look up a template for
+ * @return the created file
+ */
+ public static IFile createXmlFile(IProject project, IFile file, ResourceFolderType folderType) {
+ TypeInfo type = NewXmlFileCreationPage.getTypeInfo(folderType);
+ String xmlns = type.getXmlns();
+ String root = type.getDefaultRoot();
+ if (root == null) {
+ root = type.getRootSeed().toString();
+ }
+ String attrs = type.getDefaultAttrs(project);
+ return createXmlFile(file, xmlns, root, attrs);
+ }
+
+ private static boolean createWsParentDirectory(IContainer wsPath) {
if (wsPath.getType() == IResource.FOLDER) {
if (wsPath == null || wsPath.exists()) {
return true;
--- /dev/null
+/*
+ * 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.AndroidConstants.FD_RES_LAYOUT;
+import static com.android.sdklib.SdkConstants.FD_RES;
+
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.AdtProjectTest;
+import com.android.ide.eclipse.adt.internal.editors.xml.Hyperlinks;
+
+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.IPath;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.jface.text.Document;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IMarkerResolution;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.ide.IDE;
+import org.eclipse.ui.part.FileEditorInput;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class AaptQuickFixTest extends AdtProjectTest {
+ public void testQuickFix1() throws Exception {
+ // Test adding a value into an existing file (res/values/strings.xml)
+ checkFixes("quickfix1.xml", "android:text=\"@string/firs^tstring\"",
+ "res/values/strings.xml");
+ }
+
+ public void testQuickFix2() throws Exception {
+ // Test adding a value into a new file (res/values/dimens.xml, will be created)
+ checkFixes("quickfix1.xml", "android:layout_width=\"@dimen/^testdimen\"",
+ "res/values/dimens.xml");
+ }
+
+ public void testQuickFix3() throws Exception {
+ // Test adding a file based resource (uses new file wizard machinery)
+ checkFixes("quickfix1.xml", "layout=\"@layout/^testlayout\"", "res/layout/testlayout.xml");
+ }
+
+ private void checkFixes(String name, String caretLocation, String expectedNewPath)
+ throws Exception {
+ IProject project = getProject();
+ IFile file = getTestDataFile(project, name, FD_RES + "/" + FD_RES_LAYOUT + "/" + name);
+
+ // Determine the offset
+ String fileContent = AdtPlugin.readFile(file);
+ int caretDelta = caretLocation.indexOf("^");
+ assertTrue(caretLocation, caretDelta != -1);
+ String caretContext = caretLocation.substring(0, caretDelta)
+ + caretLocation.substring(caretDelta + "^".length());
+ int caretContextIndex = fileContent.indexOf(caretContext);
+ assertTrue("Caret content " + caretContext + " not found in file",
+ caretContextIndex != -1);
+ final int offset = caretContextIndex + caretDelta;
+
+ // Run AaptParser such that markers are added...
+ // When debugging these tests, the project gets a chance to build itself so
+ // the real aapt errors are there. But when the test is run directly, aapt has
+ // not yet run. I tried waiting for the build (using the code in SampleProjectTest)
+ // but this had various adverse effects (exception popups from the Eclipse debugger
+ // etc) so instead this test just hardcodes the aapt errors that should be
+ // observed on quickfix1.xml.
+ assertEquals("Unit test is hardcoded to errors for quickfix1.xml", "quickfix1.xml", name);
+ String osRoot = project.getLocation().toOSString();
+ List<String> errors = new ArrayList<String>();
+ String fileRelativePath = file.getProjectRelativePath().toPortableString();
+ String filePath = osRoot + File.separator + fileRelativePath;
+ errors.add(filePath + ":7: error: Error: No resource found that matches the given name"
+ + " (at 'text' with value '@string/firststring').");
+ errors.add(filePath + ":9: error: Error: No resource found that matches the given name"
+ + " (at 'layout_width' with value '@dimen/testdimen').");
+ errors.add(filePath + ":13: error: Error: No resource found that matches the given name"
+ + " (at 'layout' with value '@layout/testlayout').");
+ AaptParser.parseOutput(errors, project);
+
+ AaptQuickFix aaptQuickFix = new AaptQuickFix();
+
+ // Open file
+ IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
+ assertNotNull(page);
+ IEditorPart editor = IDE.openEditor(page, file);
+ assertTrue(editor instanceof AndroidXmlEditor);
+ AndroidXmlEditor layoutEditor = (AndroidXmlEditor) editor;
+ final ISourceViewer viewer = layoutEditor.getStructuredSourceViewer();
+
+ // Test marker resolution.
+ IMarker[] markers = file.findMarkers(AdtConstants.MARKER_AAPT_COMPILE, true,
+ IResource.DEPTH_ZERO);
+ for (IMarker marker : markers) {
+ int start = marker.getAttribute(IMarker.CHAR_START, 0);
+ int end = marker.getAttribute(IMarker.CHAR_END, 0);
+ if (offset >= start && offset <= end) {
+ // Found the target marker. Now check the marker resolution of it.
+ assertTrue(aaptQuickFix.hasResolutions(marker));
+ IMarkerResolution[] resolutions = aaptQuickFix.getResolutions(marker);
+ assertNotNull(resolutions);
+ assertEquals(1, resolutions.length);
+ IMarkerResolution resolution = resolutions[0];
+ assertNotNull(resolution);
+ assertTrue(resolution.getLabel().contains("Create resource"));
+
+ // Not running marker yet -- if we create the files here they already
+ // exist when the quick assist code runs. (The quick fix and the quick assist
+ // mostly share code for the implementation anyway.)
+ //resolution.run(marker);
+ break;
+ }
+ }
+
+ // Next test quick assist.
+
+ IQuickAssistInvocationContext invocationContext = new IQuickAssistInvocationContext() {
+ public int getLength() {
+ return 0;
+ }
+
+ public int getOffset() {
+ return offset;
+ }
+
+ public ISourceViewer getSourceViewer() {
+ return viewer;
+ }
+ };
+ ICompletionProposal[] proposals = aaptQuickFix
+ .computeQuickAssistProposals(invocationContext);
+ assertNotNull(proposals);
+ assertTrue(proposals.length == 1);
+ ICompletionProposal proposal = proposals[0];
+
+ assertNotNull(proposal.getAdditionalProposalInfo());
+ assertNotNull(proposal.getImage());
+ assertTrue(proposal.getDisplayString().contains("Create resource"));
+
+ IDocument document = new Document();
+ document.set(fileContent);
+
+ // Apply quick fix
+ proposal.apply(document);
+
+ IPath path = new Path(expectedNewPath);
+ IFile newFile = project.getFile(path);
+ assertNotNull(path.toPortableString(), newFile);
+
+ // Ensure that the newly created file was opened
+ IEditorPart currentFile = Hyperlinks.getEditor();
+ assertEquals(newFile.getProjectRelativePath(),
+ ((FileEditorInput) currentFile.getEditorInput()).getFile().getProjectRelativePath());
+
+ // Look up caret offset
+ assertTrue(currentFile instanceof AndroidXmlEditor);
+ AndroidXmlEditor newEditor = (AndroidXmlEditor) currentFile;
+ ISourceViewer newViewer = newEditor.getStructuredSourceViewer();
+ Point selectedRange = newViewer.getSelectedRange();
+
+ String newFileContents = AdtPlugin.readFile(newFile);
+
+ // Insert selection markers -- [ ] for the selection range, ^ for the caret
+ String newFileWithCaret;
+ int selectionBegin = selectedRange.x;
+ int selectionEnd = selectionBegin + selectedRange.y;
+ if (selectionBegin < selectionEnd) {
+ newFileWithCaret = newFileContents.substring(0, selectionBegin) + "[^"
+ + newFileContents.substring(selectionBegin, selectionEnd) + "]"
+ + newFileContents.substring(selectionEnd);
+ } else {
+ // Selected range
+ newFileWithCaret = newFileContents.substring(0, selectionBegin) + "^"
+ + newFileContents.substring(selectionBegin);
+ }
+
+ newFileWithCaret = removeSessionData(newFileWithCaret);
+
+ assertEqualsGolden(name, newFileWithCaret);
+ }
+}
return sb.toString();
}
+ protected String removeSessionData(String data) {
+ if (getProject() != null) {
+ data = data.replace(getProject().getName(), "PROJECTNAME");
+ }
+
+ return data;
+ }
+
public static ViewElementDescriptor createDesc(String name, String fqn, boolean hasChildren) {
if (hasChildren) {
return new ViewElementDescriptor(name, name, fqn, "", "", new AttributeDescriptor[0],
assertNotNull(xml);
assertTrue(xml.length() > 0);
+ // Remove any references to the project name such that we are isolated from
+ // that in golden file.
+ // Appears in strings.xml etc.
+ xml = removeSessionData(xml);
+
return xml;
}
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="hello">Hello World!</string>
+ <string name="app_name">PROJECTNAME</string>
+ <string name="firststring">[^TODO]</string>
+</resources>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="testdimen">[^1dp]</dimen>
+</resources>
--- /dev/null
+^<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+</LinearLayout>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <Button android:text="@string/firststring"
+ android:id="@+id/button1"
+ android:layout_width="@dimen/testdimen"
+ android:layout_height="wrap_content">
+ </Button>
+
+ <include layout="@layout/testlayout" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
+
+</LinearLayout>
+