| /* |
| * 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.TargetElementUtilBase; |
| 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 ParameterInfoController myParameterInfoController; |
| 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 static final int ourFlagsForTargetElements = TargetElementUtilBase.getInstance().getAllAccepted(); |
| |
| 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 documentation popup behavior"; |
| } |
| |
| @Override |
| protected String getAutoUpdateDescription() { |
| return "Show documentation for current element automatically"; |
| } |
| |
| @Override |
| protected String getAutoUpdateTitle() { |
| return "Auto Show Documentation for Selected Element"; |
| } |
| |
| @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); |
| if (list != null) { |
| LookupEx lookup = LookupManager.getInstance(myProject).getActiveLookup(); |
| if (lookup != null) { |
| myParameterInfoController = null; // take completion variants for documentation then |
| } else { |
| myParameterInfoController = ParameterInfoController.findControllerAtOffset(editor, list.getTextRange().getStartOffset()); |
| } |
| } |
| |
| final PsiElement originalElement = getContextElement(editor, file); |
| PsiElement element = assertSameProject(findTargetElement(editor, file)); |
| |
| if (element == null && myParameterInfoController != null) { |
| final Object[] objects = myParameterInfoController.getSelectedElements(); |
| |
| if (objects != null && objects.length > 0) { |
| if (objects[0] instanceof PsiElement) { |
| element = assertSameProject((PsiElement)objects[0]); |
| } |
| } |
| } |
| |
| 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; |
| myParameterInfoController = 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); |
| } |
| } |
| |
| private 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) { |
| TargetElementUtilBase util = TargetElementUtilBase.getInstance(); |
| 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(util.findTargetElement(editor, ourFlagsForTargetElements, offset)); |
| |
| // Allow context doc over xml tag content |
| if (element != null || contextElement != null) { |
| final PsiElement adjusted = assertSameProject(util.adjustElement(editor, ourFlagsForTargetElements, element, contextElement)); |
| if (adjusted != null) { |
| element = adjusted; |
| } |
| } |
| } |
| |
| if (element == null) { |
| final PsiReference ref = TargetElementUtilBase.findReference(editor, offset); |
| if (ref != null) { |
| element = assertSameProject(util.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 = TargetElementUtilBase.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 (myParameterInfoController != null) { |
| final String doc = ApplicationManager.getApplication().runReadAction( |
| new NullableComputable<String>() { |
| @Override |
| public String compute() { |
| return generateParameterInfoDocumentation(provider); |
| } |
| } |
| ); |
| if (doc != null) return doc; |
| } |
| 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); |
| } |
| } |
| ); |
| } |
| |
| @Nullable |
| private String generateParameterInfoDocumentation(DocumentationProvider provider) { |
| final Object[] objects = myParameterInfoController.getSelectedElements(); |
| |
| if (objects.length > 0) { |
| @NonNls StringBuffer sb = null; |
| |
| for (Object o : objects) { |
| PsiElement parameter = null; |
| if (o instanceof PsiElement) { |
| parameter = (PsiElement)o; |
| } |
| |
| if (parameter != null) { |
| final SmartPsiElementPointer originalElement = parameter.getUserData(ORIGINAL_ELEMENT_KEY); |
| final String str2 = provider.generateDoc(parameter, originalElement != null ? originalElement.getElement() : null); |
| if (str2 == null) continue; |
| if (sb == null) sb = new StringBuffer(); |
| sb.append(str2); |
| sb.append("<br>"); |
| } |
| else { |
| sb = null; |
| break; |
| } |
| } |
| |
| if (sb != null) return sb.toString(); |
| } |
| return null; |
| } |
| |
| @Override |
| @Nullable |
| public PsiElement getElement() { |
| return myElement.isValid() ? myElement : null; |
| } |
| |
| @Nullable |
| @Override |
| public String getEffectiveExternalUrl() { |
| return myEffectiveUrl; |
| } |
| } |
| } |