2 * Copyright 2000-2014 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.intellij.codeInsight.intention.impl;
19 import com.intellij.codeInsight.daemon.impl.HighlightInfo;
20 import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass;
21 import com.intellij.codeInsight.hint.HintManager;
22 import com.intellij.codeInsight.intention.EmptyIntentionAction;
23 import com.intellij.codeInsight.intention.HighPriorityAction;
24 import com.intellij.codeInsight.intention.IntentionAction;
25 import com.intellij.codeInsight.intention.LowPriorityAction;
26 import com.intellij.codeInsight.intention.impl.config.IntentionActionWrapper;
27 import com.intellij.codeInsight.intention.impl.config.IntentionManagerSettings;
28 import com.intellij.codeInspection.IntentionWrapper;
29 import com.intellij.codeInspection.LocalQuickFix;
30 import com.intellij.codeInspection.SuppressIntentionActionFromFix;
31 import com.intellij.codeInspection.ex.QuickFixWrapper;
32 import com.intellij.icons.AllIcons;
33 import com.intellij.openapi.application.ApplicationManager;
34 import com.intellij.openapi.diagnostic.Logger;
35 import com.intellij.openapi.editor.Editor;
36 import com.intellij.openapi.project.Project;
37 import com.intellij.openapi.ui.popup.*;
38 import com.intellij.openapi.util.Comparing;
39 import com.intellij.openapi.util.Iconable;
40 import com.intellij.psi.*;
41 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
42 import com.intellij.psi.util.PsiUtilBase;
43 import com.intellij.util.ThreeState;
44 import com.intellij.util.containers.ContainerUtil;
45 import gnu.trove.THashSet;
46 import gnu.trove.TObjectHashingStrategy;
47 import org.jetbrains.annotations.NotNull;
48 import org.jetbrains.annotations.Nullable;
56 class IntentionListStep implements ListPopupStep<IntentionActionWithTextCaching>, SpeedSearchFilter<IntentionActionWithTextCaching> {
57 private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.intention.impl.IntentionListStep");
59 private final Set<IntentionActionWithTextCaching> myCachedIntentions =
60 ContainerUtil.newConcurrentSet(ACTION_TEXT_AND_CLASS_EQUALS);
61 private final Set<IntentionActionWithTextCaching> myCachedErrorFixes =
62 ContainerUtil.newConcurrentSet(ACTION_TEXT_AND_CLASS_EQUALS);
63 private final Set<IntentionActionWithTextCaching> myCachedInspectionFixes = ContainerUtil.newConcurrentSet(ACTION_TEXT_AND_CLASS_EQUALS);
64 private final Set<IntentionActionWithTextCaching> myCachedGutters = ContainerUtil.newConcurrentSet(ACTION_TEXT_AND_CLASS_EQUALS);
65 private final IntentionManagerSettings mySettings;
67 private final IntentionHintComponent myIntentionHintComponent;
68 private final Editor myEditor;
69 private final PsiFile myFile;
70 private final Project myProject;
71 private static final TObjectHashingStrategy<IntentionActionWithTextCaching> ACTION_TEXT_AND_CLASS_EQUALS = new TObjectHashingStrategy<IntentionActionWithTextCaching>() {
73 public int computeHashCode(final IntentionActionWithTextCaching object) {
74 return object.getText().hashCode();
78 public boolean equals(final IntentionActionWithTextCaching o1, final IntentionActionWithTextCaching o2) {
79 return o1.getAction().getClass() == o2.getAction().getClass() && o1.getText().equals(o2.getText());
82 private Runnable myFinalRunnable;
84 IntentionListStep(@Nullable IntentionHintComponent intentionHintComponent,
85 @NotNull ShowIntentionsPass.IntentionsInfo intentions,
86 @NotNull Editor editor,
87 @NotNull PsiFile file,
88 @NotNull Project project) {
89 this(intentionHintComponent, editor, file, project);
90 updateActions(intentions);
93 IntentionListStep(@Nullable IntentionHintComponent intentionHintComponent,
94 @NotNull Editor editor,
95 @NotNull PsiFile file,
96 @NotNull Project project) {
97 myIntentionHintComponent = intentionHintComponent;
101 mySettings = IntentionManagerSettings.getInstance();
104 //true if something changed
105 boolean updateActions(@NotNull ShowIntentionsPass.IntentionsInfo intentions) {
106 boolean changed = wrapActionsTo(intentions.errorFixesToShow, myCachedErrorFixes);
107 changed |= wrapActionsTo(intentions.inspectionFixesToShow, myCachedInspectionFixes);
108 changed |= wrapActionsTo(intentions.intentionsToShow, myCachedIntentions);
109 changed |= wrapActionsTo(intentions.guttersToShow, myCachedGutters);
113 private boolean wrapActionsTo(@NotNull List<HighlightInfo.IntentionActionDescriptor> newDescriptors,
114 @NotNull Set<IntentionActionWithTextCaching> cachedActions) {
115 final int caretOffset = myEditor.getCaretModel().getOffset();
116 final int fileOffset = caretOffset > 0 && caretOffset == myFile.getTextLength() ? caretOffset - 1 : caretOffset;
118 final PsiElement hostElement;
119 if (myFile instanceof PsiCompiledElement) {
120 hostElement = element = myFile;
123 else if (PsiDocumentManager.getInstance(myProject).isUncommited(myEditor.getDocument())) {
125 FileViewProvider viewProvider = myFile.getViewProvider();
126 hostElement = element = viewProvider.findElementAt(fileOffset, viewProvider.getBaseLanguage());
129 hostElement = myFile.getViewProvider().findElementAt(fileOffset, myFile.getLanguage());
130 element = InjectedLanguageUtil.findElementAtNoCommit(myFile, fileOffset);
132 PsiFile injectedFile;
133 Editor injectedEditor;
134 if (element == null || element == hostElement) {
135 injectedFile = myFile;
136 injectedEditor = myEditor;
139 injectedFile = element.getContainingFile();
140 injectedEditor = InjectedLanguageUtil.getInjectedEditorForInjectedFile(myEditor, injectedFile);
143 boolean changed = false;
144 for (Iterator<IntentionActionWithTextCaching> iterator = cachedActions.iterator(); iterator.hasNext();) {
145 IntentionActionWithTextCaching cachedAction = iterator.next();
146 IntentionAction action = cachedAction.getAction();
147 if (!ShowIntentionActionsHandler.availableFor(myFile, myEditor, action)
148 && (hostElement == element || element != null && !ShowIntentionActionsHandler.availableFor(injectedFile, injectedEditor, action))) {
154 Set<IntentionActionWithTextCaching> wrappedNew = new THashSet<IntentionActionWithTextCaching>(newDescriptors.size(), ACTION_TEXT_AND_CLASS_EQUALS);
155 for (HighlightInfo.IntentionActionDescriptor descriptor : newDescriptors) {
156 final IntentionAction action = descriptor.getAction();
157 if (element != null && element != hostElement && ShowIntentionActionsHandler.availableFor(injectedFile, injectedEditor, action)) {
158 IntentionActionWithTextCaching cachedAction = wrapAction(descriptor, element, injectedFile, injectedEditor);
159 wrappedNew.add(cachedAction);
160 changed |= cachedActions.add(cachedAction);
162 else if (hostElement != null && ShowIntentionActionsHandler.availableFor(myFile, myEditor, action)) {
163 IntentionActionWithTextCaching cachedAction = wrapAction(descriptor, hostElement, myFile, myEditor);
164 wrappedNew.add(cachedAction);
165 changed |= cachedActions.add(cachedAction);
168 for (Iterator<IntentionActionWithTextCaching> iterator = cachedActions.iterator(); iterator.hasNext();) {
169 IntentionActionWithTextCaching cachedAction = iterator.next();
170 if (!wrappedNew.contains(cachedAction)) {
171 // action disappeared
180 IntentionActionWithTextCaching wrapAction(@NotNull HighlightInfo.IntentionActionDescriptor descriptor,
181 @NotNull PsiElement element,
182 @NotNull PsiFile containingFile,
183 @NotNull Editor containingEditor) {
184 IntentionActionWithTextCaching cachedAction = new IntentionActionWithTextCaching(descriptor);
185 final List<IntentionAction> options = descriptor.getOptions(element, containingEditor);
186 if (options == null) return cachedAction;
187 for (IntentionAction option : options) {
188 if (!option.isAvailable(myProject, containingEditor, containingFile)) {
189 // if option is not applicable in injected fragment, check in host file context
190 if (containingEditor == myEditor || !option.isAvailable(myProject, myEditor, myFile)) {
194 IntentionActionWithTextCaching textCaching = new IntentionActionWithTextCaching(option);
195 boolean isErrorFix = myCachedErrorFixes.contains(textCaching);
197 cachedAction.addErrorFix(option);
199 boolean isInspectionFix = myCachedInspectionFixes.contains(textCaching);
200 if (isInspectionFix) {
201 cachedAction.addInspectionFix(option);
204 cachedAction.addIntention(option);
211 public String getTitle() {
216 public boolean isSelectable(final IntentionActionWithTextCaching action) {
221 public PopupStep onChosen(final IntentionActionWithTextCaching action, final boolean finalChoice) {
222 if (finalChoice && !(action.getAction() instanceof EmptyIntentionAction)) {
227 if (hasSubstep(action)) {
228 return getSubStep(action, action.getToolName());
235 public Runnable getFinalRunnable() {
236 return myFinalRunnable;
239 private void applyAction(final IntentionActionWithTextCaching cachedAction) {
240 myFinalRunnable = new Runnable() {
243 HintManager.getInstance().hideAllHints();
244 ApplicationManager.getApplication().invokeLater(new Runnable() {
247 if (myProject.isDisposed()) return;
248 PsiDocumentManager.getInstance(myProject).commitAllDocuments();
249 final PsiFile file = PsiUtilBase.getPsiFileInEditor(myEditor, myProject);
254 ShowIntentionActionsHandler.chooseActionAndInvoke(file, myEditor, cachedAction.getAction(), cachedAction.getText());
261 IntentionListStep getSubStep(final IntentionActionWithTextCaching action, final String title) {
262 ShowIntentionsPass.IntentionsInfo intentions = new ShowIntentionsPass.IntentionsInfo();
263 for (final IntentionAction optionIntention : action.getOptionIntentions()) {
264 intentions.intentionsToShow.add(new HighlightInfo.IntentionActionDescriptor(optionIntention, getIcon(optionIntention)));
266 for (final IntentionAction optionFix : action.getOptionErrorFixes()) {
267 intentions.errorFixesToShow.add(new HighlightInfo.IntentionActionDescriptor(optionFix, getIcon(optionFix)));
269 for (final IntentionAction optionFix : action.getOptionInspectionFixes()) {
270 intentions.inspectionFixesToShow.add(new HighlightInfo.IntentionActionDescriptor(optionFix, getIcon(optionFix)));
273 return new IntentionListStep(myIntentionHintComponent, intentions,myEditor, myFile, myProject){
275 public String getTitle() {
281 private static Icon getIcon(IntentionAction optionIntention) {
282 return optionIntention instanceof Iconable ? ((Iconable)optionIntention).getIcon(0) : null;
286 public boolean hasSubstep(final IntentionActionWithTextCaching action) {
287 return action.getOptionIntentions().size() + action.getOptionErrorFixes().size() > 0;
292 public List<IntentionActionWithTextCaching> getValues() {
293 List<IntentionActionWithTextCaching> result = new ArrayList<IntentionActionWithTextCaching>(myCachedErrorFixes);
294 result.addAll(myCachedInspectionFixes);
295 result.addAll(myCachedIntentions);
296 result.addAll(myCachedGutters);
297 Collections.sort(result, new Comparator<IntentionActionWithTextCaching>() {
299 public int compare(final IntentionActionWithTextCaching o1, final IntentionActionWithTextCaching o2) {
300 int weight1 = getWeight(o1);
301 int weight2 = getWeight(o2);
302 if (weight1 != weight2) {
303 return weight2 - weight1;
305 return Comparing.compare(o1.getText(), o2.getText());
311 private int getWeight(IntentionActionWithTextCaching action) {
312 IntentionAction a = action.getAction();
313 int group = getGroup(action);
314 if (a instanceof IntentionActionWrapper) {
315 a = ((IntentionActionWrapper)a).getDelegate();
317 if (a instanceof IntentionWrapper) {
318 a = ((IntentionWrapper)a).getAction();
320 if (a instanceof HighPriorityAction) {
323 if (a instanceof LowPriorityAction) {
326 if (a instanceof SuppressIntentionActionFromFix) {
327 if (((SuppressIntentionActionFromFix)a).isShouldBeAppliedToInjectionHost() == ThreeState.NO) {
331 if (a instanceof QuickFixWrapper) {
332 final LocalQuickFix quickFix = ((QuickFixWrapper)a).getFix();
333 if (quickFix instanceof HighPriorityAction) {
336 if (quickFix instanceof LowPriorityAction) {
343 private int getGroup(IntentionActionWithTextCaching action) {
344 if (myCachedErrorFixes.contains(action)) {
347 if (myCachedInspectionFixes.contains(action)) {
350 if (action.getAction() instanceof EmptyIntentionAction) {
358 public String getTextFor(final IntentionActionWithTextCaching action) {
359 final String text = action.getAction().getText();
360 if (LOG.isDebugEnabled() && text.startsWith("<html>")) {
361 LOG.info("IntentionAction.getText() returned HTML: action=" + action + " text=" + text);
367 public Icon getIconFor(final IntentionActionWithTextCaching value) {
368 if (value.getIcon() != null) {
369 return value.getIcon();
372 final IntentionAction action = value.getAction();
374 Object iconable = action;
376 if (action instanceof QuickFixWrapper) {
377 iconable = ((QuickFixWrapper)action).getFix();
378 } else if (action instanceof IntentionActionWrapper) {
379 iconable = ((IntentionActionWrapper)action).getDelegate();
382 if (iconable instanceof Iconable) {
383 final Icon icon = ((Iconable)iconable).getIcon(0);
389 if (mySettings.isShowLightBulb(action)) {
390 return myCachedErrorFixes.contains(value) ? AllIcons.Actions.QuickfixBulb
391 : myCachedInspectionFixes.contains(value) ? AllIcons.Actions.IntentionBulb :
392 AllIcons.Actions.RealIntentionBulb;
395 return myCachedErrorFixes.contains(value) ? AllIcons.Actions.QuickfixOffBulb : AllIcons.Actions.RealIntentionOffBulb;
400 public void canceled() {
401 if (myIntentionHintComponent != null) {
402 myIntentionHintComponent.canceled(this);
407 public int getDefaultOptionIndex() { return 0; }
409 public ListSeparator getSeparatorAbove(final IntentionActionWithTextCaching value) {
410 List<IntentionActionWithTextCaching> values = getValues();
411 int index = values.indexOf(value);
412 if (index <= 0) return null;
413 IntentionActionWithTextCaching prev = values.get(index - 1);
415 if (getGroup(value) != getGroup(prev)) {
416 return new ListSeparator();
421 public boolean isMnemonicsNavigationEnabled() { return false; }
423 public MnemonicNavigationFilter<IntentionActionWithTextCaching> getMnemonicNavigationFilter() { return null; }
425 public boolean isSpeedSearchEnabled() { return true; }
427 public boolean isAutoSelectionEnabled() { return false; }
429 public SpeedSearchFilter<IntentionActionWithTextCaching> getSpeedSearchFilter() { return this; }
431 //speed search filter
433 public boolean canBeHidden(final IntentionActionWithTextCaching value) { return true;}
435 public String getIndexedString(final IntentionActionWithTextCaching value) { return getTextFor(value);}