1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ide.eclipse.adt.internal.build; 18 19 import static com.android.SdkConstants.ANDROID_URI; 20 import static com.android.SdkConstants.XMLNS_ANDROID; 21 import static com.android.SdkConstants.XMLNS_URI; 22 23 import com.android.ide.common.resources.ResourceUrl; 24 import com.android.ide.eclipse.adt.AdtConstants; 25 import com.android.ide.eclipse.adt.AdtPlugin; 26 import com.android.ide.eclipse.adt.AdtUtils; 27 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; 28 import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; 29 import com.android.resources.ResourceType; 30 import com.android.utils.Pair; 31 32 import org.eclipse.core.resources.IFile; 33 import org.eclipse.core.resources.IMarker; 34 import org.eclipse.core.resources.IProject; 35 import org.eclipse.core.resources.IResource; 36 import org.eclipse.core.runtime.CoreException; 37 import org.eclipse.jface.text.BadLocationException; 38 import org.eclipse.jface.text.IDocument; 39 import org.eclipse.jface.text.IRegion; 40 import org.eclipse.jface.text.Region; 41 import org.eclipse.jface.text.contentassist.ICompletionProposal; 42 import org.eclipse.jface.text.contentassist.IContextInformation; 43 import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext; 44 import org.eclipse.jface.text.quickassist.IQuickAssistProcessor; 45 import org.eclipse.jface.text.source.Annotation; 46 import org.eclipse.jface.text.source.ISourceViewer; 47 import org.eclipse.swt.graphics.Image; 48 import org.eclipse.swt.graphics.Point; 49 import org.eclipse.ui.IMarkerResolution; 50 import org.eclipse.ui.IMarkerResolution2; 51 import org.eclipse.ui.IMarkerResolutionGenerator2; 52 import org.eclipse.ui.PartInitException; 53 import org.eclipse.ui.editors.text.TextFileDocumentProvider; 54 import org.eclipse.ui.texteditor.IDocumentProvider; 55 import org.eclipse.wst.sse.core.StructuredModelManager; 56 import org.eclipse.wst.sse.core.internal.provisional.IModelManager; 57 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; 58 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; 59 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; 60 import org.w3c.dom.Attr; 61 import org.w3c.dom.Document; 62 import org.w3c.dom.Element; 63 64 import java.util.List; 65 66 /** 67 * Shared handler for both quick assist processors (Control key handler) and quick fix 68 * marker resolution (Problem view handling), since there is a lot of overlap between 69 * these two UI handlers. 70 */ 71 @SuppressWarnings("restriction") // XML model 72 public class AaptQuickFix implements IMarkerResolutionGenerator2, IQuickAssistProcessor { 73 74 public AaptQuickFix() { 75 } 76 77 /** Returns the error message from aapt that signals missing resources */ 78 private static String getTargetMarkerErrorMessage() { 79 return "No resource found that matches the given name"; 80 } 81 82 /** Returns the error message from aapt that signals a missing namespace declaration */ 83 private static String getUnboundErrorMessage() { 84 return "Error parsing XML: unbound prefix"; 85 } 86 87 // ---- Implements IMarkerResolution2 ---- 88 89 @Override 90 public boolean hasResolutions(IMarker marker) { 91 String message = null; 92 try { 93 message = (String) marker.getAttribute(IMarker.MESSAGE); 94 } catch (CoreException e) { 95 AdtPlugin.log(e, null); 96 } 97 98 return message != null 99 && (message.contains(getTargetMarkerErrorMessage()) 100 || message.contains(getUnboundErrorMessage())); 101 } 102 103 @Override 104 public IMarkerResolution[] getResolutions(IMarker marker) { 105 IResource markerResource = marker.getResource(); 106 IProject project = markerResource.getProject(); 107 try { 108 String message = (String) marker.getAttribute(IMarker.MESSAGE); 109 if (message.contains(getUnboundErrorMessage()) && markerResource instanceof IFile) { 110 return new IMarkerResolution[] { 111 new CreateNamespaceFix((IFile) markerResource) 112 }; 113 } 114 } catch (CoreException e1) { 115 AdtPlugin.log(e1, null); 116 } 117 118 int start = marker.getAttribute(IMarker.CHAR_START, 0); 119 int end = marker.getAttribute(IMarker.CHAR_END, 0); 120 if (end > start) { 121 int length = end - start; 122 IDocumentProvider provider = new TextFileDocumentProvider(); 123 try { 124 provider.connect(markerResource); 125 IDocument document = provider.getDocument(markerResource); 126 String resource = document.get(start, length); 127 if (ResourceHelper.canCreateResource(resource)) { 128 return new IMarkerResolution[] { 129 new CreateResourceProposal(project, resource) 130 }; 131 } 132 } catch (Exception e) { 133 AdtPlugin.log(e, "Can't find range information for %1$s", markerResource); 134 } finally { 135 provider.disconnect(markerResource); 136 } 137 } 138 139 return null; 140 } 141 142 // ---- Implements IQuickAssistProcessor ---- 143 144 @Override 145 public boolean canAssist(IQuickAssistInvocationContext invocationContext) { 146 return true; 147 } 148 149 @Override 150 public boolean canFix(Annotation annotation) { 151 return true; 152 } 153 154 @Override 155 public ICompletionProposal[] computeQuickAssistProposals( 156 IQuickAssistInvocationContext invocationContext) { 157 158 // We have to find the corresponding project/file (so we can look up the aapt 159 // error markers). Unfortunately, an IQuickAssistProcessor only gets 160 // access to an ISourceViewer which has no hooks back to the surrounding 161 // editor. 162 // 163 // However, the IQuickAssistProcessor will only be used interactively by a file 164 // being edited, so we can cheat like the hyperlink detector and simply 165 // look up the currently active file in the IDE. To be on the safe side, 166 // we'll make sure that that editor has the same sourceViewer such that 167 // we are indeed looking at the right file: 168 ISourceViewer sourceViewer = invocationContext.getSourceViewer(); 169 AndroidXmlEditor editor = AndroidXmlEditor.fromTextViewer(sourceViewer); 170 if (editor != null) { 171 IFile file = editor.getInputFile(); 172 if (file == null) { 173 return null; 174 } 175 IDocument document = sourceViewer.getDocument(); 176 List<IMarker> markers = AdtUtils.findMarkersOnLine(AdtConstants.MARKER_AAPT_COMPILE, 177 file, document, invocationContext.getOffset()); 178 try { 179 for (IMarker marker : markers) { 180 String message = marker.getAttribute(IMarker.MESSAGE, ""); //$NON-NLS-1$ 181 if (message.contains(getTargetMarkerErrorMessage())) { 182 int start = marker.getAttribute(IMarker.CHAR_START, 0); 183 int end = marker.getAttribute(IMarker.CHAR_END, 0); 184 int length = end - start; 185 String resource = document.get(start, length); 186 // Can only offer create value for non-framework value 187 // resources 188 if (ResourceHelper.canCreateResource(resource)) { 189 IProject project = editor.getProject(); 190 return new ICompletionProposal[] { 191 new CreateResourceProposal(project, resource) 192 }; 193 } 194 } else if (message.contains(getUnboundErrorMessage())) { 195 return new ICompletionProposal[] { 196 new CreateNamespaceFix(null) 197 }; 198 } 199 } 200 } catch (BadLocationException e) { 201 AdtPlugin.log(e, null); 202 } 203 } 204 205 return null; 206 } 207 208 @Override 209 public String getErrorMessage() { 210 return null; 211 } 212 213 /** Quick fix to insert namespace binding when missing */ 214 private final static class CreateNamespaceFix 215 implements ICompletionProposal, IMarkerResolution2 { 216 private IFile mFile; 217 218 public CreateNamespaceFix(IFile file) { 219 mFile = file; 220 } 221 222 private IndexedRegion perform(IDocument doc) { 223 IModelManager manager = StructuredModelManager.getModelManager(); 224 IStructuredModel model = manager.getExistingModelForEdit(doc); 225 if (model != null) { 226 try { 227 perform(model); 228 } finally { 229 model.releaseFromEdit(); 230 } 231 } 232 233 return null; 234 } 235 236 private IndexedRegion perform(IFile file) { 237 IModelManager manager = StructuredModelManager.getModelManager(); 238 IStructuredModel model; 239 try { 240 model = manager.getModelForEdit(file); 241 if (model != null) { 242 try { 243 perform(model); 244 } finally { 245 model.releaseFromEdit(); 246 } 247 } 248 } catch (Exception e) { 249 AdtPlugin.log(e, "Can't look up XML model"); 250 } 251 252 return null; 253 } 254 255 private IndexedRegion perform(IStructuredModel model) { 256 if (model instanceof IDOMModel) { 257 IDOMModel domModel = (IDOMModel) model; 258 Document document = domModel.getDocument(); 259 Element element = document.getDocumentElement(); 260 Attr attr = document.createAttributeNS(XMLNS_URI, XMLNS_ANDROID); 261 attr.setValue(ANDROID_URI); 262 element.getAttributes().setNamedItemNS(attr); 263 return (IndexedRegion) attr; 264 } 265 266 return null; 267 } 268 269 // ---- Implements ICompletionProposal ---- 270 271 @Override 272 public void apply(IDocument document) { 273 perform(document); 274 } 275 276 @Override 277 public String getAdditionalProposalInfo() { 278 return "Adds an Android namespace declaratiopn to the root element."; 279 } 280 281 @Override 282 public IContextInformation getContextInformation() { 283 return null; 284 } 285 286 @Override 287 public String getDisplayString() { 288 return "Insert namespace binding"; 289 } 290 291 @Override 292 public Image getImage() { 293 return AdtPlugin.getAndroidLogo(); 294 } 295 296 @Override 297 public Point getSelection(IDocument doc) { 298 return null; 299 } 300 301 302 // ---- Implements MarkerResolution2 ---- 303 304 @Override 305 public String getLabel() { 306 return getDisplayString(); 307 } 308 309 @Override 310 public void run(IMarker marker) { 311 try { 312 AdtPlugin.openFile(mFile, null); 313 } catch (PartInitException e) { 314 AdtPlugin.log(e, "Can't open file %1$s", mFile.getName()); 315 } 316 317 IndexedRegion indexedRegion = perform(mFile); 318 if (indexedRegion != null) { 319 try { 320 IRegion region = 321 new Region(indexedRegion.getStartOffset(), indexedRegion.getLength()); 322 AdtPlugin.openFile(mFile, region); 323 } catch (PartInitException e) { 324 AdtPlugin.log(e, "Can't open file %1$s", mFile.getName()); 325 } 326 } 327 } 328 329 @Override 330 public String getDescription() { 331 return getAdditionalProposalInfo(); 332 } 333 } 334 335 private static class CreateResourceProposal 336 implements ICompletionProposal, IMarkerResolution2 { 337 private final IProject mProject; 338 private final String mResource; 339 340 CreateResourceProposal(IProject project, String resource) { 341 super(); 342 mProject = project; 343 mResource = resource; 344 } 345 346 private void perform() { 347 ResourceUrl resource = ResourceUrl.parse(mResource); 348 if (resource == null) { 349 return; 350 } 351 ResourceType type = resource.type; 352 String name = resource.name; 353 assert !resource.framework; 354 String value = ""; //$NON-NLS-1$ 355 356 // Try to pick a reasonable first guess. The new value will be highlighted and 357 // selected for editing, but if we have an initial value then the new file 358 // won't show an error. 359 switch (type) { 360 case STRING: value = "TODO"; break; //$NON-NLS-1$ 361 case DIMEN: value = "1dp"; break; //$NON-NLS-1$ 362 case BOOL: value = "true"; break; //$NON-NLS-1$ 363 case COLOR: value = "#000000"; break; //$NON-NLS-1$ 364 case INTEGER: value = "1"; break; //$NON-NLS-1$ 365 case ARRAY: value = "<item>1</item>"; break; //$NON-NLS-1$ 366 } 367 368 Pair<IFile, IRegion> location = 369 ResourceHelper.createResource(mProject, type, name, value); 370 if (location != null) { 371 IFile file = location.getFirst(); 372 IRegion region = location.getSecond(); 373 try { 374 AdtPlugin.openFile(file, region); 375 } catch (PartInitException e) { 376 AdtPlugin.log(e, "Can't open file %1$s", file.getName()); 377 } 378 } 379 } 380 381 // ---- Implements ICompletionProposal ---- 382 383 @Override 384 public void apply(IDocument document) { 385 perform(); 386 } 387 388 @Override 389 public String getAdditionalProposalInfo() { 390 return "Creates an XML file entry for the given missing resource " 391 + "and opens it in the editor."; 392 } 393 394 @Override 395 public IContextInformation getContextInformation() { 396 return null; 397 } 398 399 @Override 400 public String getDisplayString() { 401 return String.format("Create resource %1$s", mResource); 402 } 403 404 @Override 405 public Image getImage() { 406 return AdtPlugin.getAndroidLogo(); 407 } 408 409 @Override 410 public Point getSelection(IDocument document) { 411 return null; 412 } 413 414 // ---- Implements MarkerResolution2 ---- 415 416 @Override 417 public String getLabel() { 418 return getDisplayString(); 419 } 420 421 @Override 422 public void run(IMarker marker) { 423 perform(); 424 } 425 426 @Override 427 public String getDescription() { 428 return getAdditionalProposalInfo(); 429 } 430 } 431 } 432