| /* |
| * 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()); |
| } |
| } |
| } |
| } |