IDEA-229503 Display javadoc rendered representation in editor

links in rendered documentation are now actionable

GitOrigin-RevId: 91ee3edb25e4b112d488780ac070462d49609312
This commit is contained in:
Dmitry Batrak
2020-02-18 15:14:40 +00:00
committed by intellij-monorepo-bot
parent 7151f3fca8
commit aa40a8a4b4
6 changed files with 234 additions and 29 deletions
@@ -173,6 +173,10 @@ public abstract class DockablePopupManager<T extends JComponent & Disposable> {
}
}
public void resetAutoUpdateState() {
restartAutoUpdate(PropertiesComponent.getInstance().getBoolean(getAutoUpdateEnabledProperty(), getAutoUpdateDefault()));
}
public void updateComponent() {
updateComponent(false);
}
@@ -22,10 +22,15 @@ import com.intellij.openapi.editor.markup.HighlighterTargetArea;
import com.intellij.openapi.editor.markup.RangeHighlighter;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Segment;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiDocCommentBase;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
@@ -47,7 +52,7 @@ class DocRenderItem {
final RangeHighlighter highlighter;
String textToRender;
private FoldRegion foldRegion;
private Inlay<DocRenderer> inlay;
Inlay<DocRenderer> inlay;
static boolean isValidRange(@NotNull Document document, @NotNull TextRange range) {
CharSequence text = document.getImmutableCharSequence();
@@ -74,7 +79,7 @@ class DocRenderItem {
boolean updated = false;
for (Iterator<DocRenderItem> it = items.iterator(); it.hasNext(); ) {
DocRenderItem item = it.next();
DocRenderPassFactory.Item matchingItem = item.isValid() ? itemsToSet.removeItem(item.getTextRange()) : null;
DocRenderPassFactory.Item matchingItem = item.isValid() ? itemsToSet.removeItem(item.highlighter) : null;
if (matchingItem == null) {
updated |= item.remove(foldingTasks);
it.remove();
@@ -133,6 +138,11 @@ class DocRenderItem {
}
}, connection);
editor.getCaretModel().addCaretListener(new MyCaretListener(), connection);
DocRenderMouseEventBridge mouseEventBridge = new DocRenderMouseEventBridge();
editor.addEditorMouseListener(mouseEventBridge, connection);
editor.addEditorMouseMotionListener(mouseEventBridge, connection);
editor.putUserData(LISTENERS_DISPOSABLE, connection);
}
}
@@ -153,10 +163,6 @@ class DocRenderItem {
highlighter.setGutterIconRenderer(new MyGutterIconRenderer(toggleAction));
}
private Segment getTextRange() {
return highlighter;
}
private int calcFoldStartOffset() {
Document document = highlighter.getDocument();
int startLine = document.getLineNumber(highlighter.getStartOffset());
@@ -261,6 +267,22 @@ class DocRenderItem {
return true;
}
PsiElement getOwner() {
Project project = editor.getProject();
if (project != null && highlighter.isValid()) {
PsiDocumentManager psiDocumentManager = PsiDocumentManager.getInstance(project);
PsiFile file = psiDocumentManager.getPsiFile(editor.getDocument());
if (file != null) {
PsiDocCommentBase comment = PsiTreeUtil.getParentOfType(file.findElementAt(highlighter.getStartOffset()), PsiDocCommentBase.class,
false);
if (comment != null) {
return comment.getOwner();
}
}
}
return null;
}
private static void updateInlays(@NotNull Editor editor) {
keepScrollingPositionWhile(editor, () -> {
AtomicBoolean updated = new AtomicBoolean();
@@ -0,0 +1,91 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.documentation.render;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.Inlay;
import com.intellij.openapi.editor.event.EditorMouseEvent;
import com.intellij.openapi.editor.event.EditorMouseEventArea;
import com.intellij.openapi.editor.event.EditorMouseListener;
import com.intellij.openapi.editor.event.EditorMouseMotionListener;
import com.intellij.openapi.editor.ex.EditorEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import sun.awt.AWTAccessor;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
class DocRenderMouseEventBridge implements EditorMouseListener, EditorMouseMotionListener {
private Inlay<? extends DocRenderer> myCurrentInlay;
@Override
public void mouseMoved(@NotNull EditorMouseEvent event) {
if (event.getArea() != EditorMouseEventArea.EDITING_AREA) return;
Inlay<? extends DocRenderer> currentInlay = redispatchEvent(event.getEditor(), event.getMouseEvent(), MouseEvent.MOUSE_MOVED);
if (currentInlay == null) {
restoreCursor();
}
else {
((EditorEx)event.getEditor()).setCustomCursor(DocRenderMouseEventBridge.class, currentInlay.getRenderer().myPane.getCursor());
if (currentInlay != myCurrentInlay) {
if (myCurrentInlay != null) {
dispatchMouseExitEvent(myCurrentInlay);
}
myCurrentInlay = currentInlay;
}
}
}
@Override
public void mouseExited(@NotNull EditorMouseEvent event) {
if (event.getArea() != EditorMouseEventArea.EDITING_AREA) return;
restoreCursor();
}
@Override
public void mouseClicked(@NotNull EditorMouseEvent event) {
if (event.getArea() != EditorMouseEventArea.EDITING_AREA) return;
redispatchEvent(event.getEditor(), event.getMouseEvent(), MouseEvent.MOUSE_CLICKED);
}
private void restoreCursor() {
if (myCurrentInlay != null) {
dispatchMouseExitEvent(myCurrentInlay);
((EditorEx)myCurrentInlay.getEditor()).setCustomCursor(DocRenderMouseEventBridge.class, null);
myCurrentInlay = null;
}
}
@Nullable
private static Inlay<? extends DocRenderer> redispatchEvent(@NotNull Editor editor, @NotNull MouseEvent mouseEvent, int eventId) {
Point mousePoint = mouseEvent.getPoint();
Inlay<? extends DocRenderer> inlay = editor.getInlayModel().getElementAt(mousePoint, DocRenderer.class);
if (inlay != null) {
DocRenderer renderer = inlay.getRenderer();
Point relativeLocation = renderer.getEditorPaneLocationWithinInlay();
Rectangle inlayBounds = inlay.getBounds();
assert inlayBounds != null;
int x = mousePoint.x - inlayBounds.x - relativeLocation.x;
int y = mousePoint.y - inlayBounds.y - relativeLocation.y;
JEditorPane editorPane = renderer.myPane;
if (x >= 0 && x < editorPane.getWidth() && y >= 0 && y < editorPane.getHeight()) {
dispatchEvent(inlay, new MouseEvent(editorPane, eventId, 0, 0, x, y, mouseEvent.getClickCount(), false, mouseEvent.getButton()));
return inlay;
}
}
return null;
}
private static void dispatchEvent(@NotNull Inlay<? extends DocRenderer> inlay, @NotNull MouseEvent event) {
DocRenderer renderer = inlay.getRenderer();
renderer.doWithRepaintTracking(() -> AWTAccessor.getComponentAccessor().processEvent(renderer.myPane, event));
}
private static void dispatchMouseExitEvent(@NotNull Inlay<? extends DocRenderer> inlay) {
dispatchEvent(inlay, new MouseEvent(inlay.getRenderer().myPane, MouseEvent.MOUSE_EXITED, 0, 0, 0, 0, 0, false));
}
}
@@ -2,7 +2,10 @@
package com.intellij.codeInsight.documentation.render;
import com.intellij.codeInsight.documentation.DocumentationComponent;
import com.intellij.codeInsight.documentation.DocumentationManager;
import com.intellij.codeInsight.documentation.QuickDocUtil;
import com.intellij.ide.ui.UISettings;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.editor.Document;
@@ -10,14 +13,20 @@ import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorCustomElementRenderer;
import com.intellij.openapi.editor.Inlay;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.event.CaretEvent;
import com.intellij.openapi.editor.event.CaretListener;
import com.intellij.openapi.editor.markup.GutterIconRenderer;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.ui.AppUIUtil;
import com.intellij.ui.ColorUtil;
import com.intellij.ui.JBColor;
import com.intellij.ui.popup.PopupFactoryImpl;
import com.intellij.ui.scale.JBUIScale;
import com.intellij.util.text.CharArrayUtil;
import com.intellij.util.ui.GraphicsUtil;
@@ -27,6 +36,9 @@ import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.View;
import javax.swing.text.html.ImageView;
import java.awt.*;
@@ -47,7 +59,8 @@ class DocRenderer implements EditorCustomElementRenderer {
private static final int TOP_BOTTOM_INSETS = 8;
private final DocRenderItem myItem;
private JEditorPane myPane;
private boolean myRepaintRequested;
JEditorPane myPane;
DocRenderer(DocRenderItem item) {
myItem = item;
@@ -136,11 +149,20 @@ class DocRenderer implements EditorCustomElementRenderer {
return Math.max(0, indentPixels - scale(LEFT_INSET));
}
Point getEditorPaneLocationWithinInlay() {
return new Point(calcInlayStartX() + scale(LEFT_INSET), scale(getTopMargin()) + scale(ARC_WIDTH));
}
private JComponent getRendererComponent(Inlay inlay, int width, int height) {
boolean newInstance = false;
if (myPane == null || Boolean.TRUE.equals(inlay.getUserData(RECREATE_COMPONENT) != null)) {
newInstance = true;
myPane = new JEditorPane(UIUtil.HTML_MIME, "");
myPane = new JEditorPane(UIUtil.HTML_MIME, "") {
@Override
public void repaint(long tm, int x, int y, int width, int height) {
myRepaintRequested = true;
}
};
myPane.setEditable(false);
myPane.putClientProperty("caretWidth", 0); // do not reserve space for caret (making content one pixel narrower than component)
myPane.setEditorKit(createEditorKit());
@@ -152,6 +174,11 @@ class DocRenderer implements EditorCustomElementRenderer {
myPane.setFont(myPane.getFont().deriveFont(fontAttributes));
myPane.setForeground(getColorFromRegistry("editor.render.doc.comments.fg"));
myPane.setText(myItem.textToRender);
myPane.addHyperlinkListener(e -> {
if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
activateLink(e);
}
});
inlay.putUserData(RECREATE_COMPONENT, null);
}
AppUIUtil.targetToDevice(myPane, inlay.getEditor().getContentComponent());
@@ -162,6 +189,52 @@ class DocRenderer implements EditorCustomElementRenderer {
return myPane;
}
private void activateLink(HyperlinkEvent event) {
Editor editor = myItem.editor;
Project project = editor.getProject();
Element element = event.getSourceElement();
if (project != null && element != null) {
Rectangle location = null;
try {
location = myPane.modelToView(element.getStartOffset());
}
catch (BadLocationException ignored) {}
PsiElement owner = myItem.getOwner();
if (owner != null && location != null) {
if (QuickDocUtil.getActiveDocComponent(project) == null) {
Point inlayPosition = Objects.requireNonNull(myItem.inlay.getBounds()).getLocation();
Point relativePosition = getEditorPaneLocationWithinInlay();
editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POINT,
new Point(inlayPosition.x + relativePosition.x + location.x,
inlayPosition.y + relativePosition.y + location.y + location.height));
}
DocumentationManager documentationManager = DocumentationManager.getInstance(project);
documentationManager.showJavaDocInfo(editor, owner, owner, () -> {
editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POINT, null);
}, "", false, true);
DocumentationComponent component = QuickDocUtil.getActiveDocComponent(project);
if (component != null) {
component.startWait();
documentationManager.navigateByLink(component, event.getDescription());
}
if (documentationManager.getDocInfoHint() == null) {
editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POINT, null);
}
if (documentationManager.hasActiveDockedDocWindow()) {
documentationManager.setAllowContentUpdateFromContext(false);
Disposable disposable = Disposer.newDisposable();
editor.getCaretModel().addCaretListener(new CaretListener() {
@Override
public void caretPositionChanged(@NotNull CaretEvent e) {
documentationManager.resetAutoUpdateState();
Disposer.dispose(disposable);
}
}, disposable);
}
}
}
}
private void trackImageUpdates(Inlay inlay) {
myPane.getPreferredSize(); // trigger internal layout
ImageObserver observer = (img, infoflags, x, y, width, height) -> {
@@ -190,6 +263,15 @@ class DocRenderer implements EditorCustomElementRenderer {
return result;
}
void doWithRepaintTracking(Runnable task) {
myRepaintRequested = false;
task.run();
Inlay<DocRenderer> inlay = myItem.inlay;
if (myRepaintRequested && inlay != null) {
inlay.repaint();
}
}
private static JBHtmlEditorKit createEditorKit() {
JBHtmlEditorKit editorKit = new JBHtmlEditorKit(true);
String editorFontName = StringUtil.escapeQuotes(EditorColorsManager.getInstance().getGlobalScheme().getEditorFontName());
@@ -3873,11 +3873,9 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
}
}
private boolean inlayWithCustomContextMenuExists(@NotNull Point point) {
private boolean hasBlockInlay(@NotNull Point point) {
Inlay inlay = myInlayModel.getElementAt(point);
if (inlay == null) return false;
EditorCustomElementRenderer renderer = inlay.getRenderer();
return renderer.getContextMenuGroupId(inlay) != null || renderer.getContextMenuGroup(inlay) != null;
return inlay != null && (inlay.getPlacement() == Inlay.Placement.ABOVE_LINE || inlay.getPlacement() == Inlay.Placement.BELOW_LINE);
}
private boolean processMousePressed(@NotNull final MouseEvent e) {
@@ -3951,8 +3949,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
// at 'line markers' area). Also, don't move caret when context menu for an inlay is invoked.
boolean moveCaret = eventArea == EditorMouseEventArea.LINE_NUMBERS_AREA ||
isInsideGutterWhitespaceArea(e) ||
eventArea == EditorMouseEventArea.EDITING_AREA &&
!(e.getButton() == MouseEvent.BUTTON3 && inlayWithCustomContextMenuExists(e.getPoint()));
eventArea == EditorMouseEventArea.EDITING_AREA && !hasBlockInlay(e.getPoint());
if (moveCaret) {
VisualPosition visualPosition = getTargetPosition(x, y, true);
LogicalPosition pos = visualToLogicalPosition(visualPosition);
@@ -60,6 +60,13 @@ public class PopupFactoryImpl extends JBPopupFactory {
* Primary intention for this key is to hint popup position for the non-caret location.
*/
public static final Key<VisualPosition> ANCHOR_POPUP_POSITION = Key.create("popup.anchor.position");
/**
* If corresponding value is defined for an {@link Editor}, popups shown for the editor will be located at specified point. This allows to
* show popups for non-default locations (caret location is used by default).
*
* @see JBPopupFactory#guessBestPopupLocation(Editor)
*/
public static final Key<Point> ANCHOR_POPUP_POINT = Key.create("popup.anchor.point");
private static final Logger LOG = Logger.getInstance(PopupFactoryImpl.class);
@@ -563,25 +570,27 @@ public class PopupFactoryImpl extends JBPopupFactory {
@Nullable
private static Point getVisibleBestPopupLocation(@NotNull Editor editor) {
VisualPosition visualPosition = editor.getUserData(ANCHOR_POPUP_POSITION);
int lineHeight = editor.getLineHeight();
Point p = editor.getUserData(ANCHOR_POPUP_POINT);
if (p == null) {
VisualPosition visualPosition = editor.getUserData(ANCHOR_POPUP_POSITION);
if (visualPosition == null) {
CaretModel caretModel = editor.getCaretModel();
if (caretModel.isUpToDate()) {
visualPosition = caretModel.getVisualPosition();
}
else {
visualPosition = editor.offsetToVisualPosition(caretModel.getOffset());
if (visualPosition == null) {
CaretModel caretModel = editor.getCaretModel();
if (caretModel.isUpToDate()) {
visualPosition = caretModel.getVisualPosition();
}
else {
visualPosition = editor.offsetToVisualPosition(caretModel.getOffset());
}
}
p = editor.visualPositionToXY(visualPosition);
p.y += lineHeight;
}
final int lineHeight = editor.getLineHeight();
Point p = editor.visualPositionToXY(visualPosition);
p.y += lineHeight;
final Rectangle visibleArea = editor.getScrollingModel().getVisibleArea();
return !visibleArea.contains(p) && !visibleArea.contains(p.x, p.y - lineHeight)
? null : p;
return !visibleArea.contains(p) && !visibleArea.contains(p.x, p.y - lineHeight) ? null : p;
}
@Override