1 /* 2 * Copyright (C) 2010 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; 18 19 import static com.android.SdkConstants.CLASS_CONSTRUCTOR; 20 import static com.android.SdkConstants.CONSTRUCTOR_NAME; 21 22 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 23 import com.android.ide.eclipse.adt.internal.project.ProjectHelper; 24 import com.android.ide.eclipse.ddms.ISourceRevealer; 25 import com.google.common.base.Predicate; 26 27 import org.eclipse.core.resources.IFile; 28 import org.eclipse.core.resources.IMarker; 29 import org.eclipse.core.resources.IProject; 30 import org.eclipse.core.resources.IResource; 31 import org.eclipse.core.runtime.CoreException; 32 import org.eclipse.core.runtime.NullProgressMonitor; 33 import org.eclipse.jdt.core.ICompilationUnit; 34 import org.eclipse.jdt.core.IJavaElement; 35 import org.eclipse.jdt.core.IJavaProject; 36 import org.eclipse.jdt.core.IMethod; 37 import org.eclipse.jdt.core.ISourceRange; 38 import org.eclipse.jdt.core.IType; 39 import org.eclipse.jdt.core.JavaModelException; 40 import org.eclipse.jdt.core.search.IJavaSearchConstants; 41 import org.eclipse.jdt.core.search.SearchEngine; 42 import org.eclipse.jdt.core.search.SearchMatch; 43 import org.eclipse.jdt.core.search.SearchParticipant; 44 import org.eclipse.jdt.core.search.SearchPattern; 45 import org.eclipse.jdt.core.search.SearchRequestor; 46 import org.eclipse.jdt.ui.JavaUI; 47 import org.eclipse.jface.text.IRegion; 48 import org.eclipse.jface.viewers.IStructuredContentProvider; 49 import org.eclipse.jface.viewers.LabelProvider; 50 import org.eclipse.jface.viewers.Viewer; 51 import org.eclipse.jface.window.Window; 52 import org.eclipse.ui.IPerspectiveRegistry; 53 import org.eclipse.ui.IWorkbench; 54 import org.eclipse.ui.IWorkbenchWindow; 55 import org.eclipse.ui.PlatformUI; 56 import org.eclipse.ui.WorkbenchException; 57 import org.eclipse.ui.dialogs.ListDialog; 58 import org.eclipse.ui.ide.IDE; 59 60 import java.util.ArrayList; 61 import java.util.Collections; 62 import java.util.Comparator; 63 import java.util.HashMap; 64 import java.util.List; 65 import java.util.Map; 66 67 /** 68 * Implementation of the com.android.ide.ddms.sourceRevealer extension point. 69 * Note that this code is duplicated in the PDT plugin's SourceRevealer as well. 70 */ 71 public class SourceRevealer implements ISourceRevealer { 72 @Override reveal(String applicationName, String className, int line)73 public boolean reveal(String applicationName, String className, int line) { 74 IProject project = ProjectHelper.findAndroidProjectByAppName(applicationName); 75 if (project != null) { 76 return BaseProjectHelper.revealSource(project, className, line); 77 } 78 79 return false; 80 } 81 82 /** 83 * Reveal the source for given fully qualified method name.<br> 84 * 85 * The method should take care of the following scenarios:<ol> 86 * <li> A search, either by filename/line number, or for fqmn might provide only 1 result. 87 * In such a case, just open that result. Give preference to the file name/line # search 88 * since that is the most accurate (gets to the line number). </li> 89 * <li> The search might not provide any results. e.g, the method name may be of the form 90 * "com.x.y$1.methodName". Searches for methods within anonymous classes will fail. In 91 * such a case, if the fileName:lineNumber argument is available, a search for that 92 * should be made instead. </li> 93 * <li> The search might provide multiple results. In such a case, the fileName/lineNumber 94 * values should be utilized to narrow down the results.</li> 95 * </ol> 96 * 97 * @param fqmn fully qualified method name 98 * @param fileName file name in which the method is present, null if not known 99 * @param lineNumber line number in the file which should be given focus, -1 if not known. 100 * Line numbers begin at 1, not 0. 101 * @param perspective perspective to switch to before the source is revealed, null to not 102 * switch perspectives 103 */ 104 @Override revealMethod(String fqmn, String fileName, int lineNumber, String perspective)105 public boolean revealMethod(String fqmn, String fileName, int lineNumber, String perspective) { 106 // Search by filename:linenumber. If there is just one result for it, that would 107 // be the correct match that is accurate to the line 108 List<SearchMatch> fileMatches = Collections.emptyList(); 109 if (fileName != null && lineNumber >= 0) { 110 fileMatches = searchForFile(fileName); 111 if (fileMatches.size() == 1) { 112 return revealLineMatch(fileMatches, fileName, lineNumber, perspective); 113 } 114 } 115 116 List<SearchMatch> methodMatches = searchForMethod(fqmn); 117 118 // if there is a unique method name match: 119 // 1. if there are > 1 file name matches, try to see if they can be narrowed down 120 // 2. if not, display the method match 121 if (methodMatches.size() == 1) { 122 if (fileMatches.size() > 0) { 123 List<SearchMatch> filteredMatches = filterMatchByResource(fileMatches, 124 methodMatches.get(0).getResource()); 125 if (filteredMatches.size() == 1) { 126 return revealLineMatch(filteredMatches, fileName, lineNumber, perspective); 127 } 128 } else if (fileName != null && lineNumber > 0) { 129 // Couldn't find file match, but we have a filename and line number: attempt 130 // to use this to pinpoint the location within the method 131 IMethod method = (IMethod) methodMatches.get(0).getElement(); 132 IJavaElement element = method; 133 while (element != null) { 134 if (element instanceof ICompilationUnit) { 135 ICompilationUnit unit = ((ICompilationUnit) element).getPrimary(); 136 IResource resource = unit.getResource(); 137 if (resource instanceof IFile) { 138 IFile file = (IFile) resource; 139 140 try { 141 // See if the line number looks like it's inside the given method 142 ISourceRange sourceRange = method.getSourceRange(); 143 IRegion region = AdtUtils.getRegionOfLine(file, lineNumber - 1); 144 // When fields are initialized with code, this logically belongs 145 // to the constructor, but the line numbers are outside of the 146 // constructor. In this case we'll trust the line number rather 147 // than the method range. 148 boolean isConstructor = fqmn.endsWith(CONSTRUCTOR_NAME); 149 if (isConstructor 150 || region != null 151 && region.getOffset() >= sourceRange.getOffset() 152 && region.getOffset() < sourceRange.getOffset() 153 + sourceRange.getLength()) { 154 // Yes: use the line number instead 155 if (perspective != null) { 156 SourceRevealer.switchToPerspective(perspective); 157 } 158 return displayFile(file, lineNumber); 159 } 160 161 } catch (JavaModelException e) { 162 AdtPlugin.log(e, null); 163 } 164 } 165 } 166 element = element.getParent(); 167 } 168 169 } 170 171 return displayMethod((IMethod) methodMatches.get(0).getElement(), perspective); 172 } 173 174 // no matches for search by method, so search by filename 175 if (methodMatches.size() == 0) { 176 if (fileMatches.size() > 0) { 177 return revealLineMatch(fileMatches, fileName, lineNumber, perspective); 178 } else { 179 // Last ditch effort: attempt to look up the class corresponding to the fqn 180 // and jump to the line there 181 if (fileMatches.isEmpty() && fqmn.indexOf('.') != -1) { 182 String className = fqmn.substring(0, fqmn.lastIndexOf('.')); 183 for (IJavaProject project : BaseProjectHelper.getAndroidProjects(null)) { 184 IType type; 185 try { 186 type = project.findType(className); 187 if (type != null && type.exists()) { 188 IResource resource = type.getResource(); 189 if (resource instanceof IFile) { 190 if (perspective != null) { 191 SourceRevealer.switchToPerspective(perspective); 192 } 193 return displayFile((IFile) resource, lineNumber); 194 } 195 } 196 } catch (JavaModelException e) { 197 AdtPlugin.log(e, null); 198 } 199 } 200 } 201 202 return false; 203 } 204 } 205 206 // multiple matches for search by method, narrow down by filename 207 if (fileName != null) { 208 return revealLineMatch( 209 filterMatchByFileName(methodMatches, fileName), 210 fileName, lineNumber, perspective); 211 } 212 213 // prompt the user 214 SearchMatch match = getMatchToDisplay(methodMatches, fqmn); 215 if (match == null) { 216 return false; 217 } else { 218 return displayMethod((IMethod) match.getElement(), perspective); 219 } 220 } 221 revealLineMatch(List<SearchMatch> matches, String fileName, int lineNumber, String perspective)222 private boolean revealLineMatch(List<SearchMatch> matches, String fileName, int lineNumber, 223 String perspective) { 224 SearchMatch match = getMatchToDisplay(matches, 225 String.format("%s:%d", fileName, lineNumber)); 226 if (match == null) { 227 return false; 228 } 229 230 if (perspective != null) { 231 SourceRevealer.switchToPerspective(perspective); 232 } 233 234 return displayFile((IFile) match.getResource(), lineNumber); 235 } 236 displayFile(IFile file, int lineNumber)237 private boolean displayFile(IFile file, int lineNumber) { 238 try { 239 IMarker marker = file.createMarker(IMarker.TEXT); 240 marker.setAttribute(IMarker.LINE_NUMBER, lineNumber); 241 IDE.openEditor( 242 PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(), 243 marker); 244 marker.delete(); 245 return true; 246 } catch (CoreException e) { 247 AdtPlugin.printErrorToConsole(e.getMessage()); 248 return false; 249 } 250 } 251 displayMethod(IMethod method, String perspective)252 private boolean displayMethod(IMethod method, String perspective) { 253 if (perspective != null) { 254 SourceRevealer.switchToPerspective(perspective); 255 } 256 257 try { 258 JavaUI.openInEditor(method); 259 return true; 260 } catch (Exception e) { 261 AdtPlugin.printErrorToConsole(e.getMessage()); 262 return false; 263 } 264 } 265 filterMatchByFileName(List<SearchMatch> matches, String fileName)266 private List<SearchMatch> filterMatchByFileName(List<SearchMatch> matches, String fileName) { 267 if (fileName == null) { 268 return matches; 269 } 270 271 // Use a map to collapse multiple matches in a single file into just one match since 272 // we know the line number in the file. 273 Map<IResource, SearchMatch> matchesPerFile = 274 new HashMap<IResource, SearchMatch>(matches.size()); 275 276 for (SearchMatch m: matches) { 277 if (m.getResource() instanceof IFile 278 && m.getResource().getName().startsWith(fileName)) { 279 matchesPerFile.put(m.getResource(), m); 280 } 281 } 282 283 List<SearchMatch> filteredMatches = new ArrayList<SearchMatch>(matchesPerFile.values()); 284 285 // sort results, first by project name, then by file name 286 Collections.sort(filteredMatches, new Comparator<SearchMatch>() { 287 @Override 288 public int compare(SearchMatch m1, SearchMatch m2) { 289 String p1 = m1.getResource().getProject().getName(); 290 String p2 = m2.getResource().getProject().getName(); 291 292 if (!p1.equals(p2)) { 293 return p1.compareTo(p2); 294 } 295 296 String r1 = m1.getResource().getName(); 297 String r2 = m2.getResource().getName(); 298 return r1.compareTo(r2); 299 } 300 }); 301 return filteredMatches; 302 } 303 filterMatchByResource(List<SearchMatch> matches, IResource resource)304 private List<SearchMatch> filterMatchByResource(List<SearchMatch> matches, 305 IResource resource) { 306 List<SearchMatch> filteredMatches = new ArrayList<SearchMatch>(matches.size()); 307 308 for (SearchMatch m: matches) { 309 if (m.getResource().equals(resource)) { 310 filteredMatches.add(m); 311 } 312 } 313 314 return filteredMatches; 315 } 316 getMatchToDisplay(List<SearchMatch> matches, String searchTerm)317 private SearchMatch getMatchToDisplay(List<SearchMatch> matches, String searchTerm) { 318 // no matches for given search 319 if (matches.size() == 0) { 320 return null; 321 } 322 323 // there is only 1 match, so we return that 324 if (matches.size() == 1) { 325 return matches.get(0); 326 } 327 328 // multiple matches, prompt the user to select 329 IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); 330 if (window == null) { 331 return null; 332 } 333 334 ListDialog dlg = new ListDialog(window.getShell()); 335 dlg.setMessage("Multiple files match search: " + searchTerm); 336 dlg.setTitle("Select file to open"); 337 dlg.setInput(matches); 338 dlg.setContentProvider(new IStructuredContentProvider() { 339 @Override 340 public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { 341 } 342 343 @Override 344 public void dispose() { 345 } 346 347 @Override 348 public Object[] getElements(Object inputElement) { 349 return ((List<?>) inputElement).toArray(); 350 } 351 }); 352 dlg.setLabelProvider(new LabelProvider() { 353 @Override 354 public String getText(Object element) { 355 SearchMatch m = (SearchMatch) element; 356 return String.format("/%s/%s", //$NON-NLS-1$ 357 m.getResource().getProject().getName(), 358 m.getResource().getProjectRelativePath().toString()); 359 } 360 }); 361 dlg.setInitialSelections(new Object[] { matches.get(0) }); 362 dlg.setHelpAvailable(false); 363 364 if (dlg.open() == Window.OK) { 365 Object[] selectedMatches = dlg.getResult(); 366 if (selectedMatches.length > 0) { 367 return (SearchMatch) selectedMatches[0]; 368 } 369 } 370 371 return null; 372 } 373 searchForFile(String fileName)374 private List<SearchMatch> searchForFile(String fileName) { 375 return searchForPattern(fileName, IJavaSearchConstants.CLASS, MATCH_IS_FILE_PREDICATE); 376 } 377 searchForMethod(String fqmn)378 private List<SearchMatch> searchForMethod(String fqmn) { 379 if (fqmn.endsWith(CONSTRUCTOR_NAME)) { 380 fqmn = fqmn.substring(0, fqmn.length() - CONSTRUCTOR_NAME.length() - 1); // -1: dot 381 return searchForPattern(fqmn, IJavaSearchConstants.CONSTRUCTOR, 382 MATCH_IS_METHOD_PREDICATE); 383 } 384 if (fqmn.endsWith(CLASS_CONSTRUCTOR)) { 385 // Don't try to search for class init methods: Eclipse will throw NPEs if you do 386 return Collections.emptyList(); 387 } 388 389 return searchForPattern(fqmn, IJavaSearchConstants.METHOD, MATCH_IS_METHOD_PREDICATE); 390 } 391 searchForPattern(String pattern, int searchFor, Predicate<SearchMatch> filterPredicate)392 private List<SearchMatch> searchForPattern(String pattern, int searchFor, 393 Predicate<SearchMatch> filterPredicate) { 394 SearchEngine se = new SearchEngine(); 395 SearchPattern searchPattern = SearchPattern.createPattern( 396 pattern, 397 searchFor, 398 IJavaSearchConstants.DECLARATIONS, 399 SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE); 400 SearchResultAccumulator requestor = new SearchResultAccumulator(filterPredicate); 401 try { 402 se.search(searchPattern, 403 new SearchParticipant[] {SearchEngine.getDefaultSearchParticipant()}, 404 SearchEngine.createWorkspaceScope(), 405 requestor, 406 new NullProgressMonitor()); 407 } catch (CoreException e) { 408 AdtPlugin.printErrorToConsole(e.getMessage()); 409 return Collections.emptyList(); 410 } 411 412 return requestor.getMatches(); 413 } 414 415 private static final Predicate<SearchMatch> MATCH_IS_FILE_PREDICATE = 416 new Predicate<SearchMatch>() { 417 @Override 418 public boolean apply(SearchMatch match) { 419 return match.getResource() instanceof IFile; 420 } 421 }; 422 423 private static final Predicate<SearchMatch> MATCH_IS_METHOD_PREDICATE = 424 new Predicate<SearchMatch>() { 425 @Override 426 public boolean apply(SearchMatch match) { 427 return match.getResource() instanceof IFile; 428 } 429 }; 430 431 private static class SearchResultAccumulator extends SearchRequestor { 432 private final List<SearchMatch> mSearchMatches = new ArrayList<SearchMatch>(); 433 private final Predicate<SearchMatch> mPredicate; 434 SearchResultAccumulator(Predicate<SearchMatch> filterPredicate)435 public SearchResultAccumulator(Predicate<SearchMatch> filterPredicate) { 436 mPredicate = filterPredicate; 437 } 438 getMatches()439 public List<SearchMatch> getMatches() { 440 return mSearchMatches; 441 } 442 443 @Override acceptSearchMatch(SearchMatch match)444 public void acceptSearchMatch(SearchMatch match) throws CoreException { 445 if (mPredicate.apply(match)) { 446 mSearchMatches.add(match); 447 } 448 } 449 } 450 switchToPerspective(String perspectiveId)451 private static void switchToPerspective(String perspectiveId) { 452 IWorkbench workbench = PlatformUI.getWorkbench(); 453 IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); 454 IPerspectiveRegistry perspectiveRegistry = workbench.getPerspectiveRegistry(); 455 if (perspectiveId != null 456 && perspectiveId.length() > 0 457 && perspectiveRegistry.findPerspectiveWithId(perspectiveId) != null) { 458 try { 459 workbench.showPerspective(perspectiveId, window); 460 } catch (WorkbenchException e) { 461 AdtPlugin.printErrorToConsole(e.getMessage()); 462 } 463 } 464 } 465 } 466