/*
* Copyright 2013-2016 Sergey Ignatov, Alexander Zolotov, Florin Patan
*
* 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.goide.codeInsight.imports;
import com.goide.GoIcons;
import com.goide.completion.GoCompletionUtil;
import com.goide.project.GoVendoringUtil;
import com.goide.psi.GoFile;
import com.goide.psi.GoReferenceExpression;
import com.goide.psi.GoTypeReferenceExpression;
import com.goide.psi.impl.GoPsiImplUtil;
import com.goide.psi.impl.GoReference;
import com.goide.psi.impl.GoTypeReference;
import com.goide.runconfig.testing.GoTestFinder;
import com.goide.stubs.index.GoPackagesIndex;
import com.goide.util.GoUtil;
import com.intellij.codeInsight.FileModificationService;
import com.intellij.codeInsight.daemon.impl.DaemonListeners;
import com.intellij.codeInsight.daemon.impl.ShowAutoImportPass;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.intention.HighPriorityAction;
import com.intellij.codeInspection.HintAction;
import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.impl.LaterInvocator;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.PopupChooserBuilder;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiReference;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.stubs.StubIndex;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBList;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.util.*;
import static com.intellij.util.containers.ContainerUtil.*;
public class GoImportPackageQuickFix extends LocalQuickFixAndIntentionActionOnPsiElement implements HintAction, HighPriorityAction {
@NotNull private final String myPackageName;
@Nullable private List<String> myPackagesToImport;
public GoImportPackageQuickFix(@NotNull PsiElement element, @NotNull String importPath) {
super(element);
myPackageName = "";
myPackagesToImport = Collections.singletonList(importPath);
}
public GoImportPackageQuickFix(@NotNull PsiReference reference) {
super(reference.getElement());
myPackageName = reference.getCanonicalText();
}
@Nullable
public PsiReference getReference(PsiElement element) {
if (element != null && element.isValid()) {
for (PsiReference reference : element.getReferences()) {
if (isSupportedReference(reference)) {
return reference;
}
}
}
return null;
}
private static boolean isSupportedReference(@Nullable PsiReference reference) {
return reference instanceof GoReference || reference instanceof GoTypeReference;
}
@Override
public boolean showHint(@NotNull Editor editor) {
return doAutoImportOrShowHint(editor, true);
}
@NotNull
@Override
public String getText() {
PsiElement element = getStartElement();
if (element != null) {
return "Import " + getText(getImportPathVariantsToImport(element));
}
return "Import package";
}
@NotNull
private static String getText(@NotNull Collection<String> packagesToImport) {
return getFirstItem(packagesToImport, "") + "? " + (packagesToImport.size() > 1 ? "(multiple choices...) " : "");
}
@NotNull
@Override
public String getFamilyName() {
return "Import package";
}
@Override
public void invoke(@NotNull Project project, @NotNull PsiFile file, @Nullable("is null when called from inspection") Editor editor,
@NotNull PsiElement startElement, @NotNull PsiElement endElement) {
if (!FileModificationService.getInstance().prepareFileForWrite(file)) return;
perform(getImportPathVariantsToImport(startElement), file, editor);
}
@Override
public boolean isAvailable(@NotNull Project project,
@NotNull PsiFile file,
@NotNull PsiElement startElement,
@NotNull PsiElement endElement) {
PsiReference reference = getReference(startElement);
return file instanceof GoFile && file.getManager().isInProject(file)
&& reference != null && reference.resolve() == null
&& !getImportPathVariantsToImport(startElement).isEmpty() && notQualified(startElement);
}
private static boolean notQualified(@Nullable PsiElement startElement) {
return startElement instanceof GoReferenceExpression && ((GoReferenceExpression)startElement).getQualifier() == null ||
startElement instanceof GoTypeReferenceExpression && ((GoTypeReferenceExpression)startElement).getQualifier() == null;
}
@NotNull
private List<String> getImportPathVariantsToImport(@NotNull PsiElement element) {
if (myPackagesToImport == null) {
myPackagesToImport = getImportPathVariantsToImport(myPackageName, element);
}
return myPackagesToImport;
}
@NotNull
public static List<String> getImportPathVariantsToImport(@NotNull String packageName, @NotNull PsiElement context) {
PsiFile contextFile = context.getContainingFile();
Set<String> imported = contextFile instanceof GoFile
? ((GoFile)contextFile).getImportedPackagesMap().keySet() : Collections.emptySet();
Project project = context.getProject();
PsiDirectory parentDirectory = contextFile != null ? contextFile.getParent() : null;
String testTargetPackage = GoTestFinder.getTestTargetPackage(contextFile);
Module module = contextFile != null ? ModuleUtilCore.findModuleForPsiElement(contextFile) : null;
boolean vendoringEnabled = GoVendoringUtil.isVendoringEnabled(module);
GlobalSearchScope scope = GoUtil.goPathResolveScope(context);
Collection<GoFile> packages = StubIndex.getElements(GoPackagesIndex.KEY, packageName, project, scope, GoFile.class);
return sorted(skipNulls(map2Set(
packages,
file -> {
if (parentDirectory != null && parentDirectory.isEquivalentTo(file.getParent())) {
if (testTargetPackage == null || !testTargetPackage.equals(file.getPackageName())) {
return null;
}
}
if (!GoPsiImplUtil.canBeAutoImported(file, false, module)) {
return null;
}
String importPath = file.getImportPath(vendoringEnabled);
return !imported.contains(importPath) ? importPath : null;
}
)), new MyImportsComparator(context, vendoringEnabled));
}
public boolean doAutoImportOrShowHint(@NotNull Editor editor, boolean showHint) {
PsiElement element = getStartElement();
if (element == null || !element.isValid()) return false;
PsiReference reference = getReference(element);
if (reference == null || reference.resolve() != null) return false;
List<String> packagesToImport = getImportPathVariantsToImport(element);
if (packagesToImport.isEmpty()) {
return false;
}
PsiFile file = element.getContainingFile();
String firstPackageToImport = getFirstItem(packagesToImport);
// autoimport on trying to fix
if (packagesToImport.size() == 1) {
if (GoCodeInsightSettings.getInstance().isAddUnambiguousImportsOnTheFly() && !LaterInvocator.isInModalContext() &&
(ApplicationManager.getApplication().isUnitTestMode() || DaemonListeners.canChangeFileSilently(file))) {
CommandProcessor.getInstance().runUndoTransparentAction(() -> perform(file, firstPackageToImport));
return true;
}
}
// show hint on failed autoimport
if (showHint) {
if (ApplicationManager.getApplication().isUnitTestMode()) return false;
if (HintManager.getInstance().hasShownHintsThatWillHideByOtherHint(true)) return false;
if (!GoCodeInsightSettings.getInstance().isShowImportPopup()) return false;
TextRange referenceRange = reference.getRangeInElement().shiftRight(element.getTextRange().getStartOffset());
HintManager.getInstance().showQuestionHint(
editor,
ShowAutoImportPass.getMessage(packagesToImport.size() > 1, getFirstItem(packagesToImport)),
referenceRange.getStartOffset(),
referenceRange.getEndOffset(),
() -> {
if (file.isValid() && !editor.isDisposed()) {
perform(packagesToImport, file, editor);
}
return true;
}
);
return true;
}
return false;
}
private void perform(@NotNull List<String> packagesToImport, @NotNull PsiFile file, @Nullable Editor editor) {
LOG.assertTrue(editor != null || packagesToImport.size() == 1, "Cannot invoke fix with ambiguous imports on null editor");
if (packagesToImport.size() > 1 && editor != null) {
JBList list = new JBList(packagesToImport);
list.installCellRenderer(o -> {
JBLabel label = new JBLabel(o.toString(), GoIcons.PACKAGE, SwingConstants.LEFT);
label.setBorder(IdeBorderFactory.createEmptyBorder(2, 4, 2, 4));
return label;
});
PopupChooserBuilder builder = JBPopupFactory.getInstance().createListPopupBuilder(list).setRequestFocus(true)
.setTitle("Package to import")
.setItemChoosenCallback(
() -> {
int i = list.getSelectedIndex();
if (i < 0) return;
perform(file, packagesToImport.get(i));
})
.setFilteringEnabled(o -> o instanceof String ? (String)o : o.toString());
JBPopup popup = builder.createPopup();
builder.getScrollPane().setBorder(null);
builder.getScrollPane().setViewportBorder(null);
popup.showInBestPositionFor(editor);
}
else if (packagesToImport.size() == 1) {
perform(file, getFirstItem(packagesToImport));
}
else {
String packages = StringUtil.join(packagesToImport, ",");
throw new IncorrectOperationException("Cannot invoke fix with ambiguous imports on editor ()" + editor + ". Packages: " + packages);
}
}
private void perform(@NotNull PsiFile file, @Nullable String pathToImport) {
if (file instanceof GoFile && pathToImport != null) {
Project project = file.getProject();
CommandProcessor.getInstance().executeCommand(project, () -> ApplicationManager.getApplication().runWriteAction(() -> {
if (!isAvailable()) return;
if (((GoFile)file).getImportedPackagesMap().containsKey(pathToImport)) return;
((GoFile)file).addImport(pathToImport, null);
}), "Add import", null);
}
}
private static class MyImportsComparator implements Comparator<String> {
@Nullable
private final String myContextImportPath;
public MyImportsComparator(@Nullable PsiElement context, boolean vendoringEnabled) {
myContextImportPath = GoCompletionUtil.getContextImportPath(context, vendoringEnabled);
}
@Override
public int compare(@NotNull String s1, @NotNull String s2) {
int result = Comparing.compare(GoCompletionUtil.calculatePackagePriority(s2, myContextImportPath),
GoCompletionUtil.calculatePackagePriority(s1, myContextImportPath));
return result != 0 ? result : Comparing.compare(s1, s2);
}
}
}