/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInsight.intention.impl;
import com.intellij.codeInsight.daemon.impl.HighlightInfo;
import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.intention.*;
import com.intellij.codeInsight.intention.impl.config.IntentionManagerSettings;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.SuppressIntentionActionFromFix;
import com.intellij.codeInspection.ex.QuickFixWrapper;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.*;
import com.intellij.openapi.util.Iconable;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
import com.intellij.psi.util.PsiUtilBase;
import com.intellij.util.ThreeState;
import com.intellij.util.containers.ContainerUtil;
import gnu.trove.THashSet;
import gnu.trove.TObjectHashingStrategy;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import javax.swing.*;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author cdr
*/
public class IntentionListStep implements ListPopupStep<IntentionActionWithTextCaching>, SpeedSearchFilter<IntentionActionWithTextCaching> {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.intention.impl.IntentionListStep");
private final Set<IntentionActionWithTextCaching> myCachedIntentions = ContainerUtil.newConcurrentSet(ACTION_TEXT_AND_CLASS_EQUALS);
private final Set<IntentionActionWithTextCaching> myCachedErrorFixes = ContainerUtil.newConcurrentSet(ACTION_TEXT_AND_CLASS_EQUALS);
private final Set<IntentionActionWithTextCaching> myCachedInspectionFixes = ContainerUtil.newConcurrentSet(ACTION_TEXT_AND_CLASS_EQUALS);
private final Set<IntentionActionWithTextCaching> myCachedGutters = ContainerUtil.newConcurrentSet(ACTION_TEXT_AND_CLASS_EQUALS);
private final Set<IntentionActionWithTextCaching> myCachedNotifications = ContainerUtil.newConcurrentSet(ACTION_TEXT_AND_CLASS_EQUALS);
private final IntentionManagerSettings mySettings;
@Nullable
private final IntentionHintComponent myIntentionHintComponent;
@Nullable
private final Editor myEditor;
private final PsiFile myFile;
private final Project myProject;
private static final TObjectHashingStrategy<IntentionActionWithTextCaching> ACTION_TEXT_AND_CLASS_EQUALS =
new TObjectHashingStrategy<IntentionActionWithTextCaching>() {
@Override
public int computeHashCode(final IntentionActionWithTextCaching object) {
return object.getText().hashCode();
}
@Override
public boolean equals(final IntentionActionWithTextCaching o1, final IntentionActionWithTextCaching o2) {
return getActionClass(o1) == getActionClass(o2) && o1.getText().equals(o2.getText());
}
private Class<? extends IntentionAction> getActionClass(IntentionActionWithTextCaching o1) {
IntentionAction action = o1.getAction();
if (action instanceof IntentionActionDelegate) {
return ((IntentionActionDelegate)action).getDelegate().getClass();
}
return action.getClass();
}
};
private Runnable myFinalRunnable;
public IntentionListStep(@Nullable IntentionHintComponent intentionHintComponent,
@NotNull ShowIntentionsPass.IntentionsInfo intentions,
@Nullable Editor editor,
@NotNull PsiFile file,
@NotNull Project project) {
this(intentionHintComponent, editor, file, project);
wrapAndUpdateActions(intentions, false); // when create bulb do not update actions again since it would impede the EDT
}
IntentionListStep(@Nullable IntentionHintComponent intentionHintComponent, @Nullable Editor editor, @NotNull PsiFile file, @NotNull Project project) {
myIntentionHintComponent = intentionHintComponent;
myEditor = editor;
myFile = file;
myProject = project;
mySettings = IntentionManagerSettings.getInstance();
}
//true if something changed
boolean wrapAndUpdateActions(@NotNull ShowIntentionsPass.IntentionsInfo newInfo, boolean callUpdate) {
boolean changed = wrapActionsTo(newInfo.errorFixesToShow, myCachedErrorFixes, callUpdate);
changed |= wrapActionsTo(newInfo.inspectionFixesToShow, myCachedInspectionFixes, callUpdate);
changed |= wrapActionsTo(newInfo.intentionsToShow, myCachedIntentions, callUpdate);
changed |= wrapActionsTo(newInfo.guttersToShow, myCachedGutters, callUpdate);
changed |= wrapActionsTo(newInfo.notificationActionsToShow, myCachedNotifications, callUpdate);
return changed;
}
private boolean wrapActionsTo(@NotNull List<HighlightInfo.IntentionActionDescriptor> newDescriptors,
@NotNull Set<IntentionActionWithTextCaching> cachedActions,
boolean callUpdate) {
boolean changed = false;
if (myEditor == null) {
LOG.assertTrue(!callUpdate);
for (HighlightInfo.IntentionActionDescriptor descriptor : newDescriptors) {
changed |= cachedActions.add(wrapAction(descriptor, myFile, myFile, null));
}
}
else {
final int caretOffset = myEditor.getCaretModel().getOffset();
final int fileOffset = caretOffset > 0 && caretOffset == myFile.getTextLength() ? caretOffset - 1 : caretOffset;
PsiElement element;
final PsiElement hostElement;
if (myFile instanceof PsiCompiledElement) {
hostElement = element = myFile;
}
else if (PsiDocumentManager.getInstance(myProject).isUncommited(myEditor.getDocument())) {
//???
FileViewProvider viewProvider = myFile.getViewProvider();
hostElement = element = viewProvider.findElementAt(fileOffset, viewProvider.getBaseLanguage());
}
else {
hostElement = myFile.getViewProvider().findElementAt(fileOffset, myFile.getLanguage());
element = InjectedLanguageUtil.findElementAtNoCommit(myFile, fileOffset);
}
PsiFile injectedFile;
Editor injectedEditor;
if (element == null || element == hostElement) {
injectedFile = myFile;
injectedEditor = myEditor;
}
else {
injectedFile = element.getContainingFile();
injectedEditor = InjectedLanguageUtil.getInjectedEditorForInjectedFile(myEditor, injectedFile);
}
for (Iterator<IntentionActionWithTextCaching> iterator = cachedActions.iterator(); iterator.hasNext(); ) {
IntentionActionWithTextCaching cachedAction = iterator.next();
IntentionAction action = cachedAction.getAction();
if (!ShowIntentionActionsHandler.availableFor(myFile, myEditor, action) &&
(hostElement == element || element != null && !ShowIntentionActionsHandler.availableFor(injectedFile, injectedEditor, action))) {
iterator.remove();
changed = true;
}
}
Set<IntentionActionWithTextCaching> wrappedNew = new THashSet<>(newDescriptors.size(), ACTION_TEXT_AND_CLASS_EQUALS);
for (HighlightInfo.IntentionActionDescriptor descriptor : newDescriptors) {
final IntentionAction action = descriptor.getAction();
if (element != null && element != hostElement && (!callUpdate || ShowIntentionActionsHandler.availableFor(injectedFile, injectedEditor, action))) {
IntentionActionWithTextCaching cachedAction = wrapAction(descriptor, element, injectedFile, injectedEditor);
wrappedNew.add(cachedAction);
changed |= cachedActions.add(cachedAction);
}
else if (hostElement != null && (!callUpdate || ShowIntentionActionsHandler.availableFor(myFile, myEditor, action))) {
IntentionActionWithTextCaching cachedAction = wrapAction(descriptor, hostElement, myFile, myEditor);
wrappedNew.add(cachedAction);
changed |= cachedActions.add(cachedAction);
}
}
for (Iterator<IntentionActionWithTextCaching> iterator = cachedActions.iterator(); iterator.hasNext(); ) {
IntentionActionWithTextCaching cachedAction = iterator.next();
if (!wrappedNew.contains(cachedAction)) {
// action disappeared
iterator.remove();
changed = true;
}
}
}
return changed;
}
@NotNull
IntentionActionWithTextCaching wrapAction(@NotNull HighlightInfo.IntentionActionDescriptor descriptor,
@Nullable PsiElement element,
@Nullable PsiFile containingFile,
@Nullable Editor containingEditor) {
IntentionActionWithTextCaching cachedAction = new IntentionActionWithTextCaching(descriptor, (cached, action) -> {
removeActionFromCached(cached);
markInvoked(action);
});
if (element == null) return cachedAction;
final List<IntentionAction> options = descriptor.getOptions(element, containingEditor);
if (options == null) return cachedAction;
for (IntentionAction option : options) {
if (containingFile != null && containingEditor != null && myEditor != null) {
if (!ShowIntentionActionsHandler.availableFor(containingFile, containingEditor, option)) {
//if option is not applicable in injected fragment, check in host file context
if (containingEditor == myEditor || !ShowIntentionActionsHandler.availableFor(myFile, myEditor, option)) {
continue;
}
}
}
else if (!option.isAvailable(myProject, containingEditor, containingFile)) {
// if option is not applicable in injected fragment, check in host file context
if (containingEditor == myEditor || !option.isAvailable(myProject, myEditor, myFile)) {
continue;
}
}
IntentionActionWithTextCaching textCaching = new IntentionActionWithTextCaching(option);
boolean isErrorFix = myCachedErrorFixes.contains(textCaching);
if (isErrorFix) {
cachedAction.addErrorFix(option);
}
boolean isInspectionFix = myCachedInspectionFixes.contains(textCaching);
if (isInspectionFix) {
cachedAction.addInspectionFix(option);
}
else {
cachedAction.addIntention(option);
}
}
return cachedAction;
}
@Override
public String getTitle() {
return null;
}
@Override
public boolean isSelectable(final IntentionActionWithTextCaching action) {
return true;
}
@Override
public PopupStep onChosen(final IntentionActionWithTextCaching action, final boolean finalChoice) {
if (finalChoice && !(action.getAction() instanceof AbstractEmptyIntentionAction)) {
applyAction(action);
return FINAL_CHOICE;
}
if (hasSubstep(action)) {
return getSubStep(action, action.getToolName());
}
return FINAL_CHOICE;
}
@Override
public Runnable getFinalRunnable() {
return myFinalRunnable;
}
private void applyAction(@NotNull IntentionActionWithTextCaching cachedAction) {
myFinalRunnable = () -> {
HintManager.getInstance().hideAllHints();
if (myProject.isDisposed() || myEditor != null && myEditor.isDisposed()) return;
if (DumbService.isDumb(myProject) && !DumbService.isDumbAware(cachedAction)) {
DumbService.getInstance(myProject).showDumbModeNotification(cachedAction.getText() + " is not available during indexing");
return;
}
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
PsiFile file = myEditor != null ? PsiUtilBase.getPsiFileInEditor(myEditor, myProject) : myFile;
if (file == null) {
return;
}
ShowIntentionActionsHandler.chooseActionAndInvoke(file, myEditor, cachedAction.getAction(), cachedAction.getText(), myProject);
};
}
private void markInvoked(@NotNull IntentionAction action) {
ShowIntentionsPass.markActionInvoked(myFile.getProject(), myEditor, action);
}
private void removeActionFromCached(@NotNull IntentionActionWithTextCaching action) {
// remove from the action from the list after invocation to make it appear unavailable sooner.
// (the highlighting will process the whole file and remove the no more available action from the list automatically - but it's may be too long)
myCachedErrorFixes.remove(action);
myCachedGutters.remove(action);
myCachedInspectionFixes.remove(action);
myCachedIntentions.remove(action);
myCachedNotifications.remove(action);
}
@NotNull
IntentionListStep getSubStep(@NotNull IntentionActionWithTextCaching action, final String title) {
ShowIntentionsPass.IntentionsInfo intentions = new ShowIntentionsPass.IntentionsInfo();
for (final IntentionAction optionIntention : action.getOptionIntentions()) {
intentions.intentionsToShow.add(new HighlightInfo.IntentionActionDescriptor(optionIntention, getIcon(optionIntention)));
}
for (final IntentionAction optionFix : action.getOptionErrorFixes()) {
intentions.errorFixesToShow.add(new HighlightInfo.IntentionActionDescriptor(optionFix, getIcon(optionFix)));
}
for (final IntentionAction optionFix : action.getOptionInspectionFixes()) {
intentions.inspectionFixesToShow.add(new HighlightInfo.IntentionActionDescriptor(optionFix, getIcon(optionFix)));
}
return new IntentionListStep(myIntentionHintComponent, intentions, myEditor, myFile, myProject) {
@Override
public String getTitle() {
return title;
}
};
}
private static Icon getIcon(IntentionAction optionIntention) {
return optionIntention instanceof Iconable ? ((Iconable)optionIntention).getIcon(0) : null;
}
@TestOnly
public Map<IntentionAction, List<IntentionAction>> getActionsWithSubActions() {
Map<IntentionAction, List<IntentionAction>> result = ContainerUtil.newLinkedHashMap();
for (IntentionActionWithTextCaching cached : getValues()) {
IntentionAction action = cached.getAction();
if (ShowIntentionActionsHandler.chooseFileForAction(myFile, myEditor, action) == null) continue;
List<IntentionActionWithTextCaching> subActions = getSubStep(cached, cached.getToolName()).getValues();
List<IntentionAction> options = subActions.stream().map(IntentionActionWithTextCaching::getAction)
.filter(option -> ShowIntentionActionsHandler.chooseFileForAction(myFile, myEditor, option) != null).collect(Collectors.toList());
result.put(action, options);
}
return result;
}
@Override
public boolean hasSubstep(final IntentionActionWithTextCaching action) {
return action.getOptionIntentions().size() + action.getOptionErrorFixes().size() > 0;
}
@Override
@NotNull
public List<IntentionActionWithTextCaching> getValues() {
List<IntentionActionWithTextCaching> result = new ArrayList<>(myCachedErrorFixes);
result.addAll(myCachedInspectionFixes);
result.addAll(myCachedIntentions);
result.addAll(myCachedGutters);
result.addAll(myCachedNotifications);
result = DumbService.getInstance(myProject).filterByDumbAwareness(result);
Collections.sort(result, (o1, o2) -> {
int weight1 = getWeight(o1);
int weight2 = getWeight(o2);
if (weight1 != weight2) {
return weight2 - weight1;
}
return o1.compareTo(o2);
});
return result;
}
private int getWeight(IntentionActionWithTextCaching action) {
IntentionAction a = action.getAction();
int group = getGroup(action);
if (a instanceof IntentionActionDelegate) {
a = ((IntentionActionDelegate)a).getDelegate();
}
if (a instanceof HighPriorityAction) {
return group + 3;
}
if (a instanceof LowPriorityAction) {
return group - 3;
}
if (a instanceof SuppressIntentionActionFromFix) {
if (((SuppressIntentionActionFromFix)a).isShouldBeAppliedToInjectionHost() == ThreeState.NO) {
return group - 1;
}
}
if (a instanceof QuickFixWrapper) {
final LocalQuickFix quickFix = ((QuickFixWrapper)a).getFix();
if (quickFix instanceof HighPriorityAction) {
return group + 3;
}
if (quickFix instanceof LowPriorityAction) {
return group - 3;
}
}
return group;
}
private int getGroup(IntentionActionWithTextCaching action) {
if (myCachedErrorFixes.contains(action)) {
return 20;
}
if (myCachedInspectionFixes.contains(action)) {
return 10;
}
if (myCachedNotifications.contains(action)) {
return 7;
}
if (myCachedGutters.contains(action)) {
return 5;
}
if (action.getAction() instanceof EmptyIntentionAction) {
return -10;
}
return 0;
}
@Override
@NotNull
public String getTextFor(final IntentionActionWithTextCaching action) {
final String text = action.getAction().getText();
if (LOG.isDebugEnabled() && text.startsWith("<html>")) {
LOG.info("IntentionAction.getText() returned HTML: action=" + action + " text=" + text);
}
return text;
}
@Override
public Icon getIconFor(final IntentionActionWithTextCaching value) {
if (value.getIcon() != null) {
return value.getIcon();
}
final IntentionAction action = value.getAction();
Object iconable = action;
//custom icon
if (action instanceof QuickFixWrapper) {
iconable = ((QuickFixWrapper)action).getFix();
}
else if (action instanceof IntentionActionDelegate) {
iconable = ((IntentionActionDelegate)action).getDelegate();
}
if (iconable instanceof Iconable) {
final Icon icon = ((Iconable)iconable).getIcon(0);
if (icon != null) {
return icon;
}
}
if (mySettings.isShowLightBulb(action)) {
return myCachedErrorFixes.contains(value)
? AllIcons.Actions.QuickfixBulb
: myCachedInspectionFixes.contains(value) ? AllIcons.Actions.IntentionBulb : AllIcons.Actions.RealIntentionBulb;
}
else {
return myCachedErrorFixes.contains(value) ? AllIcons.Actions.QuickfixOffBulb : AllIcons.Actions.RealIntentionOffBulb;
}
}
@Override
public void canceled() {
if (myIntentionHintComponent != null) {
myIntentionHintComponent.canceled(this);
}
}
@Override
public int getDefaultOptionIndex() {
return 0;
}
@Override
public ListSeparator getSeparatorAbove(final IntentionActionWithTextCaching value) {
List<IntentionActionWithTextCaching> values = getValues();
int index = values.indexOf(value);
if (index <= 0) return null;
IntentionActionWithTextCaching prev = values.get(index - 1);
if (getGroup(value) != getGroup(prev)) {
return new ListSeparator();
}
return null;
}
@Override
public boolean isMnemonicsNavigationEnabled() {
return false;
}
@Override
public MnemonicNavigationFilter<IntentionActionWithTextCaching> getMnemonicNavigationFilter() {
return null;
}
@Override
public boolean isSpeedSearchEnabled() {
return true;
}
@Override
public boolean isAutoSelectionEnabled() {
return false;
}
@Override
public SpeedSearchFilter<IntentionActionWithTextCaching> getSpeedSearchFilter() {
return this;
}
//speed search filter
@Override
public boolean canBeHidden(final IntentionActionWithTextCaching value) {
return true;
}
@Override
public String getIndexedString(final IntentionActionWithTextCaching value) {
return getTextFor(value);
}
}