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