94e1933d5cbb9f292adb7db88b4a5dfe2341a729
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / navigation / GotoTargetHandler.java
1 /*
2  * Copyright 2000-2014 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
17 package com.intellij.codeInsight.navigation;
18
19 import com.intellij.codeInsight.CodeInsightActionHandler;
20 import com.intellij.codeInsight.hint.HintManager;
21 import com.intellij.featureStatistics.FeatureUsageTracker;
22 import com.intellij.find.FindUtil;
23 import com.intellij.ide.util.EditSourceUtil;
24 import com.intellij.ide.util.PsiElementListCellRenderer;
25 import com.intellij.navigation.ItemPresentation;
26 import com.intellij.navigation.NavigationItem;
27 import com.intellij.openapi.application.ApplicationManager;
28 import com.intellij.openapi.editor.Editor;
29 import com.intellij.openapi.extensions.Extensions;
30 import com.intellij.openapi.progress.ProgressManager;
31 import com.intellij.openapi.project.DumbService;
32 import com.intellij.openapi.project.IndexNotReadyException;
33 import com.intellij.openapi.project.Project;
34 import com.intellij.openapi.ui.popup.JBPopup;
35 import com.intellij.openapi.ui.popup.PopupChooserBuilder;
36 import com.intellij.openapi.util.Computable;
37 import com.intellij.openapi.util.Ref;
38 import com.intellij.pom.Navigatable;
39 import com.intellij.psi.PsiElement;
40 import com.intellij.psi.PsiFile;
41 import com.intellij.psi.PsiNamedElement;
42 import com.intellij.ui.CollectionListModel;
43 import com.intellij.ui.JBListWithHintProvider;
44 import com.intellij.ui.popup.AbstractPopup;
45 import com.intellij.ui.popup.HintUpdateSupply;
46 import com.intellij.usages.UsageView;
47 import com.intellij.util.ArrayUtil;
48 import com.intellij.util.Function;
49 import com.intellij.util.Processor;
50 import com.intellij.util.containers.HashSet;
51 import org.jetbrains.annotations.NonNls;
52 import org.jetbrains.annotations.NotNull;
53 import org.jetbrains.annotations.Nullable;
54
55 import javax.swing.*;
56 import java.awt.*;
57 import java.util.*;
58 import java.util.List;
59
60 public abstract class GotoTargetHandler implements CodeInsightActionHandler {
61   private static final PsiElementListCellRenderer ourDefaultTargetElementRenderer = new DefaultPsiElementListCellRenderer();
62   private final DefaultListCellRenderer myActionElementRenderer = new ActionCellRenderer();
63
64   @Override
65   public boolean startInWriteAction() {
66     return false;
67   }
68
69   @Override
70   public void invoke(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
71     FeatureUsageTracker.getInstance().triggerFeatureUsed(getFeatureUsedKey());
72
73     try {
74       GotoData gotoData = getSourceAndTargetElements(editor, file);
75       if (gotoData != null && gotoData.source != null) {
76         show(project, editor, file, gotoData);
77       }
78     }
79     catch (IndexNotReadyException e) {
80       DumbService.getInstance(project).showDumbModeNotification("Navigation is not available here during index update");
81     }
82   }
83
84   @NonNls
85   protected abstract String getFeatureUsedKey();
86
87   @Nullable
88   protected abstract GotoData getSourceAndTargetElements(Editor editor, PsiFile file);
89
90   private void show(@NotNull final Project project,
91                     @NotNull Editor editor,
92                     @NotNull PsiFile file,
93                     @NotNull final GotoData gotoData) {
94     final PsiElement[] targets = gotoData.targets;
95     final List<AdditionalAction> additionalActions = gotoData.additionalActions;
96
97     if (targets.length == 0 && additionalActions.isEmpty()) {
98       HintManager.getInstance().showErrorHint(editor, getNotFoundMessage(project, editor, file));
99       return;
100     }
101
102     if (targets.length == 1 && additionalActions.isEmpty()) {
103       Navigatable descriptor = targets[0] instanceof Navigatable ? (Navigatable)targets[0] : EditSourceUtil.getDescriptor(targets[0]);
104       if (descriptor != null && descriptor.canNavigate()) {
105         navigateToElement(descriptor);
106       }
107       return;
108     }
109
110     for (PsiElement eachTarget : targets) {
111       gotoData.renderers.put(eachTarget, createRenderer(gotoData, eachTarget));
112     }
113
114     final String name = ((PsiNamedElement)gotoData.source).getName();
115     final String title = getChooserTitle(gotoData.source, name, targets.length);
116
117     if (shouldSortTargets()) {
118       Arrays.sort(targets, createComparator(gotoData.renderers, gotoData));
119     }
120
121     List<Object> allElements = new ArrayList<Object>(targets.length + additionalActions.size());
122     Collections.addAll(allElements, targets);
123     allElements.addAll(additionalActions);
124
125     final JBListWithHintProvider list = new JBListWithHintProvider(new CollectionListModel<Object>(allElements)) {
126       @Override
127       protected PsiElement getPsiElementForHint(final Object selectedValue) {
128         return selectedValue instanceof PsiElement ? (PsiElement) selectedValue : null;
129       }
130     };
131     
132     list.setCellRenderer(new DefaultListCellRenderer() {
133       @Override
134       public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
135         if (value == null) return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
136         if (value instanceof AdditionalAction) {
137           return myActionElementRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
138         }
139         PsiElementListCellRenderer renderer = getRenderer(value, gotoData.renderers, gotoData);
140         return renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
141       }
142     });
143
144     final Runnable runnable = new Runnable() {
145       @Override
146       public void run() {
147         int[] ids = list.getSelectedIndices();
148         if (ids == null || ids.length == 0) return;
149         Object[] selectedElements = list.getSelectedValues();
150         for (Object element : selectedElements) {
151           if (element instanceof AdditionalAction) {
152             ((AdditionalAction)element).execute();
153           }
154           else {
155             Navigatable nav = element instanceof Navigatable ? (Navigatable)element : EditSourceUtil.getDescriptor((PsiElement)element);
156             try {
157               if (nav != null && nav.canNavigate()) {
158                 navigateToElement(nav);
159               }
160             }
161             catch (IndexNotReadyException e) {
162               DumbService.getInstance(project).showDumbModeNotification("Navigation is not available while indexing");
163             }
164           }
165         }
166       }
167     };
168
169     final PopupChooserBuilder builder = new PopupChooserBuilder(list);
170     builder.setFilteringEnabled(new Function<Object, String>() {
171       @Override
172       public String fun(Object o) {
173         if (o instanceof AdditionalAction) {
174           return ((AdditionalAction)o).getText();
175         }
176         return getRenderer(o, gotoData.renderers, gotoData).getElementText((PsiElement)o);
177       }
178     });
179
180     final Ref<UsageView> usageView = new Ref<UsageView>();
181     final JBPopup popup = builder.
182       setTitle(title).
183       setItemChoosenCallback(runnable).
184       setMovable(true).
185       setCancelCallback(new Computable<Boolean>() {
186         @Override
187         public Boolean compute() {
188           HintUpdateSupply.hideHint(list);
189           return true;
190         }
191       }).
192       setCouldPin(new Processor<JBPopup>() {
193         @Override
194         public boolean process(JBPopup popup) {
195           usageView.set(FindUtil.showInUsageView(gotoData.source, gotoData.targets, getFindUsagesTitle(gotoData.source, name, gotoData.targets.length), project));
196           popup.cancel();
197           return false;
198         }
199       }).
200       setAdText(getAdText(gotoData.source, targets.length)).
201       createPopup();
202     if (gotoData.listUpdaterTask != null) {
203       gotoData.listUpdaterTask.init((AbstractPopup)popup, list, usageView);
204       ProgressManager.getInstance().run(gotoData.listUpdaterTask);
205     }
206     popup.showInBestPositionFor(editor);
207   }
208
209   private static PsiElementListCellRenderer getRenderer(Object value,
210                                                         Map<Object, PsiElementListCellRenderer> targetsWithRenderers,
211                                                         GotoData gotoData) {
212     PsiElementListCellRenderer renderer = targetsWithRenderers.get(value);
213     if (renderer == null) {
214       renderer = gotoData.getRenderer(value);
215     }
216     if (renderer != null) {
217       return renderer;
218     }
219     else {
220       return ourDefaultTargetElementRenderer;
221     }
222   }
223
224   protected static Comparator<PsiElement> createComparator(final Map<Object, PsiElementListCellRenderer> targetsWithRenderers,
225                                                            final GotoData gotoData) {
226     return new Comparator<PsiElement>() {
227       @Override
228       public int compare(PsiElement o1, PsiElement o2) {
229         return getComparingObject(o1).compareTo(getComparingObject(o2));
230       }
231
232       private Comparable getComparingObject(PsiElement o1) {
233         return getRenderer(o1, targetsWithRenderers, gotoData).getComparingObject(o1);
234       }
235     };
236   }
237
238   protected static PsiElementListCellRenderer createRenderer(GotoData gotoData, PsiElement eachTarget) {
239     PsiElementListCellRenderer renderer = null;
240     for (GotoTargetRendererProvider eachProvider : Extensions.getExtensions(GotoTargetRendererProvider.EP_NAME)) {
241       renderer = eachProvider.getRenderer(eachTarget, gotoData);
242       if (renderer != null) break;
243     }
244     if (renderer == null) {
245       renderer = ourDefaultTargetElementRenderer;
246     }
247     return renderer;
248   }
249
250
251   protected void navigateToElement(Navigatable descriptor) {
252     descriptor.navigate(true);
253   }
254
255   protected boolean shouldSortTargets() {
256     return true;
257   }
258
259   @NotNull
260   protected abstract String getChooserTitle(PsiElement sourceElement, String name, int length);
261   @NotNull
262   protected String getFindUsagesTitle(PsiElement sourceElement, String name, int length) {
263     return getChooserTitle(sourceElement, name, length);
264   }
265
266   @NotNull
267   protected abstract String getNotFoundMessage(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file);
268
269   @Nullable
270   protected String getAdText(PsiElement source, int length) {
271     return null;
272   }
273
274   public interface AdditionalAction {
275     @NotNull
276     String getText();
277
278     Icon getIcon();
279
280     void execute();
281   }
282
283   public static class GotoData {
284     @NotNull public final PsiElement source;
285     public PsiElement[] targets;
286     public final List<AdditionalAction> additionalActions;
287
288     private boolean hasDifferentNames;
289     public ListBackgroundUpdaterTask listUpdaterTask;
290     protected final Set<String> myNames;
291     public Map<Object, PsiElementListCellRenderer> renderers = new HashMap<Object, PsiElementListCellRenderer>();
292
293     public GotoData(@NotNull PsiElement source, @NotNull PsiElement[] targets, @NotNull List<AdditionalAction> additionalActions) {
294       this.source = source;
295       this.targets = targets;
296       this.additionalActions = additionalActions;
297
298       myNames = new HashSet<String>();
299       for (PsiElement target : targets) {
300         if (target instanceof PsiNamedElement) {
301           myNames.add(((PsiNamedElement)target).getName());
302           if (myNames.size() > 1) break;
303         }
304       }
305
306       hasDifferentNames = myNames.size() > 1;
307     }
308
309     public boolean hasDifferentNames() {
310       return hasDifferentNames;
311     }
312
313     public boolean addTarget(final PsiElement element) {
314       if (ArrayUtil.find(targets, element) > -1) return false;
315       targets = ArrayUtil.append(targets, element);
316       renderers.put(element, createRenderer(this, element));
317       if (!hasDifferentNames && element instanceof PsiNamedElement) {
318         final String name = ApplicationManager.getApplication().runReadAction(new Computable<String>() {
319           @Override
320           public String compute() {
321             return ((PsiNamedElement)element).getName();
322           }
323         });
324         myNames.add(name);
325         hasDifferentNames = myNames.size() > 1;
326       }
327       return true;
328     }
329
330     public PsiElementListCellRenderer getRenderer(Object value) {
331       return renderers.get(value);
332     }
333   }
334
335   private static class DefaultPsiElementListCellRenderer extends PsiElementListCellRenderer {
336     @Override
337     public String getElementText(final PsiElement element) {
338       if (element instanceof PsiNamedElement) {
339         String name = ((PsiNamedElement)element).getName();
340         if (name != null) {
341           return name;
342         }
343       }
344       return element.getContainingFile().getName();
345     }
346
347     @Override
348     protected String getContainerText(final PsiElement element, final String name) {
349       if (element instanceof NavigationItem) {
350         final ItemPresentation presentation = ((NavigationItem)element).getPresentation();
351         return presentation != null ? presentation.getLocationString():null;
352       }
353
354       return null;
355     }
356
357     @Override
358     protected int getIconFlags() {
359       return 0;
360     }
361   }
362
363   private static class ActionCellRenderer extends DefaultListCellRenderer {
364     @Override
365     public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
366       Component result = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
367       if (value != null) {
368         AdditionalAction action = (AdditionalAction)value;
369         setText(action.getText());
370         setIcon(action.getIcon());
371       }
372       return result;
373     }
374   }
375 }