diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/SourceRevealer.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/SourceRevealer.java | 465 |
1 files changed, 465 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/SourceRevealer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/SourceRevealer.java new file mode 100644 index 000000000..b1b5390ee --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/SourceRevealer.java @@ -0,0 +1,465 @@ +/* + * 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()); + } + } + } +} |