package com.intellij.lang.javascript.flex;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.hint.QuestionAction;
import com.intellij.codeInsight.navigation.NavigationUtil;
import com.intellij.codeInspection.HintAction;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.injected.editor.VirtualFileWindow;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.lang.javascript.JSBundle;
import com.intellij.lang.javascript.psi.JSFunction;
import com.intellij.lang.javascript.psi.JSNewExpression;
import com.intellij.lang.javascript.psi.JSReferenceExpression;
import com.intellij.lang.javascript.psi.ecmal4.JSAttributeList;
import com.intellij.lang.javascript.psi.ecmal4.JSAttributeListOwner;
import com.intellij.lang.javascript.psi.ecmal4.JSClass;
import com.intellij.lang.javascript.psi.ecmal4.JSQualifiedNamedElement;
import com.intellij.lang.javascript.psi.impl.JSPsiImplUtils;
import com.intellij.lang.javascript.psi.resolve.JSResolveUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.GlobalSearchScopes;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.util.SmartList;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
/**
* @author Maxim.Mossienko
* Date: Apr 25, 2008
* Time: 8:36:38 PM
*/
public class AddImportECMAScriptClassOrFunctionAction implements HintAction, QuestionAction, LocalQuickFix {
private final PsiPolyVariantReference myReference;
private Editor myEditor;
private boolean isAvailable;
private boolean isAvailableCalculated;
private long modificationCount = -1;
private String myText = "";
private final boolean myUnambiguousTheFlyMode;
public AddImportECMAScriptClassOrFunctionAction(final Editor editor, final PsiPolyVariantReference psiReference) {
this(editor, psiReference, false);
}
public AddImportECMAScriptClassOrFunctionAction(final Editor editor,
final PsiPolyVariantReference psiReference,
final boolean unambiguousTheFlyMode) {
myReference = psiReference;
myEditor = editor;
myUnambiguousTheFlyMode = unambiguousTheFlyMode;
}
public boolean showHint(@NotNull final Editor editor) {
myEditor = editor;
final PsiElement element = myReference.getElement();
TextRange range = InjectedLanguageManager.getInstance(element.getProject()).injectedToHost(element, element.getTextRange());
HintManager.getInstance().showQuestionHint(editor, getText(), range.getStartOffset(), range.getEndOffset(), this);
return true;
}
@NotNull
public String getText() {
return myText;
}
@NotNull
public String getFamilyName() {
return getText();
}
public void applyFix(@NotNull final Project project, @NotNull final ProblemDescriptor descriptor) {
invoke(project, myEditor, descriptor.getPsiElement().getContainingFile());
}
public boolean isAvailable(@NotNull final Project project, final Editor editor, final PsiFile file) {
if (!myReference.getElement().isValid()) return false;
final long modL = myReference.getElement().getManager().getModificationTracker().getModificationCount();
if (!isAvailableCalculated || modL != modificationCount) {
final ResolveResult[] results = myReference.multiResolve(false);
boolean hasValidResult = false;
for(ResolveResult r:results) {
if (r.isValidResult()) {
hasValidResult = true;
break;
}
}
if (!hasValidResult) {
final Collection<JSQualifiedNamedElement> candidates = getCandidates(editor, file);
isAvailableCalculated = true;
isAvailable = candidates.size() > 0;
String text;
if (isAvailable) {
final JSQualifiedNamedElement element = candidates.iterator().next();
text = element.getQualifiedName() + "?";
if (candidates.size() > 1) text += " (multiple choices...)";
if (!ApplicationManager.getApplication().isHeadlessEnvironment()) {
text += " Alt+Enter";
}
} else {
text = "";
}
myText = text;
} else {
isAvailableCalculated = true;
isAvailable = false;
myText = "";
}
modificationCount = modL;
}
return isAvailable;
}
private Collection<JSQualifiedNamedElement> getCandidates(Editor editor, PsiFile file) {
final Collection<JSQualifiedNamedElement> candidates;
if (myReference instanceof JSReferenceExpression && ((JSReferenceExpression)myReference).getQualifier() == null) {
Collection<JSQualifiedNamedElement> c = getCandidates(editor, file, myReference.getCanonicalText());
filterCandidates(c);
candidates = new THashSet<>(c, JSPsiImplUtils.QUALIFIED_NAME_HASHING_STRATEGY);
}
else {
JSQualifiedNamedElement invalidResult = null;
for (ResolveResult r : myReference.multiResolve(false)) {
PsiElement element = r.getElement();
if (element instanceof JSQualifiedNamedElement) {
invalidResult = (JSQualifiedNamedElement)element;
}
}
if (invalidResult != null) {
if(myReference.getElement().getParent() instanceof JSNewExpression && invalidResult instanceof JSFunction &&
((JSFunction)invalidResult).isConstructor()) {
invalidResult = (JSClass)invalidResult.getParent();
}
candidates = new SmartList<>();
candidates.add(invalidResult);
}
else {
candidates = Collections.emptyList();
}
}
return candidates;
}
public static Collection<JSQualifiedNamedElement> getCandidates(final Editor editor, final PsiFile file, final String name) {
final Module module = ModuleUtil.findModuleForPsiElement(file);
if (module != null) {
GlobalSearchScope searchScope;
VirtualFile virtualFile = file.getVirtualFile();
if (virtualFile instanceof VirtualFileWindow) virtualFile = ((VirtualFileWindow)virtualFile).getDelegate();
if (GlobalSearchScopes.projectProductionScope(file.getProject()).contains(virtualFile)) { // skip tests suggestions
searchScope = GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, false);
} else {
searchScope = JSResolveUtil.getResolveScope(file);
}
return JSResolveUtil.findElementsByName(name, editor.getProject(), searchScope);
}
else {
return Collections.emptyList();
}
}
public void invoke(@NotNull final Project project, final Editor editor, final PsiFile file) {
final Collection<JSQualifiedNamedElement> candidates = getCandidates(editor, file);
if (candidates.isEmpty() || myUnambiguousTheFlyMode && candidates.size() != 1) {
return;
}
if (candidates.size() > 1) {
NavigationUtil.getPsiElementPopup(
candidates.toArray(new JSQualifiedNamedElement[candidates.size()]),
new JSQualifiedNamedElementRenderer(),
JSBundle.message("choose.class.to.import.title"),
new PsiElementProcessor<JSQualifiedNamedElement>() {
public boolean execute(@NotNull final JSQualifiedNamedElement element) {
CommandProcessor.getInstance().executeCommand(
project,
() -> doImport(element.getQualifiedName()),
"Import " + element.getQualifiedName(),
this
);
return false;
}
}
).showInBestPositionFor(editor);
}
else {
final JSQualifiedNamedElement element = candidates.iterator().next();
if (myUnambiguousTheFlyMode) {
CommandProcessor.getInstance().runUndoTransparentAction(() -> doImport(element.getQualifiedName()));
}
else {
CommandProcessor.getInstance().executeCommand(
project,
() -> doImport(element.getQualifiedName()),
"Import " + element.getQualifiedName(),
this
);
}
}
}
private void doImport(final String qName) {
ApplicationManager.getApplication().runWriteAction(() -> {
final PsiElement element = myReference.getElement();
SmartPsiElementPointer<PsiElement> pointer =
SmartPointerManager.getInstance(element.getProject()).createSmartPsiElementPointer(element);
ImportUtils.doImport(element, qName, true);
PsiElement newElement = pointer.getElement();
if (newElement != null) {
ImportUtils.insertUseNamespaceIfNeeded(qName, newElement);
}
});
}
public boolean startInWriteAction() {
return false;
}
public boolean execute() {
final PsiFile containingFile = myReference.getElement().getContainingFile();
invoke(containingFile.getProject(), myEditor, containingFile);
return true;
}
private static void filterCandidates(Collection<JSQualifiedNamedElement>candidates) {
for (Iterator<JSQualifiedNamedElement> i = candidates.iterator(); i.hasNext();) {
JSQualifiedNamedElement element = i.next();
if (!element.getQualifiedName().contains(".")) {
i.remove();
}
else if (element instanceof JSAttributeListOwner &&
((JSAttributeListOwner)element).getAttributeList().getAccessType() != JSAttributeList.AccessType.PUBLIC) {
i.remove();
}
}
}
}