mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IDEA-229503 Display javadoc rendered representation in editor
links in rendered documentation are now actionable GitOrigin-RevId: 91ee3edb25e4b112d488780ac070462d49609312
This commit is contained in:
committed by
intellij-monorepo-bot
parent
7151f3fca8
commit
aa40a8a4b4
+4
@@ -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);
|
||||
}
|
||||
|
||||
+29
-7
@@ -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();
|
||||
|
||||
+91
@@ -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));
|
||||
}
|
||||
}
|
||||
+84
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user