52fdb9d21ef4198bde08c87b0eeb47332a034347
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / navigation / actions / GotoDeclarationAction.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
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 package com.intellij.codeInsight.navigation.actions;
17
18 import com.intellij.codeInsight.CodeInsightActionHandler;
19 import com.intellij.codeInsight.CodeInsightBundle;
20 import com.intellij.codeInsight.TargetElementUtil;
21 import com.intellij.codeInsight.actions.BaseCodeInsightAction;
22 import com.intellij.codeInsight.hint.HintManager;
23 import com.intellij.codeInsight.navigation.NavigationUtil;
24 import com.intellij.featureStatistics.FeatureUsageTracker;
25 import com.intellij.find.actions.ShowUsagesAction;
26 import com.intellij.ide.util.DefaultPsiElementCellRenderer;
27 import com.intellij.ide.util.EditSourceUtil;
28 import com.intellij.injected.editor.EditorWindow;
29 import com.intellij.openapi.actionSystem.ActionManager;
30 import com.intellij.openapi.actionSystem.AnActionEvent;
31 import com.intellij.openapi.actionSystem.Presentation;
32 import com.intellij.openapi.diagnostic.Logger;
33 import com.intellij.openapi.editor.Document;
34 import com.intellij.openapi.editor.Editor;
35 import com.intellij.openapi.editor.LogicalPosition;
36 import com.intellij.openapi.editor.ex.EditorGutterComponentEx;
37 import com.intellij.openapi.editor.ex.util.EditorUtil;
38 import com.intellij.openapi.extensions.ExtensionException;
39 import com.intellij.openapi.extensions.Extensions;
40 import com.intellij.openapi.fileEditor.OpenFileDescriptor;
41 import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory;
42 import com.intellij.openapi.project.DumbAware;
43 import com.intellij.openapi.project.DumbService;
44 import com.intellij.openapi.project.IndexNotReadyException;
45 import com.intellij.openapi.project.Project;
46 import com.intellij.openapi.ui.popup.JBPopupFactory;
47 import com.intellij.openapi.util.TextRange;
48 import com.intellij.pom.Navigatable;
49 import com.intellij.psi.*;
50 import com.intellij.psi.search.PsiElementProcessor;
51 import com.intellij.psi.util.PsiUtilCore;
52 import com.intellij.ui.awt.RelativePoint;
53 import org.jetbrains.annotations.NotNull;
54 import org.jetbrains.annotations.Nullable;
55
56 import javax.swing.*;
57 import java.awt.*;
58 import java.awt.event.InputEvent;
59 import java.awt.event.MouseEvent;
60 import java.text.MessageFormat;
61 import java.util.Arrays;
62 import java.util.Collection;
63 import java.util.Collections;
64
65 public class GotoDeclarationAction extends BaseCodeInsightAction implements CodeInsightActionHandler, DumbAware {
66   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.navigation.actions.GotoDeclarationAction");
67
68   @NotNull
69   @Override
70   protected CodeInsightActionHandler getHandler() {
71     return this;
72   }
73
74   @Override
75   protected boolean isValidForLookup() {
76     return true;
77   }
78
79   @Override
80   public void invoke(@NotNull final Project project, @NotNull Editor editor, @NotNull PsiFile file) {
81     PsiDocumentManager.getInstance(project).commitAllDocuments();
82
83     DumbService.getInstance(project).setAlternativeResolveEnabled(true);
84     try {
85       int offset = editor.getCaretModel().getOffset();
86       PsiElement[] elements = findAllTargetElements(project, editor, offset);
87       FeatureUsageTracker.getInstance().triggerFeatureUsed("navigation.goto.declaration");
88
89       if (elements.length != 1) {
90         if (elements.length == 0 && suggestCandidates(TargetElementUtil.findReference(editor, offset)).isEmpty()) {
91           PsiElement element = findElementToShowUsagesOf(editor, editor.getCaretModel().getOffset());
92           if (element != null) {
93             ShowUsagesAction showUsages = (ShowUsagesAction)ActionManager.getInstance().getAction(ShowUsagesAction.ID);
94             RelativePoint popupPosition = JBPopupFactory.getInstance().guessBestPopupLocation(editor);
95             showUsages.startFindUsages(element, popupPosition, editor, ShowUsagesAction.USAGES_PAGE_SIZE);
96             return;
97           }
98         }
99         chooseAmbiguousTarget(editor, offset, elements, file);
100         return;
101       }
102
103       PsiElement element = elements[0];
104       PsiElement navElement = element.getNavigationElement();
105       navElement = TargetElementUtil.getInstance().getGotoDeclarationTarget(element, navElement);
106       if (navElement != null) {
107         gotoTargetElement(navElement, editor, file);
108       }
109     }
110     catch (IndexNotReadyException e) {
111       DumbService.getInstance(project).showDumbModeNotification("Navigation is not available here during index update");
112     }
113     finally {
114       DumbService.getInstance(project).setAlternativeResolveEnabled(false);
115     }
116   }
117
118   public static PsiNameIdentifierOwner findElementToShowUsagesOf(@NotNull Editor editor, int offset) {
119     PsiElement elementAt = TargetElementUtil.getInstance().findTargetElement(editor, TargetElementUtil.ELEMENT_NAME_ACCEPTED, offset);
120     if (elementAt instanceof PsiNameIdentifierOwner) {
121       return (PsiNameIdentifierOwner)elementAt;
122     }
123     return null;
124   }
125
126   private static void chooseAmbiguousTarget(final Editor editor, int offset, PsiElement[] elements, PsiFile currentFile) {
127     PsiElementProcessor<PsiElement> navigateProcessor = element -> {
128       gotoTargetElement(element, editor, currentFile);
129       return true;
130     };
131     boolean found =
132       chooseAmbiguousTarget(editor, offset, navigateProcessor, CodeInsightBundle.message("declaration.navigation.title"), elements);
133     if (!found) {
134       HintManager.getInstance().showErrorHint(editor, "Cannot find declaration to go to");
135     }
136   }
137
138   private static void gotoTargetElement(@NotNull PsiElement element, @NotNull Editor currentEditor, @NotNull PsiFile currentFile) {
139     if (element.getContainingFile() == currentFile) {
140       Project project = element.getProject();
141       IdeDocumentHistory.getInstance(project).includeCurrentCommandAsNavigation();
142       new OpenFileDescriptor(project, currentFile.getViewProvider().getVirtualFile(), element.getTextOffset()).navigateIn(currentEditor);
143       return;
144     }
145
146     Navigatable navigatable = element instanceof Navigatable ? (Navigatable)element : EditSourceUtil.getDescriptor(element);
147     if (navigatable != null && navigatable.canNavigate()) {
148       navigatable.navigate(true);
149     }
150   }
151
152   // returns true if processor is run or is going to be run after showing popup
153   public static boolean chooseAmbiguousTarget(@NotNull Editor editor,
154                                               int offset,
155                                               @NotNull PsiElementProcessor<PsiElement> processor,
156                                               @NotNull String titlePattern,
157                                               @Nullable PsiElement[] elements) {
158     if (TargetElementUtil.inVirtualSpace(editor, offset)) {
159       return false;
160     }
161
162     final PsiReference reference = TargetElementUtil.findReference(editor, offset);
163
164     if (elements == null || elements.length == 0) {
165       final Collection<PsiElement> candidates = suggestCandidates(reference);
166       elements = PsiUtilCore.toPsiElementArray(candidates);
167     }
168
169     if (elements.length == 1) {
170       PsiElement element = elements[0];
171       LOG.assertTrue(element != null);
172       processor.execute(element);
173       return true;
174     }
175     if (elements.length > 1) {
176       String title;
177
178       if (reference == null) {
179         title = titlePattern;
180       }
181       else {
182         final TextRange range = reference.getRangeInElement();
183         final String elementText = reference.getElement().getText();
184         LOG.assertTrue(range.getStartOffset() >= 0 && range.getEndOffset() <= elementText.length(), Arrays.toString(elements) + ";" + reference);
185         final String refText = range.substring(elementText);
186         title = MessageFormat.format(titlePattern, refText);
187       }
188
189       NavigationUtil.getPsiElementPopup(elements, new DefaultPsiElementCellRenderer(), title, processor).showInBestPositionFor(editor);
190       return true;
191     }
192     return false;
193   }
194
195   @NotNull
196   private static Collection<PsiElement> suggestCandidates(@Nullable PsiReference reference) {
197     if (reference == null) {
198       return Collections.emptyList();
199     }
200     return TargetElementUtil.getInstance().getTargetCandidates(reference);
201   }
202
203   @Override
204   public boolean startInWriteAction() {
205     return false;
206   }
207
208   @Nullable
209   public static PsiElement findTargetElement(Project project, Editor editor, int offset) {
210     final PsiElement[] targets = findAllTargetElements(project, editor, offset);
211     return targets.length == 1 ? targets[0] : null;
212   }
213
214   @NotNull
215   public static PsiElement[] findAllTargetElements(Project project, Editor editor, int offset) {
216     if (TargetElementUtil.inVirtualSpace(editor, offset)) {
217       return PsiElement.EMPTY_ARRAY;
218     }
219
220     final PsiElement[] targets = findTargetElementsNoVS(project, editor, offset, true);
221     return targets != null ? targets : PsiElement.EMPTY_ARRAY;
222   }
223
224   @Nullable
225   public static PsiElement[] findTargetElementsNoVS(Project project, Editor editor, int offset, boolean lookupAccepted) {
226     final Document document = editor.getDocument();
227     PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(document);
228     if (file == null) return null;
229
230     if (file instanceof PsiCompiledFile) {
231       PsiFile decompiled = ((PsiCompiledFile)file).getDecompiledPsiFile();
232       if (decompiled != null) file = decompiled;
233     }
234
235     PsiElement elementAt = file.findElementAt(TargetElementUtil.adjustOffset(file, document, offset));
236     for (GotoDeclarationHandler handler : Extensions.getExtensions(GotoDeclarationHandler.EP_NAME)) {
237       try {
238         PsiElement[] result = handler.getGotoDeclarationTargets(elementAt, offset, editor);
239         if (result != null && result.length > 0) {
240           for (PsiElement element : result) {
241             if (element == null) {
242               LOG.error("Null target element is returned by " + handler.getClass().getName());
243               return null;
244             }
245           }
246           return result;
247         }
248       }
249       catch (AbstractMethodError e) {
250         LOG.error(new ExtensionException(handler.getClass()));
251       }
252     }
253
254     int flags = TargetElementUtil.getInstance().getAllAccepted() & ~TargetElementUtil.ELEMENT_NAME_ACCEPTED;
255     if (!lookupAccepted) {
256       flags &= ~TargetElementUtil.LOOKUP_ITEM_ACCEPTED;
257     }
258     PsiElement element = TargetElementUtil.getInstance().findTargetElement(editor, flags, offset);
259     if (element != null) {
260       return new PsiElement[]{element};
261     }
262
263     // if no references found in injected fragment, try outer document
264     if (editor instanceof EditorWindow) {
265       EditorWindow window = (EditorWindow)editor;
266       return findTargetElementsNoVS(project, window.getDelegate(), window.getDocument().injectedToHost(offset), lookupAccepted);
267     }
268
269     return null;
270   }
271
272   @Override
273   public void update(final AnActionEvent event) {
274     InputEvent inputEvent = event.getInputEvent();
275     if (inputEvent instanceof MouseEvent) {
276       Component component = inputEvent.getComponent();
277       if (component != null) {
278         Point point = ((MouseEvent)inputEvent).getPoint();
279         Component componentAt = SwingUtilities.getDeepestComponentAt(component, point.x, point.y);
280         Project project = event.getProject();
281         if (project == null) {
282           event.getPresentation().setEnabled(false);
283           return;
284         }
285
286         Editor editor = getBaseEditor(event.getDataContext(), project);
287         if (componentAt instanceof EditorGutterComponentEx) {
288           event.getPresentation().setEnabled(false);
289           return;
290         }
291         else if (editor != null && componentAt == editor.getContentComponent()) {
292           LogicalPosition pos = editor.xyToLogicalPosition(SwingUtilities.convertPoint(component, point, componentAt));
293           if (EditorUtil.inVirtualSpace(editor, pos)) {
294             event.getPresentation().setEnabled(false);
295             return;
296           }
297         }
298       }
299     }
300
301     for (GotoDeclarationHandler handler : Extensions.getExtensions(GotoDeclarationHandler.EP_NAME)) {
302       try {
303         String text = handler.getActionText(event.getDataContext());
304         if (text != null) {
305           Presentation presentation = event.getPresentation();
306           presentation.setText(text);
307           break;
308         }
309       }
310       catch (AbstractMethodError e) {
311         LOG.error(handler.toString(), e);
312       }
313     }
314
315     super.update(event);
316   }
317 }