com.intellij.codeInsight.documentation.DocumentationManager.java Source code

Java tutorial

Introduction

Here is the source code for com.intellij.codeInsight.documentation.DocumentationManager.java

Source

/*
 * Copyright 2000-2012 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.documentation;

import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.TargetElementUtil;
import com.intellij.codeInsight.hint.HintManagerImpl;
import com.intellij.codeInsight.hint.ParameterInfoController;
import com.intellij.codeInsight.lookup.Lookup;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupEx;
import com.intellij.codeInsight.lookup.LookupManager;
import com.intellij.ide.BrowserUtil;
import com.intellij.ide.DataManager;
import com.intellij.ide.actions.BaseNavigateToSourceAction;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.ide.util.gotoByName.ChooseByNameBase;
import com.intellij.lang.Language;
import com.intellij.lang.LanguageDocumentation;
import com.intellij.lang.documentation.*;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
import com.intellij.openapi.actionSystem.ex.AnActionListener;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.OrderEntry;
import com.intellij.openapi.roots.libraries.LibraryUtil;
import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.*;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.openapi.wm.ToolWindowId;
import com.intellij.openapi.wm.ex.WindowManagerEx;
import com.intellij.psi.*;
import com.intellij.psi.presentation.java.SymbolPresentationUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.ui.ListScrollingUtil;
import com.intellij.ui.content.Content;
import com.intellij.ui.popup.AbstractPopup;
import com.intellij.ui.popup.PopupPositionManager;
import com.intellij.ui.popup.PopupUpdateProcessor;
import com.intellij.util.Alarm;
import com.intellij.util.BooleanFunction;
import com.intellij.util.Consumer;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.List;

public class DocumentationManager extends DockablePopupManager<DocumentationComponent>
        implements DocumentationManagerProtocol {

    @NonNls
    public static final String JAVADOC_LOCATION_AND_SIZE = "javadoc.popup";
    public static final DataKey<String> SELECTED_QUICK_DOC_TEXT = DataKey.create("QUICK_DOC.SELECTED_TEXT");

    private static final Logger LOG = Logger.getInstance("#" + DocumentationManager.class.getName());
    private static final String SHOW_DOCUMENTATION_IN_TOOL_WINDOW = "ShowDocumentationInToolWindow";
    private static final String DOCUMENTATION_AUTO_UPDATE_ENABLED = "DocumentationAutoUpdateEnabled";

    private Editor myEditor = null;
    private final Alarm myUpdateDocAlarm;
    private WeakReference<JBPopup> myDocInfoHintRef;
    private Component myPreviouslyFocused = null;
    public static final Key<SmartPsiElementPointer> ORIGINAL_ELEMENT_KEY = Key.create("Original element");

    private final ActionManagerEx myActionManagerEx;

    private boolean myCloseOnSneeze;

    @Override
    protected String getToolwindowId() {
        return ToolWindowId.DOCUMENTATION;
    }

    @Override
    protected DocumentationComponent createComponent() {
        return new DocumentationComponent(this, createActions());
    }

    @Override
    protected String getRestorePopupDescription() {
        return "Restore popup view mode";
    }

    @Override
    protected String getAutoUpdateDescription() {
        return "Refresh documentation on selection change automatically";
    }

    @Override
    protected String getAutoUpdateTitle() {
        return "Auto-update from Source";
    }

    @NotNull
    @Override
    protected AnAction createRestorePopupAction() {
        AnAction restorePopupAction = super.createRestorePopupAction();
        ShortcutSet quickDocShortcut = ActionManager.getInstance().getAction(IdeActions.ACTION_QUICK_JAVADOC)
                .getShortcutSet();
        restorePopupAction.registerCustomShortcutSet(quickDocShortcut, null);
        return restorePopupAction;
    }

    @Override
    protected void restorePopupBehavior() {
        if (myPreviouslyFocused != null) {
            IdeFocusManager.getInstance(myProject).requestFocus(myPreviouslyFocused, true);
        }
        super.restorePopupBehavior();
        updateComponent();
    }

    /**
     * @return    <code>true</code> if quick doc control is configured to not prevent user-IDE interaction (e.g. should be closed if
     *            the user presses a key);
     *            <code>false</code> otherwise
     */
    public boolean isCloseOnSneeze() {
        return myCloseOnSneeze;
    }

    public static DocumentationManager getInstance(Project project) {
        return ServiceManager.getService(project, DocumentationManager.class);
    }

    public DocumentationManager(final Project project, ActionManagerEx managerEx) {
        super(project);
        myActionManagerEx = managerEx;
        final AnActionListener actionListener = new AnActionListener() {
            @Override
            public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
                final JBPopup hint = getDocInfoHint();
                if (hint != null) {
                    if (action instanceof HintManagerImpl.ActionToIgnore) {
                        ((AbstractPopup) hint).focusPreferredComponent();
                        return;
                    }
                    if (action instanceof ListScrollingUtil.ListScrollAction)
                        return;
                    if (action == myActionManagerEx.getAction(IdeActions.ACTION_EDITOR_MOVE_CARET_DOWN))
                        return;
                    if (action == myActionManagerEx.getAction(IdeActions.ACTION_EDITOR_MOVE_CARET_UP))
                        return;
                    if (action == myActionManagerEx.getAction(IdeActions.ACTION_EDITOR_MOVE_CARET_PAGE_DOWN))
                        return;
                    if (action == myActionManagerEx.getAction(IdeActions.ACTION_EDITOR_MOVE_CARET_PAGE_UP))
                        return;
                    if (action == ActionManagerEx.getInstanceEx().getAction(IdeActions.ACTION_EDITOR_ESCAPE))
                        return;
                    if (ActionPlaces.JAVADOC_INPLACE_SETTINGS.equals(event.getPlace()))
                        return;
                    if (action instanceof BaseNavigateToSourceAction)
                        return;
                    closeDocHint();
                }
            }

            @Override
            public void beforeEditorTyping(char c, DataContext dataContext) {
                final JBPopup hint = getDocInfoHint();
                if (hint != null && LookupManager.getActiveLookup(myEditor) == null) {
                    hint.cancel();
                }
            }

            @Override
            public void afterActionPerformed(final AnAction action, final DataContext dataContext,
                    AnActionEvent event) {
            }
        };
        myActionManagerEx.addAnActionListener(actionListener, project);
        myUpdateDocAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, myProject);
    }

    private void closeDocHint() {
        JBPopup hint = getDocInfoHint();
        if (hint == null) {
            return;
        }
        myCloseOnSneeze = false;
        hint.cancel();
        Component toFocus = myPreviouslyFocused;
        hint.cancel();
        if (toFocus != null) {
            IdeFocusManager.getInstance(myProject).requestFocus(toFocus, true);
        }
    }

    public void setAllowContentUpdateFromContext(boolean allow) {
        if (hasActiveDockedDocWindow()) {
            restartAutoUpdate(allow);
        }
    }

    public void updateToolwindowContext() {
        if (hasActiveDockedDocWindow()) {
            updateComponent();
        }
    }

    public void showJavaDocInfoAtToolWindow(@NotNull PsiElement element, @NotNull PsiElement original) {
        final Content content = recreateToolWindow(element, original);
        if (content == null)
            return;

        fetchDocInfo(getDefaultCollector(element, original), (DocumentationComponent) content.getComponent(), true);
    }

    public void showJavaDocInfo(@NotNull final PsiElement element, final PsiElement original) {
        showJavaDocInfo(element, original, null);
    }

    /**
     * Asks to show quick doc for the target element.
     *
     * @param editor         editor with an element for which quick do should be shown
     * @param element        target element which documentation should be shown
     * @param original       element that was used as a quick doc anchor. Example: consider a code like {@code Runnable task;}.
     *                       A user wants to see javadoc for the {@code Runnable}, so, original element is a class name from the variable
     *                       declaration but <code>'element'</code> argument is a {@code Runnable} descriptor
     * @param closeCallback  callback to be notified on target hint close (if any)
     * @param closeOnSneeze  flag that defines whether quick doc control should be as non-obtrusive as possible. E.g. there are at least
     *                       two possible situations - the quick doc is shown automatically on mouse over element; the quick doc is shown
     *                       on explicit action call (Ctrl+Q). We want to close the doc on, say, editor viewport position change
     *                       at the first situation but don't want to do that at the second
     */
    public void showJavaDocInfo(@NotNull Editor editor, @NotNull final PsiElement element,
            @NotNull final PsiElement original, @Nullable Runnable closeCallback, boolean closeOnSneeze) {
        myEditor = editor;
        myCloseOnSneeze = closeOnSneeze;
        showJavaDocInfo(element, original, closeCallback);
    }

    public void showJavaDocInfo(@NotNull final PsiElement element, final PsiElement original,
            @Nullable Runnable closeCallback) {
        PopupUpdateProcessor updateProcessor = new PopupUpdateProcessor(element.getProject()) {
            @Override
            public void updatePopup(Object lookupItemObject) {
                if (lookupItemObject instanceof PsiElement) {
                    doShowJavaDocInfo((PsiElement) lookupItemObject, false, this, original, null);
                }
            }
        };

        doShowJavaDocInfo(element, false, updateProcessor, original, closeCallback);
    }

    public void showJavaDocInfo(final Editor editor, @Nullable final PsiFile file, boolean requestFocus) {
        showJavaDocInfo(editor, file, requestFocus, null);
    }

    public void showJavaDocInfo(final Editor editor, @Nullable final PsiFile file, boolean requestFocus,
            @Nullable final Runnable closeCallback) {
        myEditor = editor;
        final Project project = getProject(file);
        PsiDocumentManager.getInstance(project).commitAllDocuments();

        final PsiElement list = ParameterInfoController.findArgumentList(file, editor.getCaretModel().getOffset(),
                -1);
        PsiElement expressionList = null;
        if (list != null) {
            LookupEx lookup = LookupManager.getInstance(myProject).getActiveLookup();
            if (lookup != null) {
                expressionList = null; // take completion variants for documentation then
            } else {
                expressionList = list;
            }
        }

        final PsiElement originalElement = getContextElement(editor, file);
        PsiElement element = assertSameProject(findTargetElement(editor, file));

        if (element == null && expressionList != null) {
            element = expressionList;
        }

        if (element == null && file == null)
            return; //file == null for text field editor

        if (element == null) { // look if we are within a javadoc comment
            element = assertSameProject(originalElement);
            if (element == null)
                return;

            PsiComment comment = PsiTreeUtil.getParentOfType(element, PsiComment.class);
            if (comment == null)
                return;

            element = comment instanceof PsiDocCommentBase ? ((PsiDocCommentBase) comment).getOwner()
                    : comment.getParent();
            if (element == null)
                return;
            //if (!(element instanceof PsiDocCommentOwner)) return null;
        }

        final PopupUpdateProcessor updateProcessor = new PopupUpdateProcessor(project) {
            @Override
            public void updatePopup(Object lookupIteObject) {
                if (lookupIteObject == null) {
                    return;
                }
                if (lookupIteObject instanceof PsiElement) {
                    doShowJavaDocInfo((PsiElement) lookupIteObject, false, this, originalElement, closeCallback);
                    return;
                }

                DocumentationProvider documentationProvider = getProviderFromElement(file);

                PsiElement element = documentationProvider.getDocumentationElementForLookupItem(
                        PsiManager.getInstance(myProject), lookupIteObject, originalElement);

                if (element == null)
                    return;

                if (myEditor != null) {
                    final PsiFile file = element.getContainingFile();
                    if (file != null) {
                        Editor editor = myEditor;
                        showJavaDocInfo(myEditor, file, false);
                        myEditor = editor;
                    }
                } else {
                    doShowJavaDocInfo(element, false, this, originalElement, closeCallback);
                }
            }
        };

        doShowJavaDocInfo(element, requestFocus, updateProcessor, originalElement, closeCallback);
    }

    public PsiElement findTargetElement(Editor editor, PsiFile file) {
        return findTargetElement(editor, file, getContextElement(editor, file));
    }

    private static PsiElement getContextElement(Editor editor, PsiFile file) {
        return file != null ? file.findElementAt(editor.getCaretModel().getOffset()) : null;
    }

    private void doShowJavaDocInfo(@NotNull final PsiElement element, boolean requestFocus,
            PopupUpdateProcessor updateProcessor, final PsiElement originalElement,
            @Nullable final Runnable closeCallback) {
        Project project = getProject(element);
        if (!project.isOpen())
            return;

        storeOriginalElement(project, originalElement, element);

        myPreviouslyFocused = WindowManagerEx.getInstanceEx().getFocusedComponent(project);

        JBPopup _oldHint = getDocInfoHint();

        if (myToolWindow == null
                && PropertiesComponent.getInstance().isTrueValue(SHOW_DOCUMENTATION_IN_TOOL_WINDOW)) {
            createToolWindow(element, originalElement);
        } else if (myToolWindow != null) {
            Content content = myToolWindow.getContentManager().getSelectedContent();
            if (content != null) {
                DocumentationComponent component = (DocumentationComponent) content.getComponent();
                if (element.getManager().areElementsEquivalent(component.getElement(), element)) {
                    JComponent preferredFocusableComponent = content.getPreferredFocusableComponent();
                    // focus toolwindow on the second actionPerformed
                    boolean focus = requestFocus || CommandProcessor.getInstance().getCurrentCommand() != null;
                    if (preferredFocusableComponent != null && focus) {
                        IdeFocusManager.getInstance(myProject).requestFocus(preferredFocusableComponent, true);
                    }
                } else {
                    content.setDisplayName(getTitle(element, true));
                    fetchDocInfo(getDefaultCollector(element, originalElement), component, true);
                }
            }

            if (!myToolWindow.isVisible()) {
                myToolWindow.show(null);
            }
        } else if (_oldHint != null && _oldHint.isVisible() && _oldHint instanceof AbstractPopup) {
            DocumentationComponent oldComponent = (DocumentationComponent) ((AbstractPopup) _oldHint)
                    .getComponent();
            fetchDocInfo(getDefaultCollector(element, originalElement), oldComponent);
        } else {
            showInPopup(element, requestFocus, updateProcessor, originalElement, closeCallback);
        }
    }

    private void showInPopup(@NotNull final PsiElement element, boolean requestFocus,
            PopupUpdateProcessor updateProcessor, final PsiElement originalElement,
            @Nullable final Runnable closeCallback) {
        final DocumentationComponent component = new DocumentationComponent(this);
        component.setNavigateCallback(new Consumer<PsiElement>() {
            @Override
            public void consume(PsiElement psiElement) {
                final AbstractPopup jbPopup = (AbstractPopup) getDocInfoHint();
                if (jbPopup != null) {
                    final String title = getTitle(psiElement, false);
                    jbPopup.setCaption(title);
                }
            }
        });
        Processor<JBPopup> pinCallback = new Processor<JBPopup>() {
            @Override
            public boolean process(JBPopup popup) {
                createToolWindow(element, originalElement);
                myToolWindow.setAutoHide(false);
                popup.cancel();
                return false;
            }
        };

        ActionListener actionListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                createToolWindow(element, originalElement);
                final JBPopup hint = getDocInfoHint();
                if (hint != null && hint.isVisible())
                    hint.cancel();
            }
        };
        List<Pair<ActionListener, KeyStroke>> actions = ContainerUtil.newSmartList();
        AnAction quickDocAction = ActionManagerEx.getInstanceEx().getAction(IdeActions.ACTION_QUICK_JAVADOC);
        for (Shortcut shortcut : quickDocAction.getShortcutSet().getShortcuts()) {
            if (!(shortcut instanceof KeyboardShortcut))
                continue;
            actions.add(Pair.create(actionListener, ((KeyboardShortcut) shortcut).getFirstKeyStroke()));
        }

        boolean hasLookup = LookupManager.getActiveLookup(myEditor) != null;
        final JBPopup hint = JBPopupFactory.getInstance().createComponentPopupBuilder(component, component)
                .setProject(element.getProject()).addListener(updateProcessor).addUserData(updateProcessor)
                .setKeyboardActions(actions).setDimensionServiceKey(myProject, JAVADOC_LOCATION_AND_SIZE, false)
                .setResizable(true).setMovable(true).setRequestFocus(requestFocus)
                .setCancelOnClickOutside(!hasLookup) // otherwise selecting lookup items by mouse would close the doc
                .setTitle(getTitle(element, false)).setCouldPin(pinCallback).setModalContext(false)
                .setCancelCallback(new Computable<Boolean>() {
                    @Override
                    public Boolean compute() {
                        myCloseOnSneeze = false;
                        if (closeCallback != null) {
                            closeCallback.run();
                        }
                        if (fromQuickSearch()) {
                            ((ChooseByNameBase.JPanelProvider) myPreviouslyFocused.getParent()).unregisterHint();
                        }

                        Disposer.dispose(component);
                        myEditor = null;
                        myPreviouslyFocused = null;
                        return Boolean.TRUE;
                    }
                }).setKeyEventHandler(new BooleanFunction<KeyEvent>() {
                    @Override
                    public boolean fun(KeyEvent e) {
                        if (myCloseOnSneeze) {
                            closeDocHint();
                        }
                        if ((AbstractPopup.isCloseRequest(e) && getDocInfoHint() != null)) {
                            closeDocHint();
                            return true;
                        }
                        return false;
                    }
                }).createPopup();

        component.setHint(hint);

        if (myEditor == null) {
            // subsequent invocation of javadoc popup from completion will have myEditor == null because of cancel invoked, 
            // so reevaluate the editor for proper popup placement
            Lookup lookup = LookupManager.getInstance(myProject).getActiveLookup();
            myEditor = lookup != null ? lookup.getEditor() : null;
        }
        fetchDocInfo(getDefaultCollector(element, originalElement), component);

        myDocInfoHintRef = new WeakReference<JBPopup>(hint);

        if (fromQuickSearch() && myPreviouslyFocused != null) {
            ((ChooseByNameBase.JPanelProvider) myPreviouslyFocused.getParent()).registerHint(hint);
        }
    }

    static String getTitle(@NotNull final PsiElement element, final boolean _short) {
        final String title = SymbolPresentationUtil.getSymbolPresentableText(element);
        return _short ? title != null ? title : element.getText()
                : CodeInsightBundle.message("javadoc.info.title", title != null ? title : element.getText());
    }

    public static void storeOriginalElement(final Project project, final PsiElement originalElement,
            final PsiElement element) {
        if (element == null)
            return;
        try {
            element.putUserData(ORIGINAL_ELEMENT_KEY,
                    SmartPointerManager.getInstance(project).createSmartPsiElementPointer(originalElement));
        } catch (RuntimeException ex) {
            // PsiPackage does not allow putUserData
        }
    }

    @Nullable
    public PsiElement findTargetElement(@NotNull final Editor editor, @Nullable final PsiFile file,
            PsiElement contextElement) {
        return findTargetElement(editor, editor.getCaretModel().getOffset(), file, contextElement);
    }

    @Nullable
    public PsiElement findTargetElement(final Editor editor, int offset, @Nullable final PsiFile file,
            PsiElement contextElement) {
        try {
            return findTargetElementUnsafe(editor, offset, file, contextElement);
        } catch (IndexNotReadyException inre) {
            LOG.warn("Index not ready");
            LOG.debug(inre);
            return null;
        }
    }

    /**
     * in case index is not ready will throw IndexNotReadyException
     */
    @Nullable
    private PsiElement findTargetElementUnsafe(final Editor editor, int offset, @Nullable final PsiFile file,
            PsiElement contextElement) {
        Set<String> allAccepted = TargetElementUtil.getAllAccepted();

        PsiElement element = assertSameProject(getElementFromLookup(editor, file));
        if (element == null && file != null) {
            final DocumentationProvider documentationProvider = getProviderFromElement(file);
            if (documentationProvider instanceof DocumentationProviderEx) {
                element = assertSameProject(((DocumentationProviderEx) documentationProvider)
                        .getCustomDocumentationElement(editor, file, contextElement));
            }
        }

        if (element == null) {
            element = assertSameProject(TargetElementUtil.findTargetElement(editor, allAccepted, offset));

            // Allow context doc over xml tag content
            if (element != null || contextElement != null) {
                final PsiElement adjusted = assertSameProject(
                        TargetElementUtil.adjustElement(editor, allAccepted, element, contextElement));
                if (adjusted != null) {
                    element = adjusted;
                }
            }
        }

        if (element == null) {
            final PsiReference ref = TargetElementUtil.findReference(editor, offset);
            if (ref != null) {
                element = assertSameProject(TargetElementUtil.adjustReference(ref));
                if (ref instanceof PsiPolyVariantReference) {
                    element = assertSameProject(ref.getElement());
                }
            }
        }

        storeOriginalElement(myProject, contextElement, element);

        return element;
    }

    @Nullable
    public PsiElement getElementFromLookup(final Editor editor, @Nullable final PsiFile file) {

        final Lookup activeLookup = LookupManager.getInstance(myProject).getActiveLookup();

        if (activeLookup != null) {
            LookupElement item = activeLookup.getCurrentItem();
            if (item != null) {

                int offset = editor.getCaretModel().getOffset();
                if (offset > 0 && offset == editor.getDocument().getTextLength())
                    offset--;
                PsiReference ref = TargetElementUtil.findReference(editor, offset);
                PsiElement contextElement = file == null ? null : file.findElementAt(offset);
                PsiElement targetElement = ref != null ? ref.getElement() : contextElement;
                if (targetElement != null) {
                    PsiUtilCore.ensureValid(targetElement);
                }

                DocumentationProvider documentationProvider = getProviderFromElement(file);

                PsiManager psiManager = PsiManager.getInstance(myProject);
                return documentationProvider.getDocumentationElementForLookupItem(psiManager, item.getObject(),
                        targetElement);
            }
        }
        return null;
    }

    private boolean fromQuickSearch() {
        return myPreviouslyFocused != null
                && myPreviouslyFocused.getParent() instanceof ChooseByNameBase.JPanelProvider;
    }

    private DocumentationCollector getDefaultCollector(@NotNull final PsiElement element,
            @Nullable final PsiElement originalElement) {
        return new DefaultDocumentationCollector(element, originalElement);
    }

    @Nullable
    public JBPopup getDocInfoHint() {
        if (myDocInfoHintRef == null)
            return null;
        JBPopup hint = myDocInfoHintRef.get();
        if (hint == null || !hint.isVisible()) {
            myDocInfoHintRef = null;
            return null;
        }
        return hint;
    }

    public void fetchDocInfo(final DocumentationCollector provider, final DocumentationComponent component) {
        doFetchDocInfo(component, provider, true, false);
    }

    public void fetchDocInfo(final DocumentationCollector provider, final DocumentationComponent component,
            final boolean clearHistory) {
        doFetchDocInfo(component, provider, true, clearHistory);
    }

    public void fetchDocInfo(final PsiElement element, final DocumentationComponent component) {
        doFetchDocInfo(component, getDefaultCollector(element, null), true, false);
    }

    public ActionCallback queueFetchDocInfo(final DocumentationCollector provider,
            final DocumentationComponent component, final boolean clearHistory) {
        return doFetchDocInfo(component, provider, false, clearHistory);
    }

    public ActionCallback queueFetchDocInfo(final PsiElement element, final DocumentationComponent component) {
        return queueFetchDocInfo(getDefaultCollector(element, null), component, false);
    }

    private ActionCallback doFetchDocInfo(final DocumentationComponent component,
            final DocumentationCollector provider, final boolean cancelRequests, final boolean clearHistory) {
        final ActionCallback callback = new ActionCallback();
        boolean wasEmpty = component.isEmpty();
        component.startWait();
        if (cancelRequests) {
            myUpdateDocAlarm.cancelAllRequests();
        }
        if (wasEmpty) {
            component.setText(CodeInsightBundle.message("javadoc.fetching.progress"), null, clearHistory);
            final AbstractPopup jbPopup = (AbstractPopup) getDocInfoHint();
            if (jbPopup != null) {
                jbPopup.setDimensionServiceKey(null);
            }
        }

        myUpdateDocAlarm.addRequest(new Runnable() {
            @Override
            public void run() {
                if (myProject.isDisposed())
                    return;
                final Throwable[] ex = new Throwable[1];
                String text = null;
                try {
                    text = provider.getDocumentation();
                } catch (Throwable e) {
                    LOG.info(e);
                    ex[0] = e;
                }

                if (ex[0] != null) {
                    //noinspection SSBasedInspection
                    SwingUtilities.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            String message = ex[0] instanceof IndexNotReadyException
                                    ? "Documentation is not available until indices are built."
                                    : CodeInsightBundle.message("javadoc.external.fetch.error.message",
                                            ex[0].getLocalizedMessage());
                            component.setText(message, null, true);
                            callback.setDone();
                        }
                    });
                    return;
                }

                final PsiElement element = ApplicationManager.getApplication()
                        .runReadAction(new Computable<PsiElement>() {
                            @Override
                            @Nullable
                            public PsiElement compute() {
                                return provider.getElement();
                            }
                        });
                if (element == null) {
                    return;
                }
                final String documentationText = text;
                //noinspection SSBasedInspection
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        PsiDocumentManager.getInstance(myProject).commitAllDocuments();

                        if (!element.isValid()) {
                            callback.setDone();
                            return;
                        }

                        if (documentationText == null) {
                            component.setText(CodeInsightBundle.message("no.documentation.found"), element, true);
                        } else if (documentationText.length() == 0) {
                            component.setText(component.getText(), element, true, clearHistory);
                        } else {
                            component.setData(element, documentationText, clearHistory,
                                    provider.getEffectiveExternalUrl());
                        }

                        final AbstractPopup jbPopup = (AbstractPopup) getDocInfoHint();
                        if (jbPopup == null) {
                            callback.setDone();
                            return;
                        } else {
                            jbPopup.setDimensionServiceKey(JAVADOC_LOCATION_AND_SIZE);
                        }
                        jbPopup.setCaption(getTitle(element, false));
                        callback.setDone();
                    }
                });
            }
        }, 10);
        return callback;
    }

    @NotNull
    public static DocumentationProvider getProviderFromElement(final PsiElement element) {
        return getProviderFromElement(element, null);
    }

    @NotNull
    public static DocumentationProvider getProviderFromElement(@Nullable PsiElement element,
            @Nullable PsiElement originalElement) {
        if (element != null && !element.isValid()) {
            element = null;
        }
        if (originalElement != null && !originalElement.isValid()) {
            originalElement = null;
        }

        if (originalElement == null) {
            originalElement = getOriginalElement(element);
        }

        PsiFile containingFile = originalElement != null ? originalElement.getContainingFile()
                : element != null ? element.getContainingFile() : null;
        Set<DocumentationProvider> result = new LinkedHashSet<DocumentationProvider>();

        final Language containingFileLanguage = containingFile != null ? containingFile.getLanguage() : null;
        DocumentationProvider originalProvider = containingFile != null
                ? LanguageDocumentation.INSTANCE.forLanguage(containingFileLanguage)
                : null;

        final Language elementLanguage = element != null ? element.getLanguage() : null;
        DocumentationProvider elementProvider = element == null || elementLanguage.is(containingFileLanguage) ? null
                : LanguageDocumentation.INSTANCE.forLanguage(elementLanguage);

        result.add(elementProvider);
        result.add(originalProvider);

        if (containingFile != null) {
            final Language baseLanguage = containingFile.getViewProvider().getBaseLanguage();
            if (!baseLanguage.is(containingFileLanguage)) {
                result.add(LanguageDocumentation.INSTANCE.forLanguage(baseLanguage));
            }
        } else if (element instanceof PsiDirectory) {
            final Set<Language> langs = new HashSet<Language>();

            for (PsiFile file : ((PsiDirectory) element).getFiles()) {
                final Language baseLanguage = file.getViewProvider().getBaseLanguage();
                if (!langs.contains(baseLanguage)) {
                    langs.add(baseLanguage);
                    result.add(LanguageDocumentation.INSTANCE.forLanguage(baseLanguage));
                }
            }
        }
        return CompositeDocumentationProvider.wrapProviders(result);
    }

    @Nullable
    public static PsiElement getOriginalElement(final PsiElement element) {
        SmartPsiElementPointer originalElementPointer = element != null ? element.getUserData(ORIGINAL_ELEMENT_KEY)
                : null;
        return originalElementPointer != null ? originalElementPointer.getElement() : null;
    }

    void navigateByLink(final DocumentationComponent component, final String url) {
        component.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        final PsiElement psiElement = component.getElement();
        if (psiElement == null) {
            return;
        }
        final PsiManager manager = PsiManager.getInstance(getProject(psiElement));
        if (url.startsWith("open")) {
            final PsiFile containingFile = psiElement.getContainingFile();
            OrderEntry libraryEntry = null;
            if (containingFile != null) {
                final VirtualFile virtualFile = containingFile.getVirtualFile();
                libraryEntry = LibraryUtil.findLibraryEntry(virtualFile, myProject);
            } else if (psiElement instanceof PsiDirectoryContainer) {
                PsiDirectory[] directories = ((PsiDirectoryContainer) psiElement).getDirectories();
                for (PsiDirectory directory : directories) {
                    final VirtualFile virtualFile = directory.getVirtualFile();
                    libraryEntry = LibraryUtil.findLibraryEntry(virtualFile, myProject);
                    if (libraryEntry != null) {
                        break;
                    }
                }
            }
            if (libraryEntry != null) {
                ProjectSettingsService.getInstance(myProject).openLibraryOrSdkSettings(libraryEntry);
            }
        } else if (url.startsWith(PSI_ELEMENT_PROTOCOL)) {
            final String refText = url.substring(PSI_ELEMENT_PROTOCOL.length());
            DocumentationProvider provider = getProviderFromElement(psiElement);
            PsiElement targetElement = provider.getDocumentationElementForLink(manager, refText, psiElement);
            if (targetElement == null) {
                for (DocumentationProvider documentationProvider : Extensions
                        .getExtensions(DocumentationProvider.EP_NAME)) {
                    targetElement = documentationProvider.getDocumentationElementForLink(manager, refText,
                            psiElement);
                    if (targetElement != null) {
                        break;
                    }
                }
            }
            if (targetElement == null) {
                for (Language language : Language.getRegisteredLanguages()) {
                    DocumentationProvider documentationProvider = LanguageDocumentation.INSTANCE
                            .forLanguage(language);
                    if (documentationProvider != null) {
                        targetElement = documentationProvider.getDocumentationElementForLink(manager, refText,
                                psiElement);
                        if (targetElement != null) {
                            break;
                        }
                    }
                }
            }
            if (targetElement != null) {
                fetchDocInfo(getDefaultCollector(targetElement, null), component);
            }
        } else {
            final DocumentationProvider provider = getProviderFromElement(psiElement);
            boolean processed = false;
            if (provider instanceof CompositeDocumentationProvider) {
                for (DocumentationProvider p : ((CompositeDocumentationProvider) provider).getAllProviders()) {
                    if (!(p instanceof ExternalDocumentationHandler))
                        continue;

                    final ExternalDocumentationHandler externalHandler = (ExternalDocumentationHandler) p;
                    if (externalHandler.canFetchDocumentationLink(url)) {
                        fetchDocInfo(new DocumentationCollector() {
                            @Override
                            public String getDocumentation() throws Exception {
                                return externalHandler.fetchExternalDocumentation(url, psiElement);
                            }

                            @Override
                            public PsiElement getElement() {
                                return psiElement;
                            }

                            @Nullable
                            @Override
                            public String getEffectiveExternalUrl() {
                                return url;
                            }
                        }, component);
                        processed = true;
                    } else if (externalHandler.handleExternalLink(manager, url, psiElement)) {
                        processed = true;
                        break;
                    }
                }
            }

            if (!processed) {

                fetchDocInfo(new DocumentationCollector() {
                    @Override
                    public String getDocumentation() throws Exception {
                        if (url.startsWith(DOC_ELEMENT_PROTOCOL)) {
                            final List<String> urls = ApplicationManager.getApplication()
                                    .runReadAction(new NullableComputable<List<String>>() {
                                        @Override
                                        public List<String> compute() {
                                            final DocumentationProvider provider = getProviderFromElement(
                                                    psiElement);
                                            return provider.getUrlFor(psiElement, getOriginalElement(psiElement));
                                        }
                                    });
                            String url1 = urls != null && !urls.isEmpty() ? urls.get(0) : url;
                            BrowserUtil.browse(url1);
                        } else {
                            BrowserUtil.browse(url);
                        }
                        return "";
                    }

                    @Override
                    public PsiElement getElement() {
                        //String loc = getElementLocator(docUrl);
                        //
                        //if (loc != null) {
                        //  PsiElement context = component.getElement();
                        //  return JavaDocUtil.findReferenceTarget(context.getManager(), loc, context);
                        //}

                        return psiElement;
                    }

                    @Nullable
                    @Override
                    public String getEffectiveExternalUrl() {
                        return url;
                    }
                }, component);
            }
        }

        component.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
    }

    void showHint(final JBPopup hint) {
        final Component focusOwner = IdeFocusManager.getInstance(myProject).getFocusOwner();
        DataContext dataContext = DataManager.getInstance().getDataContext(focusOwner);
        PopupPositionManager.positionPopupInBestPosition(hint, myEditor, dataContext);
    }

    public void requestFocus() {
        if (fromQuickSearch()) {
            myPreviouslyFocused.getParent().requestFocus();
        }
    }

    public Project getProject(@Nullable final PsiElement element) {
        assertSameProject(element);
        return myProject;
    }

    private PsiElement assertSameProject(@Nullable PsiElement element) {
        if (element != null && element.isValid() && myProject != element.getProject()) {
            throw new AssertionError(myProject + "!=" + element.getProject() + "; element=" + element);
        }
        return element;
    }

    public static void createHyperlink(StringBuilder buffer, String refText, String label, boolean plainLink) {
        DocumentationManagerUtil.createHyperlink(buffer, refText, label, plainLink);
    }

    @Override
    public String getShowInToolWindowProperty() {
        return SHOW_DOCUMENTATION_IN_TOOL_WINDOW;
    }

    @Override
    public String getAutoUpdateEnabledProperty() {
        return DOCUMENTATION_AUTO_UPDATE_ENABLED;
    }

    @Override
    protected void doUpdateComponent(PsiElement element, PsiElement originalElement,
            DocumentationComponent component) {
        fetchDocInfo(getDefaultCollector(element, originalElement), component);
    }

    @Override
    protected void doUpdateComponent(Editor editor, PsiFile psiFile) {
        showJavaDocInfo(editor, psiFile, false, null);
    }

    @Override
    protected void doUpdateComponent(@NotNull PsiElement element) {
        showJavaDocInfo(element, element, null);
    }

    @Override
    protected String getTitle(PsiElement element) {
        return getTitle(element, true);
    }

    @Nullable
    public Image getElementImage(@NotNull PsiElement element, @NotNull String imageSpec) {
        DocumentationProvider provider = getProviderFromElement(element);
        if (provider instanceof CompositeDocumentationProvider) {
            for (DocumentationProvider p : ((CompositeDocumentationProvider) provider).getAllProviders()) {
                if (p instanceof DocumentationProviderEx) {
                    Image image = ((DocumentationProviderEx) p).getLocalImageForElement(element, imageSpec);
                    if (image != null)
                        return image;
                }
            }
        }
        return null;
    }

    private interface DocumentationCollector {
        @Nullable
        String getDocumentation() throws Exception;

        @Nullable
        PsiElement getElement();

        @Nullable
        String getEffectiveExternalUrl();
    }

    private class DefaultDocumentationCollector implements DocumentationCollector {

        private final PsiElement myElement;
        private final PsiElement myOriginalElement;

        private String myEffectiveUrl;

        public DefaultDocumentationCollector(PsiElement element, PsiElement originalElement) {
            myElement = element;
            myOriginalElement = originalElement;
        }

        @Override
        @Nullable
        public String getDocumentation() throws Exception {
            final DocumentationProvider provider = ApplicationManager.getApplication()
                    .runReadAction(new Computable<DocumentationProvider>() {
                        @Override
                        public DocumentationProvider compute() {
                            return getProviderFromElement(myElement, myOriginalElement);
                        }
                    });

            if (provider instanceof ExternalDocumentationProvider) {
                final List<String> urls = ApplicationManager.getApplication()
                        .runReadAction(new NullableComputable<List<String>>() {
                            @Override
                            public List<String> compute() {
                                final SmartPsiElementPointer originalElementPtr = myElement
                                        .getUserData(ORIGINAL_ELEMENT_KEY);
                                final PsiElement originalElement = originalElementPtr != null
                                        ? originalElementPtr.getElement()
                                        : null;
                                if (((ExternalDocumentationProvider) provider).hasDocumentationFor(myElement,
                                        originalElement)) {
                                    return provider.getUrlFor(myElement, originalElement);
                                }
                                return null;
                            }
                        });
                if (urls != null) {
                    for (String url : urls) {
                        final String doc = ((ExternalDocumentationProvider) provider)
                                .fetchExternalDocumentation(myProject, myElement, Collections.singletonList(url));
                        if (doc != null) {
                            myEffectiveUrl = url;
                            return doc;
                        }
                    }
                }
            }
            return ApplicationManager.getApplication().runReadAction(new Computable<String>() {
                @Override
                @Nullable
                public String compute() {
                    final SmartPsiElementPointer originalElement = myElement.getUserData(ORIGINAL_ELEMENT_KEY);
                    return provider.generateDoc(myElement,
                            originalElement != null ? originalElement.getElement() : null);
                }
            });
        }

        @Override
        @Nullable
        public PsiElement getElement() {
            return myElement.isValid() ? myElement : null;
        }

        @Nullable
        @Override
        public String getEffectiveExternalUrl() {
            return myEffectiveUrl;
        }
    }
}