OSDN Git Service

Add Quickfix and Quick Assistant for creating resources
authorTor Norbye <tnorbye@google.com>
Tue, 15 Mar 2011 04:15:57 +0000 (21:15 -0700)
committerTor Norbye <tnorbye@google.com>
Thu, 17 Mar 2011 20:30:26 +0000 (13:30 -0700)
This changeset adds two related features:

- A "marker resolution" which adds a quickfix to aapt errors for
  nonexistent resources. This means that you can right click on the
  error in the Problems view to create the associated missing
  resource.

- A "quick assistant" which looks for a missing resource on the
  current line and if found adds a fix handler for it, similar to the
  quickfix above.

When a fix is invoked, it will create the new file or value, and open
it in the editor with the relevant value section selected.

The quick assistant can be invoked by the normal Ctrl-1 (Cmd-1). The
quickfix must be invoked from the Problems view; the Java editor seems
to add an extra level of integration with a lightbulb in the editor
margin but we don't get that in XML.

The quickfixes work for all value-based resources, as well as some
file-based ones (in particular, those supported by the New XML File
wizard, which it reuses.)

This changeset also adds unit tests for quickfixes along with a few
infrastructure changes to support it.

Change-Id: I962bcf6c98934685e4d74389469d0903115a75e3

15 files changed:
eclipse/dictionary.txt
eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptQuickFix.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/xml/Hyperlinks.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ResourceChooser.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/build/AaptQuickFixTest.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/AdtProjectTest.java
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/quickfix1-expected-quickFix1.xml [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/quickfix1-expected-quickFix2.xml [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/quickfix1-expected-quickFix3.xml [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/quickfix1.xml [new file with mode: 0644]

index b9ccb29..5ec965c 100644 (file)
@@ -84,6 +84,7 @@ gen
 groovy
 guava
 hardcoded
+hardcodes
 hotfix
 href
 http
@@ -140,6 +141,7 @@ pings
 placeholder
 plugin
 popup
+popups
 pre
 precompiler
 pref
index 0bdbee1..1da34ee 100644 (file)
       <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">
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptQuickFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptQuickFix.java
new file mode 100644 (file)
index 0000000..57ba97c
--- /dev/null
@@ -0,0 +1,263 @@
+/*
+ * 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();
+        }
+    }
+}
index 61c34bf..bffcd3f 100644 (file)
@@ -905,7 +905,7 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor {
     /**
      * 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();
index 65ec7fd..5245cc4 100644 (file)
@@ -829,7 +829,7 @@ public abstract class VisualRefactoring extends Refactoring {
         return null;
     }
 
-    protected static IndexedRegion getRegion(Node node) {
+    public static IndexedRegion getRegion(Node node) {
         if (node instanceof IndexedRegion) {
             return (IndexedRegion) node;
         }
index 5a2f39f..6ec16dd 100644 (file)
@@ -528,14 +528,21 @@ public class Hyperlinks {
         }
     }
 
-    /** 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);
@@ -1029,7 +1036,7 @@ public class Hyperlinks {
     }
 
     /** 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;
         }
@@ -1340,7 +1347,7 @@ public class Hyperlinks {
     }
 
     /** 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
index cfa29f8..83d0f96 100644 (file)
@@ -17,6 +17,7 @@
 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;
@@ -24,6 +25,7 @@ import static com.android.sdklib.SdkConstants.FD_RESOURCES;
 
 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;
@@ -32,17 +34,24 @@ import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
 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;
@@ -65,6 +74,7 @@ import org.eclipse.ui.dialogs.SelectionStatusDialog;
 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;
@@ -278,11 +288,93 @@ public class ResourceChooser extends AbstractElementListSelectionDialog {
             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;
@@ -295,7 +387,7 @@ public class ResourceChooser extends AbstractElementListSelectionDialog {
                         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();
@@ -324,13 +416,17 @@ public class ResourceChooser extends AbstractElementListSelectionDialog {
                         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();
                     }
                 }
@@ -351,7 +447,9 @@ public class ResourceChooser extends AbstractElementListSelectionDialog {
             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$
@@ -361,9 +459,10 @@ public class ResourceChooser extends AbstractElementListSelectionDialog {
             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) {
index 8829d76..43ae559 100644 (file)
@@ -1331,4 +1331,20 @@ class NewXmlFileCreationPage extends WizardPage {
         }
     }
 
+    /**
+     * 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;
+    }
 }
index a87400b..6bce6fb 100644 (file)
@@ -21,10 +21,12 @@ package com.android.ide.eclipse.adt.internal.wizards.newxmlfile;
 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;
@@ -130,23 +132,10 @@ public class NewXmlFileWizard extends Wizard implements INewWizard {
 
     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;
         }
@@ -159,6 +148,28 @@ public class NewXmlFileWizard extends Wizard implements INewWizard {
             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);
@@ -166,10 +177,9 @@ public class NewXmlFileWizard extends Wizard implements INewWizard {
             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$
@@ -196,7 +206,39 @@ public class NewXmlFileWizard extends Wizard implements INewWizard {
         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;
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/build/AaptQuickFixTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/build/AaptQuickFixTest.java
new file mode 100644 (file)
index 0000000..f70f9af
--- /dev/null
@@ -0,0 +1,205 @@
+/*
+ * 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);
+    }
+}
index 4281077..597ae5a 100644 (file)
@@ -238,6 +238,14 @@ public class AdtProjectTest extends SdkTestCase {
         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],
@@ -277,6 +285,11 @@ public class AdtProjectTest extends SdkTestCase {
         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;
     }
 
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/quickfix1-expected-quickFix1.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/quickfix1-expected-quickFix1.xml
new file mode 100644 (file)
index 0000000..2ef716b
--- /dev/null
@@ -0,0 +1,6 @@
+<?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>
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/quickfix1-expected-quickFix2.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/quickfix1-expected-quickFix2.xml
new file mode 100644 (file)
index 0000000..a0d04fb
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <dimen name="testdimen">[^1dp]</dimen>
+</resources>
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/quickfix1-expected-quickFix3.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/quickfix1-expected-quickFix3.xml
new file mode 100644 (file)
index 0000000..14256f4
--- /dev/null
@@ -0,0 +1,6 @@
+^<?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>
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/quickfix1.xml b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/testdata/quickfix1.xml
new file mode 100644 (file)
index 0000000..927c8d1
--- /dev/null
@@ -0,0 +1,16 @@
+<?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>
+