/*
* Copyright 2000-2014 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.navigation.actions;
import com.intellij.codeInsight.CodeInsightActionHandler;
import com.intellij.codeInsight.CodeInsightBundle;
import consulo.codeInsight.TargetElementUtil;
import consulo.codeInsight.TargetElementUtilEx;
import com.intellij.codeInsight.actions.BaseCodeInsightAction;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.navigation.NavigationUtil;
import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.find.actions.ShowUsagesAction;
import com.intellij.ide.util.DefaultPsiElementCellRenderer;
import com.intellij.ide.util.EditSourceUtil;
import com.intellij.ide.util.PsiElementListCellRenderer;
import com.intellij.injected.editor.EditorWindow;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ex.EditorGutterComponentEx;
import com.intellij.openapi.extensions.ExtensionException;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.pom.Navigatable;
import com.intellij.psi.*;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.util.ObjectUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import consulo.annotations.RequiredDispatchThread;
import consulo.codeInsight.navigation.actions.GotoDeclarationHandlerEx;
import javax.swing.*;
import java.awt.*;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
public class GotoDeclarationAction extends BaseCodeInsightAction implements CodeInsightActionHandler, DumbAware {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.navigation.actions.GotoDeclarationAction");
@NotNull
@Override
protected CodeInsightActionHandler getHandler() {
return this;
}
@Override
protected boolean isValidForLookup() {
return true;
}
@RequiredDispatchThread
@Override
public void invoke(@NotNull final Project project, @NotNull Editor editor, @NotNull PsiFile file) {
PsiDocumentManager.getInstance(project).commitAllDocuments();
DumbService.getInstance(project).setAlternativeResolveEnabled(true);
try {
int offset = editor.getCaretModel().getOffset();
Pair<PsiElement[], GotoDeclarationHandler> elementsInfo = findAllTargetElementsInfo(project, editor, offset);
PsiElement[] elements = elementsInfo.getFirst();
FeatureUsageTracker.getInstance().triggerFeatureUsed("navigation.goto.declaration");
if (elements.length != 1) {
if (elements.length == 0) {
PsiElement element = findElementToShowUsagesOf(editor, file, editor.getCaretModel().getOffset());
if (element != null) {
ShowUsagesAction showUsages = (ShowUsagesAction)ActionManager.getInstance().getAction(ShowUsagesAction.ID);
RelativePoint popupPosition = JBPopupFactory.getInstance().guessBestPopupLocation(editor);
showUsages.startFindUsages(element, popupPosition, editor, ShowUsagesAction.USAGES_PAGE_SIZE);
return;
}
}
chooseAmbiguousTarget(editor, offset, elements, calcElementRender(elementsInfo.getSecond(), elements));
return;
}
PsiElement element = elements[0];
PsiElement navElement = element.getNavigationElement();
navElement = TargetElementUtil.getGotoDeclarationTarget(element, navElement);
if (navElement != null) {
gotoTargetElement(navElement);
}
}
catch (IndexNotReadyException e) {
DumbService.getInstance(project).showDumbModeNotification("Navigation is not available here during index update");
}
finally {
DumbService.getInstance(project).setAlternativeResolveEnabled(false);
}
}
@Nullable
private static PsiElementListCellRenderer<PsiElement> calcElementRender(@Nullable GotoDeclarationHandler declarationHandler, @NotNull PsiElement[] elements) {
if(declarationHandler instanceof GotoDeclarationHandlerEx) {
return ((GotoDeclarationHandlerEx)declarationHandler).createRender(elements);
}
return null;
}
public static PsiNameIdentifierOwner findElementToShowUsagesOf(@NotNull Editor editor, @NotNull PsiFile file, int offset) {
PsiElement elementAt = TargetElementUtil.findTargetElement(editor, ContainerUtil.newHashSet(TargetElementUtilEx.ELEMENT_NAME_ACCEPTED), offset);
if (elementAt instanceof PsiNameIdentifierOwner) {
return (PsiNameIdentifierOwner)elementAt;
}
return null;
}
private static void chooseAmbiguousTarget(final Editor editor, int offset, PsiElement[] elements, @Nullable PsiElementListCellRenderer<PsiElement> render) {
PsiElementProcessor<PsiElement> navigateProcessor = new PsiElementProcessor<PsiElement>() {
@Override
public boolean execute(@NotNull final PsiElement element) {
gotoTargetElement(element);
return true;
}
};
boolean found = chooseAmbiguousTarget(editor, offset, navigateProcessor, CodeInsightBundle.message("declaration.navigation.title"), elements, render);
if (!found) {
HintManager.getInstance().showErrorHint(editor, "Cannot find declaration to go to");
}
}
private static void gotoTargetElement(PsiElement element) {
Navigatable navigatable = element instanceof Navigatable ? (Navigatable)element : EditSourceUtil.getDescriptor(element);
if (navigatable != null && navigatable.canNavigate()) {
navigatable.navigate(true);
}
}
public static boolean chooseAmbiguousTarget(@NotNull Editor editor,
int offset,
@NotNull PsiElementProcessor<PsiElement> processor,
@NotNull String titlePattern,
@Nullable PsiElement[] elements) {
return chooseAmbiguousTarget(editor, offset, processor, titlePattern, elements, null);
}
// returns true if processor is run or is going to be run after showing popup
public static boolean chooseAmbiguousTarget(@NotNull Editor editor,
int offset,
@NotNull PsiElementProcessor<PsiElement> processor,
@NotNull String titlePattern,
@Nullable PsiElement[] elements,
@Nullable PsiElementListCellRenderer<PsiElement> renderer) {
if (TargetElementUtil.inVirtualSpace(editor, offset)) {
return false;
}
final PsiReference reference = TargetElementUtil.findReference(editor, offset);
if (elements == null || elements.length == 0) {
final Collection<PsiElement> candidates = suggestCandidates(reference);
elements = PsiUtilCore.toPsiElementArray(candidates);
}
if (elements.length == 1) {
PsiElement element = elements[0];
LOG.assertTrue(element != null);
processor.execute(element);
return true;
}
if (elements.length > 1) {
String title;
if (reference == null) {
title = titlePattern;
}
else {
final TextRange range = reference.getRangeInElement();
final String elementText = reference.getElement().getText();
LOG.assertTrue(range.getStartOffset() >= 0 && range.getEndOffset() <= elementText.length(), Arrays.toString(elements) + ";" + reference);
final String refText = range.substring(elementText);
title = MessageFormat.format(titlePattern, refText);
}
if(renderer == null) {
renderer = new DefaultPsiElementCellRenderer();
}
NavigationUtil.getPsiElementPopup(elements, renderer, title, processor).showInBestPositionFor(editor);
return true;
}
return false;
}
private static Collection<PsiElement> suggestCandidates(final PsiReference reference) {
if (reference == null) {
return Collections.emptyList();
}
return TargetElementUtil.getTargetCandidates(reference);
}
@Override
public boolean startInWriteAction() {
return false;
}
@Nullable
public static PsiElement findTargetElement(Project project, Editor editor, int offset) {
final Pair<PsiElement[], GotoDeclarationHandler> pair = findAllTargetElementsInfo(project, editor, offset);
PsiElement[] targets = pair.getFirst();
return targets.length == 1 ? targets[0] : null;
}
@NotNull
public static Pair<PsiElement[], GotoDeclarationHandler> findAllTargetElementsInfo(Project project, Editor editor, int offset) {
if (TargetElementUtil.inVirtualSpace(editor, offset)) {
return Pair.create(PsiElement.EMPTY_ARRAY, null);
}
Pair<PsiElement[], GotoDeclarationHandler> pair = findTargetElementsNoVSWithHandler(project, editor, offset, true);
return Pair.create(ObjectUtil.notNull(pair.getFirst(), PsiElement.EMPTY_ARRAY), pair.getSecond());
}
@Nullable
public static PsiElement[] findTargetElementsNoVS(Project project, Editor editor, int offset, boolean lookupAccepted) {
return findTargetElementsNoVSWithHandler(project, editor, offset, lookupAccepted).getFirst();
}
@NotNull
public static Pair<PsiElement[], GotoDeclarationHandler> findTargetElementsNoVSWithHandler(Project project,
Editor editor,
int offset,
boolean lookupAccepted) {
final Document document = editor.getDocument();
PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(document);
if (file == null) return Pair.empty();
if (file instanceof PsiCompiledElement) {
PsiElement mirror = ((PsiCompiledElement)file).getMirror();
if (mirror instanceof PsiFile) file = (PsiFile)mirror;
}
PsiElement elementAt = file.findElementAt(TargetElementUtil.adjustOffset(file, document, offset));
for (GotoDeclarationHandler handler : Extensions.getExtensions(GotoDeclarationHandler.EP_NAME)) {
try {
PsiElement[] result = handler.getGotoDeclarationTargets(elementAt, offset, editor);
if (result != null && result.length > 0) {
for (PsiElement element : result) {
if (element == null) {
LOG.error("Null target element is returned by " + handler.getClass().getName());
return Pair.empty();
}
}
return Pair.create(result, handler);
}
}
catch (AbstractMethodError e) {
LOG.error(new ExtensionException(handler.getClass()));
}
}
Set<String> flags = ContainerUtil.newHashSet(TargetElementUtil.getAllAccepted());
flags.remove(TargetElementUtilEx.ELEMENT_NAME_ACCEPTED);
if (!lookupAccepted) {
flags.remove(TargetElementUtilEx.LOOKUP_ITEM_ACCEPTED);
}
PsiElement element = TargetElementUtil.findTargetElement(editor, flags, offset);
if (element != null) {
return Pair.create(new PsiElement[] {element}, null);
}
// if no references found in injected fragment, try outer document
if (editor instanceof EditorWindow) {
EditorWindow window = (EditorWindow)editor;
return findTargetElementsNoVSWithHandler(project, window.getDelegate(), window.getDocument().injectedToHost(offset), lookupAccepted);
}
return Pair.empty();
}
@RequiredDispatchThread
@Override
public void update(final AnActionEvent event) {
InputEvent inputEvent = event.getInputEvent();
if (inputEvent instanceof MouseEvent) {
Component component = inputEvent.getComponent();
if (component != null) {
Point point = ((MouseEvent)inputEvent).getPoint();
Component componentAt = SwingUtilities.getDeepestComponentAt(component, point.x, point.y);
if (componentAt instanceof EditorGutterComponentEx) {
event.getPresentation().setEnabled(false);
return;
}
}
}
for (GotoDeclarationHandler handler : Extensions.getExtensions(GotoDeclarationHandler.EP_NAME)) {
try {
String text = handler.getActionText(event.getDataContext());
if (text != null) {
Presentation presentation = event.getPresentation();
presentation.setText(text);
break;
}
}
catch (AbstractMethodError e) {
LOG.error(handler.toString(), e);
}
}
super.update(event);
}
}