/* * Copyright (C) 2010 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; import static com.android.SdkConstants.CLASS_CONSTRUCTOR; import static com.android.SdkConstants.CONSTRUCTOR_NAME; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.ide.eclipse.adt.internal.project.ProjectHelper; import com.android.ide.eclipse.ddms.ISourceRevealer; import com.google.common.base.Predicate; 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.core.runtime.NullProgressMonitor; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.ISourceRange; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.search.IJavaSearchConstants; import org.eclipse.jdt.core.search.SearchEngine; import org.eclipse.jdt.core.search.SearchMatch; import org.eclipse.jdt.core.search.SearchParticipant; import org.eclipse.jdt.core.search.SearchPattern; import org.eclipse.jdt.core.search.SearchRequestor; import org.eclipse.jdt.ui.JavaUI; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.window.Window; import org.eclipse.ui.IPerspectiveRegistry; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.WorkbenchException; import org.eclipse.ui.dialogs.ListDialog; import org.eclipse.ui.ide.IDE; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Implementation of the com.android.ide.ddms.sourceRevealer extension point. * Note that this code is duplicated in the PDT plugin's SourceRevealer as well. */ public class SourceRevealer implements ISourceRevealer { @Override public boolean reveal(String applicationName, String className, int line) { IProject project = ProjectHelper.findAndroidProjectByAppName(applicationName); if (project != null) { return BaseProjectHelper.revealSource(project, className, line); } return false; } /** * Reveal the source for given fully qualified method name.<br> * * The method should take care of the following scenarios:<ol> * <li> A search, either by filename/line number, or for fqmn might provide only 1 result. * In such a case, just open that result. Give preference to the file name/line # search * since that is the most accurate (gets to the line number). </li> * <li> The search might not provide any results. e.g, the method name may be of the form * "com.x.y$1.methodName". Searches for methods within anonymous classes will fail. In * such a case, if the fileName:lineNumber argument is available, a search for that * should be made instead. </li> * <li> The search might provide multiple results. In such a case, the fileName/lineNumber * values should be utilized to narrow down the results.</li> * </ol> * * @param fqmn fully qualified method name * @param fileName file name in which the method is present, null if not known * @param lineNumber line number in the file which should be given focus, -1 if not known. * Line numbers begin at 1, not 0. * @param perspective perspective to switch to before the source is revealed, null to not * switch perspectives */ @Override public boolean revealMethod(String fqmn, String fileName, int lineNumber, String perspective) { // Search by filename:linenumber. If there is just one result for it, that would // be the correct match that is accurate to the line List<SearchMatch> fileMatches = Collections.emptyList(); if (fileName != null && lineNumber >= 0) { fileMatches = searchForFile(fileName); if (fileMatches.size() == 1) { return revealLineMatch(fileMatches, fileName, lineNumber, perspective); } } List<SearchMatch> methodMatches = searchForMethod(fqmn); // if there is a unique method name match: // 1. if there are > 1 file name matches, try to see if they can be narrowed down // 2. if not, display the method match if (methodMatches.size() == 1) { if (fileMatches.size() > 0) { List<SearchMatch> filteredMatches = filterMatchByResource(fileMatches, methodMatches.get(0).getResource()); if (filteredMatches.size() == 1) { return revealLineMatch(filteredMatches, fileName, lineNumber, perspective); } } else if (fileName != null && lineNumber > 0) { // Couldn't find file match, but we have a filename and line number: attempt // to use this to pinpoint the location within the method IMethod method = (IMethod) methodMatches.get(0).getElement(); IJavaElement element = method; while (element != null) { if (element instanceof ICompilationUnit) { ICompilationUnit unit = ((ICompilationUnit) element).getPrimary(); IResource resource = unit.getResource(); if (resource instanceof IFile) { IFile file = (IFile) resource; try { // See if the line number looks like it's inside the given method ISourceRange sourceRange = method.getSourceRange(); IRegion region = AdtUtils.getRegionOfLine(file, lineNumber - 1); // When fields are initialized with code, this logically belongs // to the constructor, but the line numbers are outside of the // constructor. In this case we'll trust the line number rather // than the method range. boolean isConstructor = fqmn.endsWith(CONSTRUCTOR_NAME); if (isConstructor || region != null && region.getOffset() >= sourceRange.getOffset() && region.getOffset() < sourceRange.getOffset() + sourceRange.getLength()) { // Yes: use the line number instead if (perspective != null) { SourceRevealer.switchToPerspective(perspective); } return displayFile(file, lineNumber); } } catch (JavaModelException e) { AdtPlugin.log(e, null); } } } element = element.getParent(); } } return displayMethod((IMethod) methodMatches.get(0).getElement(), perspective); } // no matches for search by method, so search by filename if (methodMatches.size() == 0) { if (fileMatches.size() > 0) { return revealLineMatch(fileMatches, fileName, lineNumber, perspective); } else { // Last ditch effort: attempt to look up the class corresponding to the fqn // and jump to the line there if (fileMatches.isEmpty() && fqmn.indexOf('.') != -1) { String className = fqmn.substring(0, fqmn.lastIndexOf('.')); for (IJavaProject project : BaseProjectHelper.getAndroidProjects(null)) { IType type; try { type = project.findType(className); if (type != null && type.exists()) { IResource resource = type.getResource(); if (resource instanceof IFile) { if (perspective != null) { SourceRevealer.switchToPerspective(perspective); } return displayFile((IFile) resource, lineNumber); } } } catch (JavaModelException e) { AdtPlugin.log(e, null); } } } return false; } } // multiple matches for search by method, narrow down by filename if (fileName != null) { return revealLineMatch( filterMatchByFileName(methodMatches, fileName), fileName, lineNumber, perspective); } // prompt the user SearchMatch match = getMatchToDisplay(methodMatches, fqmn); if (match == null) { return false; } else { return displayMethod((IMethod) match.getElement(), perspective); } } private boolean revealLineMatch(List<SearchMatch> matches, String fileName, int lineNumber, String perspective) { SearchMatch match = getMatchToDisplay(matches, String.format("%s:%d", fileName, lineNumber)); if (match == null) { return false; } if (perspective != null) { SourceRevealer.switchToPerspective(perspective); } return displayFile((IFile) match.getResource(), lineNumber); } private boolean displayFile(IFile file, int lineNumber) { try { IMarker marker = file.createMarker(IMarker.TEXT); marker.setAttribute(IMarker.LINE_NUMBER, lineNumber); IDE.openEditor( PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(), marker); marker.delete(); return true; } catch (CoreException e) { AdtPlugin.printErrorToConsole(e.getMessage()); return false; } } private boolean displayMethod(IMethod method, String perspective) { if (perspective != null) { SourceRevealer.switchToPerspective(perspective); } try { JavaUI.openInEditor(method); return true; } catch (Exception e) { AdtPlugin.printErrorToConsole(e.getMessage()); return false; } } private List<SearchMatch> filterMatchByFileName(List<SearchMatch> matches, String fileName) { if (fileName == null) { return matches; } // Use a map to collapse multiple matches in a single file into just one match since // we know the line number in the file. Map<IResource, SearchMatch> matchesPerFile = new HashMap<IResource, SearchMatch>(matches.size()); for (SearchMatch m: matches) { if (m.getResource() instanceof IFile && m.getResource().getName().startsWith(fileName)) { matchesPerFile.put(m.getResource(), m); } } List<SearchMatch> filteredMatches = new ArrayList<SearchMatch>(matchesPerFile.values()); // sort results, first by project name, then by file name Collections.sort(filteredMatches, new Comparator<SearchMatch>() { @Override public int compare(SearchMatch m1, SearchMatch m2) { String p1 = m1.getResource().getProject().getName(); String p2 = m2.getResource().getProject().getName(); if (!p1.equals(p2)) { return p1.compareTo(p2); } String r1 = m1.getResource().getName(); String r2 = m2.getResource().getName(); return r1.compareTo(r2); } }); return filteredMatches; } private List<SearchMatch> filterMatchByResource(List<SearchMatch> matches, IResource resource) { List<SearchMatch> filteredMatches = new ArrayList<SearchMatch>(matches.size()); for (SearchMatch m: matches) { if (m.getResource().equals(resource)) { filteredMatches.add(m); } } return filteredMatches; } private SearchMatch getMatchToDisplay(List<SearchMatch> matches, String searchTerm) { // no matches for given search if (matches.size() == 0) { return null; } // there is only 1 match, so we return that if (matches.size() == 1) { return matches.get(0); } // multiple matches, prompt the user to select IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); if (window == null) { return null; } ListDialog dlg = new ListDialog(window.getShell()); dlg.setMessage("Multiple files match search: " + searchTerm); dlg.setTitle("Select file to open"); dlg.setInput(matches); dlg.setContentProvider(new IStructuredContentProvider() { @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { } @Override public void dispose() { } @Override public Object[] getElements(Object inputElement) { return ((List<?>) inputElement).toArray(); } }); dlg.setLabelProvider(new LabelProvider() { @Override public String getText(Object element) { SearchMatch m = (SearchMatch) element; return String.format("/%s/%s", //$NON-NLS-1$ m.getResource().getProject().getName(), m.getResource().getProjectRelativePath().toString()); } }); dlg.setInitialSelections(new Object[] { matches.get(0) }); dlg.setHelpAvailable(false); if (dlg.open() == Window.OK) { Object[] selectedMatches = dlg.getResult(); if (selectedMatches.length > 0) { return (SearchMatch) selectedMatches[0]; } } return null; } private List<SearchMatch> searchForFile(String fileName) { return searchForPattern(fileName, IJavaSearchConstants.CLASS, MATCH_IS_FILE_PREDICATE); } private List<SearchMatch> searchForMethod(String fqmn) { if (fqmn.endsWith(CONSTRUCTOR_NAME)) { fqmn = fqmn.substring(0, fqmn.length() - CONSTRUCTOR_NAME.length() - 1); // -1: dot return searchForPattern(fqmn, IJavaSearchConstants.CONSTRUCTOR, MATCH_IS_METHOD_PREDICATE); } if (fqmn.endsWith(CLASS_CONSTRUCTOR)) { // Don't try to search for class init methods: Eclipse will throw NPEs if you do return Collections.emptyList(); } return searchForPattern(fqmn, IJavaSearchConstants.METHOD, MATCH_IS_METHOD_PREDICATE); } private List<SearchMatch> searchForPattern(String pattern, int searchFor, Predicate<SearchMatch> filterPredicate) { SearchEngine se = new SearchEngine(); SearchPattern searchPattern = SearchPattern.createPattern( pattern, searchFor, IJavaSearchConstants.DECLARATIONS, SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE); SearchResultAccumulator requestor = new SearchResultAccumulator(filterPredicate); try { se.search(searchPattern, new SearchParticipant[] {SearchEngine.getDefaultSearchParticipant()}, SearchEngine.createWorkspaceScope(), requestor, new NullProgressMonitor()); } catch (CoreException e) { AdtPlugin.printErrorToConsole(e.getMessage()); return Collections.emptyList(); } return requestor.getMatches(); } private static final Predicate<SearchMatch> MATCH_IS_FILE_PREDICATE = new Predicate<SearchMatch>() { @Override public boolean apply(SearchMatch match) { return match.getResource() instanceof IFile; } }; private static final Predicate<SearchMatch> MATCH_IS_METHOD_PREDICATE = new Predicate<SearchMatch>() { @Override public boolean apply(SearchMatch match) { return match.getResource() instanceof IFile; } }; private static class SearchResultAccumulator extends SearchRequestor { private final List<SearchMatch> mSearchMatches = new ArrayList<SearchMatch>(); private final Predicate<SearchMatch> mPredicate; public SearchResultAccumulator(Predicate<SearchMatch> filterPredicate) { mPredicate = filterPredicate; } public List<SearchMatch> getMatches() { return mSearchMatches; } @Override public void acceptSearchMatch(SearchMatch match) throws CoreException { if (mPredicate.apply(match)) { mSearchMatches.add(match); } } } private static void switchToPerspective(String perspectiveId) { IWorkbench workbench = PlatformUI.getWorkbench(); IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); IPerspectiveRegistry perspectiveRegistry = workbench.getPerspectiveRegistry(); if (perspectiveId != null && perspectiveId.length() > 0 && perspectiveRegistry.findPerspectiveWithId(perspectiveId) != null) { try { workbench.showPerspective(perspectiveId, window); } catch (WorkbenchException e) { AdtPlugin.printErrorToConsole(e.getMessage()); } } } }