Home | History | Annotate | Download | only in adt
      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
     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
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    374     private List<SearchMatch> searchForFile(String fileName) {
    375         return searchForPattern(fileName, IJavaSearchConstants.CLASS, MATCH_IS_FILE_PREDICATE);
    376     }
    377 
    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 
    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 
    435         public SearchResultAccumulator(Predicate<SearchMatch> filterPredicate) {
    436             mPredicate = filterPredicate;
    437         }
    438 
    439         public List<SearchMatch> getMatches() {
    440             return mSearchMatches;
    441         }
    442 
    443         @Override
    444         public void acceptSearchMatch(SearchMatch match) throws CoreException {
    445             if (mPredicate.apply(match)) {
    446                 mSearchMatches.add(match);
    447             }
    448         }
    449     }
    450 
    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